mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-01 20:33:14 +08:00
During in-container updates, the old version of the application continues running while an update is applied. That means that it's possible for the `node_modules/ember-source` version to be different to the version currently loaded in the transpiler. That means it's theoretically possible for theme assets to be built with the old compiler, and then stored against the new version. This commit removes that race condition by adding an `ember_version` method to the JS transpiler. This is guaranteed to give us an accurate version number for the template-compiler currently being used for themes. This commit also bumps the BASE_COMPILER_VERSION to force a recompile on any sites affected by this race condition.
174 lines
4.8 KiB
Ruby
174 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class DiscourseJsProcessor
|
|
class TranspileError < StandardError
|
|
end
|
|
|
|
def self.transpile(data, root_path, logical_path, theme_id: nil, extension: nil)
|
|
transpiler = Transpiler.new(skip_module: skip_module?(data))
|
|
transpiler.perform(data, root_path, logical_path, theme_id: theme_id, extension: extension)
|
|
end
|
|
|
|
def self.skip_module?(data)
|
|
!!(data.present? && data =~ %r{^// discourse-skip-module$})
|
|
end
|
|
|
|
class Transpiler
|
|
TRANSPILER_PATH = "tmp/theme-transpiler.js"
|
|
|
|
@mutex = Mutex.new
|
|
@ctx_init = Mutex.new
|
|
@processor_mutex = Mutex.new
|
|
|
|
def self.mutex
|
|
@mutex
|
|
end
|
|
|
|
def self.build_theme_transpiler
|
|
Discourse::Utils.execute_command(
|
|
"pnpm",
|
|
"-C=app/assets/javascripts/theme-transpiler",
|
|
"node",
|
|
"build.js",
|
|
)
|
|
end
|
|
|
|
def self.build_production_theme_transpiler
|
|
File.write(TRANSPILER_PATH, build_theme_transpiler)
|
|
TRANSPILER_PATH
|
|
end
|
|
|
|
def self.create_new_context
|
|
# timeout any eval that takes longer than 15 seconds
|
|
ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000)
|
|
|
|
# General shims
|
|
ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
|
|
ctx.attach("rails.logger.warn", proc { |err| Rails.logger.warn(err.to_s) })
|
|
ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) })
|
|
|
|
source =
|
|
if Rails.env.production?
|
|
File.read(TRANSPILER_PATH)
|
|
else
|
|
@processor_mutex.synchronize { build_theme_transpiler }
|
|
end
|
|
|
|
ctx.eval(source, filename: "theme-transpiler.js")
|
|
|
|
ctx
|
|
end
|
|
|
|
def self.reset_context
|
|
@ctx&.dispose
|
|
@ctx = nil
|
|
end
|
|
|
|
def self.v8
|
|
return @ctx if @ctx
|
|
|
|
# ensure we only init one of these
|
|
@ctx_init.synchronize do
|
|
return @ctx if @ctx
|
|
@ctx = create_new_context
|
|
end
|
|
|
|
@ctx
|
|
end
|
|
|
|
# Call a method in the global scope of the v8 context.
|
|
# The `fetch_result_call` kwarg provides a workaround for the lack of mini_racer async
|
|
# result support. The first call can perform some async operation, and then `fetch_result_call`
|
|
# will be called to fetch the result.
|
|
def self.v8_call(*args, **kwargs)
|
|
fetch_result_call = kwargs.delete(:fetch_result_call)
|
|
mutex.synchronize do
|
|
result = v8.call(*args, **kwargs)
|
|
result = v8.call(fetch_result_call) if fetch_result_call
|
|
result
|
|
end
|
|
rescue MiniRacer::RuntimeError => e
|
|
message = e.message
|
|
begin
|
|
# Workaround for https://github.com/rubyjs/mini_racer/issues/262
|
|
possible_encoded_message = message.delete_prefix("Error: ")
|
|
decoded = JSON.parse("{\"value\": #{possible_encoded_message}}")["value"]
|
|
message = "Error: #{decoded}"
|
|
rescue JSON::ParserError
|
|
message = e.message
|
|
end
|
|
transpile_error = TranspileError.new(message)
|
|
transpile_error.set_backtrace(e.backtrace)
|
|
raise transpile_error
|
|
end
|
|
|
|
def initialize(skip_module: false)
|
|
@skip_module = skip_module
|
|
end
|
|
|
|
def perform(
|
|
source,
|
|
root_path = nil,
|
|
logical_path = nil,
|
|
theme_id: nil,
|
|
extension: nil,
|
|
generate_map: false
|
|
)
|
|
self.class.v8_call(
|
|
"transpile",
|
|
source,
|
|
{
|
|
skipModule: @skip_module,
|
|
moduleId: module_name(root_path, logical_path),
|
|
filename: logical_path || "unknown",
|
|
extension: extension,
|
|
themeId: theme_id,
|
|
generateMap: generate_map,
|
|
},
|
|
)
|
|
end
|
|
|
|
def module_name(root_path, logical_path)
|
|
path = nil
|
|
|
|
root_base = File.basename(Rails.root)
|
|
# If the resource is a plugin, use the plugin name as a prefix
|
|
if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/}
|
|
plugin_path = "#{Regexp.last_match[1]}/plugin.rb"
|
|
|
|
plugin = Discourse.plugins.find { |p| p.path == plugin_path }
|
|
path =
|
|
"discourse/plugins/#{plugin.name}/#{logical_path.sub(%r{javascripts/}, "")}" if plugin
|
|
end
|
|
|
|
# We need to strip the app subdirectory to replicate how ember-cli works.
|
|
path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin")
|
|
end
|
|
|
|
def compile_raw_template(source, theme_id: nil)
|
|
self.class.v8_call("compileRawTemplate", source, theme_id)
|
|
end
|
|
|
|
def terser(tree, opts)
|
|
self.class.v8_call("minify", tree, opts, fetch_result_call: "getMinifyResult")
|
|
end
|
|
|
|
def rollup(tree, opts)
|
|
self.class.v8_call("rollup", tree, opts, fetch_result_call: "getRollupResult")
|
|
end
|
|
|
|
def post_css(css:, map:, source_map_file:)
|
|
self.class.v8_call(
|
|
"postCss",
|
|
css,
|
|
map,
|
|
source_map_file,
|
|
fetch_result_call: "getPostCssResult",
|
|
)
|
|
end
|
|
|
|
def ember_version
|
|
self.class.v8_call("emberVersion")
|
|
end
|
|
end
|
|
end
|