2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
2014-05-06 06:04:09 +08:00
|
|
|
require "execjs"
|
2016-05-19 20:25:08 +08:00
|
|
|
require "mini_racer"
|
2014-05-06 06:04:09 +08:00
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
class DiscourseJsProcessor
|
2022-09-01 18:50:46 +08:00
|
|
|
class TranspileError < StandardError
|
|
|
|
end
|
2020-10-13 21:58:08 +08:00
|
|
|
|
DEV: Allow Ember CLI assets to be used by development Rails app (#16511)
Previously, accessing the Rails app directly in development mode would give you assets from our 'legacy' Ember asset pipeline. The only way to run with Ember CLI assets was to run ember-cli as a proxy. This was quite limiting when working on things which are bypassed when using the ember-cli proxy (e.g. changes to `application.html.erb`). Also, since `ember-auto-import` introduced chunking, visiting `/theme-qunit` under Ember CLI was failing to include all necessary chunks.
This commit teaches Sprockets about our Ember CLI assets so that they can be used in development mode, and are automatically collected up under `/public/assets` during `assets:precompile`. As a bonus, this allows us to remove all the custom manifest modification from `assets:precompile`.
The key changes are:
- Introduce a shared `EmberCli.enabled?` helper
- When ember-cli is enabled, add ember-cli `/dist/assets` as the top-priority Rails asset directory
- Have ember-cli output a `chunks.json` manifest, and teach `preload_script` to read it and append the correct chunks to their associated `afterFile`
- Remove most custom ember-cli logic from the `assets:precompile` step. Instead, rely on Rails to take care of pulling the 'precompiled' assets into the `public/assets` directory. Move the 'renaming' logic to runtime, so it can be used in development mode as well.
- Remove fingerprinting from `ember-cli-build`, and allow Rails to take care of things
Long-term, we may want to replace Sprockets with the lighter-weight Propshaft. The changes made in this commit have been made with that long-term goal in mind.
tldr: when you visit the rails app directly, you'll now be served the current ember-cli assets. To keep these up-to-date make sure either `ember serve`, or `ember build --watch` is running. If you really want to load the old non-ember-cli assets, then you should start the server with `EMBER_CLI_PROD_ASSETS=0`. (the legacy asset pipeline will be removed very soon)
2022-04-21 23:26:34 +08:00
|
|
|
def self.ember_cli?(filename)
|
|
|
|
filename.include?("/app/assets/javascripts/discourse/dist/")
|
|
|
|
end
|
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
def self.call(input)
|
|
|
|
root_path = input[:load_path] || ""
|
|
|
|
logical_path =
|
|
|
|
(input[:filename] || "").sub(root_path, "").gsub(/\.(js|es6).*$/, "").sub(%r{^/}, "")
|
|
|
|
data = input[:data]
|
2014-05-06 06:04:09 +08:00
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
data = transpile(data, root_path, logical_path) if should_transpile?(input[:filename])
|
2017-04-17 22:11:51 +08:00
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
{ data: data }
|
|
|
|
end
|
|
|
|
|
2023-10-02 18:36:06 +08:00
|
|
|
def self.transpile(data, root_path, logical_path, theme_id: nil, extension: nil)
|
2020-03-11 21:43:55 +08:00
|
|
|
transpiler = Transpiler.new(skip_module: skip_module?(data))
|
2023-10-02 18:36:06 +08:00
|
|
|
transpiler.perform(data, root_path, logical_path, theme_id: theme_id, extension: extension)
|
2020-03-11 21:43:55 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.should_transpile?(filename)
|
|
|
|
filename ||= ""
|
|
|
|
|
DEV: Allow Ember CLI assets to be used by development Rails app (#16511)
Previously, accessing the Rails app directly in development mode would give you assets from our 'legacy' Ember asset pipeline. The only way to run with Ember CLI assets was to run ember-cli as a proxy. This was quite limiting when working on things which are bypassed when using the ember-cli proxy (e.g. changes to `application.html.erb`). Also, since `ember-auto-import` introduced chunking, visiting `/theme-qunit` under Ember CLI was failing to include all necessary chunks.
This commit teaches Sprockets about our Ember CLI assets so that they can be used in development mode, and are automatically collected up under `/public/assets` during `assets:precompile`. As a bonus, this allows us to remove all the custom manifest modification from `assets:precompile`.
The key changes are:
- Introduce a shared `EmberCli.enabled?` helper
- When ember-cli is enabled, add ember-cli `/dist/assets` as the top-priority Rails asset directory
- Have ember-cli output a `chunks.json` manifest, and teach `preload_script` to read it and append the correct chunks to their associated `afterFile`
- Remove most custom ember-cli logic from the `assets:precompile` step. Instead, rely on Rails to take care of pulling the 'precompiled' assets into the `public/assets` directory. Move the 'renaming' logic to runtime, so it can be used in development mode as well.
- Remove fingerprinting from `ember-cli-build`, and allow Rails to take care of things
Long-term, we may want to replace Sprockets with the lighter-weight Propshaft. The changes made in this commit have been made with that long-term goal in mind.
tldr: when you visit the rails app directly, you'll now be served the current ember-cli assets. To keep these up-to-date make sure either `ember serve`, or `ember build --watch` is running. If you really want to load the old non-ember-cli assets, then you should start the server with `EMBER_CLI_PROD_ASSETS=0`. (the legacy asset pipeline will be removed very soon)
2022-04-21 23:26:34 +08:00
|
|
|
# skip ember cli
|
|
|
|
return false if ember_cli?(filename)
|
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
# es6 is always transpiled
|
|
|
|
return true if filename.end_with?(".es6") || filename.end_with?(".es6.erb")
|
|
|
|
|
|
|
|
# For .js check the path...
|
|
|
|
return false unless filename.end_with?(".js") || filename.end_with?(".js.erb")
|
|
|
|
|
|
|
|
relative_path = filename.sub(Rails.root.to_s, "").sub(%r{^/*}, "")
|
2020-03-27 00:22:33 +08:00
|
|
|
|
|
|
|
js_root = "app/assets/javascripts"
|
|
|
|
test_root = "test/javascripts"
|
|
|
|
|
|
|
|
return false if relative_path.start_with?("#{js_root}/locales/")
|
|
|
|
return false if relative_path.start_with?("#{js_root}/plugins/")
|
|
|
|
|
2020-03-27 00:47:10 +08:00
|
|
|
!!(relative_path =~ %r{^#{js_root}/[^/]+/} || relative_path =~ %r{^#{test_root}/[^/]+/})
|
2020-03-11 21:43:55 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.skip_module?(data)
|
|
|
|
!!(data.present? && data =~ %r{^// discourse-skip-module$})
|
|
|
|
end
|
|
|
|
|
|
|
|
class Transpiler
|
2024-08-02 00:59:34 +08:00
|
|
|
TRANSPILER_PATH = "tmp/theme-transpiler.js"
|
2023-08-25 17:44:30 +08:00
|
|
|
|
2020-03-11 21:43:55 +08:00
|
|
|
@mutex = Mutex.new
|
|
|
|
@ctx_init = Mutex.new
|
2023-08-24 22:36:22 +08:00
|
|
|
@processor_mutex = Mutex.new
|
2020-03-11 21:43:55 +08:00
|
|
|
|
|
|
|
def self.mutex
|
|
|
|
@mutex
|
2014-05-06 06:04:09 +08:00
|
|
|
end
|
|
|
|
|
2023-10-02 18:36:06 +08:00
|
|
|
def self.build_theme_transpiler
|
2024-08-02 00:59:34 +08:00
|
|
|
FileUtils.rm_rf("tmp/theme-transpiler") # cleanup old files - remove after Jan 2025
|
2024-09-03 17:51:07 +08:00
|
|
|
Discourse::Utils.execute_command(
|
|
|
|
"pnpm",
|
|
|
|
"-C=app/assets/javascripts/theme-transpiler",
|
|
|
|
"node",
|
|
|
|
"build.js",
|
|
|
|
)
|
2024-08-02 00:59:34 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.build_production_theme_transpiler
|
|
|
|
File.write(TRANSPILER_PATH, build_theme_transpiler)
|
2023-10-02 18:36:06 +08:00
|
|
|
TRANSPILER_PATH
|
2022-08-30 02:53:42 +08:00
|
|
|
end
|
|
|
|
|
2014-05-06 06:04:09 +08:00
|
|
|
def self.create_new_context
|
2016-04-22 07:52:12 +08:00
|
|
|
# timeout any eval that takes longer than 15 seconds
|
2020-05-15 12:01:54 +08:00
|
|
|
ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000)
|
2022-08-30 02:53:42 +08:00
|
|
|
|
|
|
|
# General shims
|
2016-05-19 20:25:08 +08:00
|
|
|
ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
|
2022-08-30 02:53:42 +08:00
|
|
|
ctx.attach("rails.logger.warn", proc { |err| Rails.logger.warn(err.to_s) })
|
2016-05-19 20:25:08 +08:00
|
|
|
ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) })
|
2022-09-01 18:50:46 +08:00
|
|
|
|
2024-08-02 00:59:34 +08:00
|
|
|
source =
|
|
|
|
if Rails.env.production?
|
|
|
|
File.read(TRANSPILER_PATH)
|
|
|
|
else
|
|
|
|
@processor_mutex.synchronize { build_theme_transpiler }
|
|
|
|
end
|
2023-08-24 19:19:57 +08:00
|
|
|
|
2024-08-02 00:59:34 +08:00
|
|
|
ctx.eval(source, filename: "theme-transpiler.js")
|
2017-06-30 04:22:19 +08:00
|
|
|
|
2014-05-06 06:04:09 +08:00
|
|
|
ctx
|
|
|
|
end
|
|
|
|
|
2016-11-02 10:34:20 +08:00
|
|
|
def self.reset_context
|
2017-07-20 12:17:45 +08:00
|
|
|
@ctx&.dispose
|
2016-11-02 10:34:20 +08:00
|
|
|
@ctx = nil
|
|
|
|
end
|
|
|
|
|
2014-05-06 06:04:09 +08: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-19 01:20:10 +08: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 18:50:46 +08:00
|
|
|
def self.v8_call(*args, **kwargs)
|
2022-10-19 01:20:10 +08:00
|
|
|
fetch_result_call = kwargs.delete(:fetch_result_call)
|
2022-09-01 18:50:46 +08:00
|
|
|
mutex.synchronize do
|
2022-10-19 01:20:10 +08:00
|
|
|
result = v8.call(*args, **kwargs)
|
|
|
|
result = v8.call(fetch_result_call) if fetch_result_call
|
|
|
|
result
|
2022-09-01 18:50:46 +08: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-19 02:41:27 +08:00
|
|
|
end
|
|
|
|
|
2022-09-01 18:50:46 +08:00
|
|
|
def initialize(skip_module: false)
|
|
|
|
@skip_module = skip_module
|
2016-06-15 02:31:51 +08:00
|
|
|
end
|
|
|
|
|
2023-10-02 18:36:06 +08:00
|
|
|
def perform(source, root_path = nil, logical_path = nil, theme_id: nil, extension: nil)
|
2022-09-01 18:50:46 +08:00
|
|
|
self.class.v8_call(
|
|
|
|
"transpile",
|
|
|
|
source,
|
|
|
|
{
|
2023-04-25 00:39:02 +08:00
|
|
|
skipModule: @skip_module,
|
2022-09-01 18:50:46 +08:00
|
|
|
moduleId: module_name(root_path, logical_path),
|
|
|
|
filename: logical_path || "unknown",
|
2023-10-02 18:36:06 +08:00
|
|
|
extension: extension,
|
2022-09-01 18:50:46 +08:00
|
|
|
themeId: theme_id,
|
|
|
|
},
|
|
|
|
)
|
2016-03-19 02:41:27 +08:00
|
|
|
end
|
|
|
|
|
2014-05-06 06:04:09 +08:00
|
|
|
def module_name(root_path, logical_path)
|
2014-05-16 04:31:45 +08:00
|
|
|
path = nil
|
|
|
|
|
2014-05-21 04:54:59 +08:00
|
|
|
root_base = File.basename(Rails.root)
|
2014-05-16 04:31:45 +08:00
|
|
|
# If the resource is a plugin, use the plugin name as a prefix
|
2014-05-21 04:54:59 +08:00
|
|
|
if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/}
|
2014-05-16 04:31:45 +08: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-06 06:04:09 +08:00
|
|
|
end
|
|
|
|
|
2017-06-30 04:22:19 +08:00
|
|
|
# We need to strip the app subdirectory to replicate how ember-cli works.
|
2020-04-30 00:18:21 +08:00
|
|
|
path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin")
|
2014-05-06 06:04:09 +08:00
|
|
|
end
|
|
|
|
|
2022-09-01 18:50:46 +08:00
|
|
|
def compile_raw_template(source, theme_id: nil)
|
|
|
|
self.class.v8_call("compileRawTemplate", source, theme_id)
|
|
|
|
end
|
|
|
|
|
2022-10-19 01:20:10 +08:00
|
|
|
def terser(tree, opts)
|
|
|
|
self.class.v8_call("minify", tree, opts, fetch_result_call: "getMinifyResult")
|
|
|
|
end
|
2014-05-06 06:04:09 +08:00
|
|
|
end
|
|
|
|
end
|