2019-05-03 08:17:27 +10:00
|
|
|
# frozen_string_literal: true
|
2014-05-05 18:04:09 -04:00
|
|
|
|
2020-03-11 09:43:55 -04:00
|
|
|
class DiscourseJsProcessor
|
2022-09-01 11:50:46 +01:00
|
|
|
class TranspileError < StandardError
|
|
|
|
end
|
2020-10-13 15:58:08 +02:00
|
|
|
|
2023-10-02 12:36:06 +02:00
|
|
|
def self.transpile(data, root_path, logical_path, theme_id: nil, extension: nil)
|
2020-03-11 09:43:55 -04:00
|
|
|
transpiler = Transpiler.new(skip_module: skip_module?(data))
|
2023-10-02 12:36:06 +02:00
|
|
|
transpiler.perform(data, root_path, logical_path, theme_id: theme_id, extension: extension)
|
2020-03-11 09:43:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.skip_module?(data)
|
|
|
|
!!(data.present? && data =~ %r{^// discourse-skip-module$})
|
|
|
|
end
|
|
|
|
|
|
|
|
class Transpiler
|
2024-08-01 17:59:34 +01:00
|
|
|
TRANSPILER_PATH = "tmp/theme-transpiler.js"
|
2023-08-25 11:44:30 +02:00
|
|
|
|
2020-03-11 09:43:55 -04:00
|
|
|
@mutex = Mutex.new
|
|
|
|
@ctx_init = Mutex.new
|
2023-08-24 16:36:22 +02:00
|
|
|
@processor_mutex = Mutex.new
|
2020-03-11 09:43:55 -04:00
|
|
|
|
|
|
|
def self.mutex
|
|
|
|
@mutex
|
2014-05-05 18:04:09 -04:00
|
|
|
end
|
|
|
|
|
2023-10-02 12:36:06 +02:00
|
|
|
def self.build_theme_transpiler
|
2024-09-03 10:51:07 +01:00
|
|
|
Discourse::Utils.execute_command(
|
|
|
|
"pnpm",
|
|
|
|
"-C=app/assets/javascripts/theme-transpiler",
|
|
|
|
"node",
|
|
|
|
"build.js",
|
|
|
|
)
|
2024-08-01 17:59:34 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.build_production_theme_transpiler
|
|
|
|
File.write(TRANSPILER_PATH, build_theme_transpiler)
|
2023-10-02 12:36:06 +02:00
|
|
|
TRANSPILER_PATH
|
2022-08-29 19:53:42 +01:00
|
|
|
end
|
|
|
|
|
2014-05-05 18:04:09 -04:00
|
|
|
def self.create_new_context
|
2016-04-21 16:52:12 -07:00
|
|
|
# timeout any eval that takes longer than 15 seconds
|
2020-05-15 14:01:54 +10:00
|
|
|
ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000)
|
2022-08-29 19:53:42 +01:00
|
|
|
|
|
|
|
# General shims
|
2016-05-19 22:25:08 +10:00
|
|
|
ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
|
2022-08-29 19:53:42 +01:00
|
|
|
ctx.attach("rails.logger.warn", proc { |err| Rails.logger.warn(err.to_s) })
|
2016-05-19 22:25:08 +10:00
|
|
|
ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) })
|
2022-09-01 11:50:46 +01:00
|
|
|
|
2024-08-01 17:59:34 +01:00
|
|
|
source =
|
|
|
|
if Rails.env.production?
|
|
|
|
File.read(TRANSPILER_PATH)
|
|
|
|
else
|
|
|
|
@processor_mutex.synchronize { build_theme_transpiler }
|
|
|
|
end
|
2023-08-24 13:19:57 +02:00
|
|
|
|
DEV: Use rollup for theme JS compilation (#33103)
This commit is a complete reimplementation of our theme JS compilation
system.
Previously, we compiled theme JS into AMD `define` statements on a
per-source-file basis, and then concatenated them together for the
client. These AMD modules would integrate with those in Discourse core,
allowing two way access between core/theme modules. Going forward, we'll
be moving away from AMD, and towards native ES modules in core. Before
we can do that, we need to stop relying on AMD as the 'glue' between
core and themes/plugins.
This change introduces Rollup (running in mini-racer) as a compiler for
theme JS. This is configured to generate a single ES Module which
exports a list of 'compat modules'. Core `import()`s the modules for
each active theme, and adds them all to AMD. In future, this consumption
can be updated to avoid AMD entirely.
All module resolution within a theme is handled by Rollup, and does not
use AMD.
Import of core/plugin modules from themes are automatically transformed
into calls to a new `window.moduleBroker` interface. For now, this is a
direct interface to AMD. In future, this can be updated to point to real
ES Modules in core.
Despite the complete overhaul of the internals, this is not a breaking
change, and should have no impact on existing themes. If any
incompatibilities are found, please report them on
https://meta.discourse.org.
---------
Co-authored-by: Jarek Radosz <jarek@cvx.dev>
Co-authored-by: Chris Manson <chris@manson.ie>
2025-07-25 12:02:29 +01:00
|
|
|
# source = File.read("app/assets/javascripts/theme-transpiler/theme-transpiler.js")
|
|
|
|
|
2024-08-01 17:59:34 +01:00
|
|
|
ctx.eval(source, filename: "theme-transpiler.js")
|
2017-06-29 16:22:19 -04:00
|
|
|
|
2014-05-05 18:04:09 -04:00
|
|
|
ctx
|
|
|
|
end
|
|
|
|
|
2016-11-02 13:34:20 +11:00
|
|
|
def self.reset_context
|
2017-07-20 13:17:45 +09:00
|
|
|
@ctx&.dispose
|
2016-11-02 13:34:20 +11:00
|
|
|
@ctx = nil
|
|
|
|
end
|
|
|
|
|
2014-05-05 18:04:09 -04:00
|
|
|
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
|
|
|
|
|
2022-10-18 18:20:10 +01:00
|
|
|
# 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.
|
2022-09-01 11:50:46 +01:00
|
|
|
def self.v8_call(*args, **kwargs)
|
2022-10-18 18:20:10 +01:00
|
|
|
fetch_result_call = kwargs.delete(:fetch_result_call)
|
2022-09-01 11:50:46 +01:00
|
|
|
mutex.synchronize do
|
2022-10-18 18:20:10 +01:00
|
|
|
result = v8.call(*args, **kwargs)
|
|
|
|
result = v8.call(fetch_result_call) if fetch_result_call
|
|
|
|
result
|
2022-09-01 11:50:46 +01:00
|
|
|
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
|
2016-03-18 14:41:27 -04:00
|
|
|
end
|
|
|
|
|
2022-09-01 11:50:46 +01:00
|
|
|
def initialize(skip_module: false)
|
|
|
|
@skip_module = skip_module
|
2016-06-14 14:31:51 -04:00
|
|
|
end
|
|
|
|
|
2025-07-16 17:17:36 +01:00
|
|
|
def perform(
|
|
|
|
source,
|
|
|
|
root_path = nil,
|
|
|
|
logical_path = nil,
|
|
|
|
theme_id: nil,
|
|
|
|
extension: nil,
|
|
|
|
generate_map: false
|
|
|
|
)
|
2022-09-01 11:50:46 +01:00
|
|
|
self.class.v8_call(
|
|
|
|
"transpile",
|
|
|
|
source,
|
|
|
|
{
|
2023-04-24 17:39:02 +01:00
|
|
|
skipModule: @skip_module,
|
2022-09-01 11:50:46 +01:00
|
|
|
moduleId: module_name(root_path, logical_path),
|
|
|
|
filename: logical_path || "unknown",
|
2023-10-02 12:36:06 +02:00
|
|
|
extension: extension,
|
2022-09-01 11:50:46 +01:00
|
|
|
themeId: theme_id,
|
2025-07-16 17:17:36 +01:00
|
|
|
generateMap: generate_map,
|
2022-09-01 11:50:46 +01:00
|
|
|
},
|
|
|
|
)
|
2016-03-18 14:41:27 -04:00
|
|
|
end
|
|
|
|
|
2014-05-05 18:04:09 -04:00
|
|
|
def module_name(root_path, logical_path)
|
2014-05-15 16:31:45 -04:00
|
|
|
path = nil
|
|
|
|
|
2014-05-20 16:54:59 -04:00
|
|
|
root_base = File.basename(Rails.root)
|
2014-05-15 16:31:45 -04:00
|
|
|
# If the resource is a plugin, use the plugin name as a prefix
|
2014-05-20 16:54:59 -04:00
|
|
|
if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/}
|
2014-05-15 16:31:45 -04:00
|
|
|
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
|
2014-05-05 18:04:09 -04:00
|
|
|
end
|
|
|
|
|
2017-06-29 16:22:19 -04:00
|
|
|
# We need to strip the app subdirectory to replicate how ember-cli works.
|
2020-04-29 12:18:21 -04:00
|
|
|
path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin")
|
2014-05-05 18:04:09 -04:00
|
|
|
end
|
|
|
|
|
2022-09-01 11:50:46 +01:00
|
|
|
def compile_raw_template(source, theme_id: nil)
|
|
|
|
self.class.v8_call("compileRawTemplate", source, theme_id)
|
|
|
|
end
|
|
|
|
|
2022-10-18 18:20:10 +01:00
|
|
|
def terser(tree, opts)
|
|
|
|
self.class.v8_call("minify", tree, opts, fetch_result_call: "getMinifyResult")
|
|
|
|
end
|
2025-02-20 14:40:27 +00:00
|
|
|
|
DEV: Use rollup for theme JS compilation (#33103)
This commit is a complete reimplementation of our theme JS compilation
system.
Previously, we compiled theme JS into AMD `define` statements on a
per-source-file basis, and then concatenated them together for the
client. These AMD modules would integrate with those in Discourse core,
allowing two way access between core/theme modules. Going forward, we'll
be moving away from AMD, and towards native ES modules in core. Before
we can do that, we need to stop relying on AMD as the 'glue' between
core and themes/plugins.
This change introduces Rollup (running in mini-racer) as a compiler for
theme JS. This is configured to generate a single ES Module which
exports a list of 'compat modules'. Core `import()`s the modules for
each active theme, and adds them all to AMD. In future, this consumption
can be updated to avoid AMD entirely.
All module resolution within a theme is handled by Rollup, and does not
use AMD.
Import of core/plugin modules from themes are automatically transformed
into calls to a new `window.moduleBroker` interface. For now, this is a
direct interface to AMD. In future, this can be updated to point to real
ES Modules in core.
Despite the complete overhaul of the internals, this is not a breaking
change, and should have no impact on existing themes. If any
incompatibilities are found, please report them on
https://meta.discourse.org.
---------
Co-authored-by: Jarek Radosz <jarek@cvx.dev>
Co-authored-by: Chris Manson <chris@manson.ie>
2025-07-25 12:02:29 +01:00
|
|
|
def rollup(tree, opts)
|
|
|
|
self.class.v8_call("rollup", tree, opts, fetch_result_call: "getRollupResult")
|
|
|
|
end
|
|
|
|
|
2025-02-20 14:40:27 +00:00
|
|
|
def post_css(css:, map:, source_map_file:)
|
|
|
|
self.class.v8_call(
|
|
|
|
"postCss",
|
|
|
|
css,
|
|
|
|
map,
|
|
|
|
source_map_file,
|
|
|
|
fetch_result_call: "getPostCssResult",
|
|
|
|
)
|
|
|
|
end
|
2014-05-05 18:04:09 -04:00
|
|
|
end
|
|
|
|
end
|