DEV: perform theme extra_js compilation all together

Previously, compiling theme 'extra_js' was done with a number of steps. Each theme_field would be compiled into its own value_baked column, and then the JavascriptCache content would be built by concatenating all of those compiled values.

This commit streamlines things by removing the value_baked step. The raw value of all extra_js theme_fields are passed directly to the ThemeJavascriptCompiler, and then the result is stored in the JavascriptCache.

In itself, this commit should not cause any behavior change. It is designed to open the door to more advanced compilation features which have interdependencies between different source files (e.g. template colocation, sourcemaps).
This commit is contained in:
David Taylor 2022-10-17 15:04:04 +01:00
parent 9879cb0e68
commit 65a5c84a92
6 changed files with 68 additions and 48 deletions

View File

@ -118,12 +118,12 @@ class Theme < ActiveRecord::Base
all_extra_js = theme_fields all_extra_js = theme_fields
.where(target_id: Theme.targets[:extra_js]) .where(target_id: Theme.targets[:extra_js])
.order(:name, :id) .order(:name, :id)
.pluck(:value_baked) .pluck(:name, :value)
.join("\n") .to_h
if all_extra_js.present? if all_extra_js.present?
js_compiler = ThemeJavascriptCompiler.new(id, name) js_compiler = ThemeJavascriptCompiler.new(id, name)
js_compiler.append_raw_script(all_extra_js) js_compiler.append_tree(all_extra_js)
settings_hash = build_settings_hash settings_hash = build_settings_hash
js_compiler.prepend_settings(settings_hash) if settings_hash.present? js_compiler.prepend_settings(settings_hash) if settings_hash.present?
javascript_cache || build_javascript_cache javascript_cache || build_javascript_cache
@ -707,14 +707,17 @@ class Theme < ActiveRecord::Base
end end
def baked_js_tests_with_digest def baked_js_tests_with_digest
content = theme_fields tests_tree = theme_fields
.where(target_id: Theme.targets[:tests_js]) .where(target_id: Theme.targets[:tests_js])
.order(name: :asc) .order(name: :asc)
.each(&:ensure_baked!) .pluck(:name, :value)
.map(&:value_baked) .to_h
.join("\n")
return [nil, nil] if content.blank? return [nil, nil] if tests_tree.blank?
compiler = ThemeJavascriptCompiler.new(id, name)
compiler.append_tree(tests_tree, for_tests: true)
content = compiler.content
content = <<~JS + content content = <<~JS + content
(function() { (function() {

View File

@ -153,30 +153,6 @@ class ThemeField < ActiveRecord::Base
[doc.to_s, errors&.join("\n")] [doc.to_s, errors&.join("\n")]
end end
def process_extra_js(content)
errors = []
js_compiler = ThemeJavascriptCompiler.new(theme_id, theme.name)
filename, extension = name.split(".", 2)
filename = "test/#{filename}" if js_tests_field?
begin
case extension
when "js.es6", "js"
js_compiler.append_module(content, filename, include_variables: true)
when "hbs"
js_compiler.append_ember_template(filename, content)
when "hbr", "raw.hbs"
js_compiler.append_raw_template(filename.sub("discourse/templates/", ""), content)
else
raise ThemeJavascriptCompiler::CompileError.new(I18n.t("themes.compile_error.unrecognized_extension", extension: extension))
end
rescue ThemeJavascriptCompiler::CompileError => ex
errors << ex.message
end
[js_compiler.content, errors&.join("\n")]
end
def validate_svg_sprite_xml def validate_svg_sprite_xml
upload = Upload.find(self.upload_id) rescue nil upload = Upload.find(self.upload_id) rescue nil
@ -377,8 +353,8 @@ class ThemeField < ActiveRecord::Base
self.compiler_version = Theme.compiler_version self.compiler_version = Theme.compiler_version
DB.after_commit { CSP::Extension.clear_theme_extensions_cache! } DB.after_commit { CSP::Extension.clear_theme_extensions_cache! }
elsif extra_js_field? || js_tests_field? elsif extra_js_field? || js_tests_field?
self.value_baked, self.error = process_extra_js(self.value) self.error = nil
self.error = nil unless self.error.present? self.value_baked = "baked"
self.compiler_version = Theme.compiler_version self.compiler_version = Theme.compiler_version
elsif basic_scss_field? elsif basic_scss_field?
ensure_scss_compiles! ensure_scss_compiles!

View File

@ -68,8 +68,6 @@ en:
bad_color_scheme: "Can not update theme, invalid color palette" bad_color_scheme: "Can not update theme, invalid color palette"
other_error: "Something went wrong updating theme" other_error: "Something went wrong updating theme"
ember_selector_error: "Sorry using #ember or .ember-view CSS selectors is not permitted, because these names are dynamically generated at runtime and will change over time, eventually resulting in broken CSS. Try a different selector." ember_selector_error: "Sorry using #ember or .ember-view CSS selectors is not permitted, because these names are dynamically generated at runtime and will change over time, eventually resulting in broken CSS. Try a different selector."
compile_error:
unrecognized_extension: "Unrecognized file extension: %{extension}"
import_error: import_error:
generic: An error occurred while importing that theme generic: An error occurred while importing that theme
upload: "Error creating upload asset: %{name}. %{errors}" upload: "Error creating upload asset: %{name}. %{errors}"

View File

@ -25,6 +25,38 @@ class ThemeJavascriptCompiler
JS JS
end end
def append_tree(tree, for_tests: false)
root_name = "discourse"
# Replace legacy extensions
tree.transform_keys! do |filename|
if filename.ends_with? ".js.es6"
filename.sub(/\.js\.es6\z/, ".js")
elsif filename.ends_with? ".raw.hbs"
filename.sub(/\.raw\.hbs\z/, ".hbr")
else
filename
end
end
# Transpile and write to output
tree.each_pair do |filename, content|
module_name, extension = filename.split(".", 2)
module_name = "test/#{module_name}" if for_tests
if extension == "js"
append_module(content, module_name)
elsif extension == "hbs"
append_ember_template(module_name, content)
elsif extension == "hbr"
append_raw_template(module_name.sub("discourse/templates/", ""), content)
else
append_js_error("unknown file extension '#{extension}' (#{filename})")
end
rescue CompileError => e
append_js_error "#{e.message} (#{filename})"
end
end
def append_ember_template(name, hbs_template) def append_ember_template(name, hbs_template)
name = "/#{name}" if !name.start_with?("/") name = "/#{name}" if !name.start_with?("/")
module_name = "discourse/theme-#{@theme_id}#{name}" module_name = "discourse/theme-#{@theme_id}#{name}"
@ -93,7 +125,8 @@ class ThemeJavascriptCompiler
end end
def append_js_error(message) def append_js_error(message)
@content << "console.error('Theme Transpilation Error:', #{message.inspect});" message = "[THEME #{@theme_id} '#{@theme_name}'] Compile error: #{message}"
append_raw_script "console.error(#{message.to_json});"
end end
private private

View File

@ -2,7 +2,6 @@
RSpec.describe ThemeJavascriptCompiler do RSpec.describe ThemeJavascriptCompiler do
let(:compiler) { ThemeJavascriptCompiler.new(1, 'marks') } let(:compiler) { ThemeJavascriptCompiler.new(1, 'marks') }
let(:theme_id) { 22 }
describe "#append_raw_template" do describe "#append_raw_template" do
it 'uses the correct template paths' do it 'uses the correct template paths' do
@ -72,4 +71,20 @@ RSpec.describe ThemeJavascriptCompiler do
end.to raise_error(ThemeJavascriptCompiler::CompileError, /Parse error on line 1/) end.to raise_error(ThemeJavascriptCompiler::CompileError, /Parse error on line 1/)
end end
end end
describe "#append_tree" do
it "can handle multiple modules" do
compiler.append_tree(
{
"discourse/components/mycomponent.js" => <<~JS,
import Component from "@glimmer/component";
export default class MyComponent extends Component {}
JS
"discourse/templates/components/mycomponent.hbs" => "{{my-component-template}}"
}
)
expect(compiler.content).to include('define("discourse/theme-1/components/mycomponent"')
expect(compiler.content).to include('define("discourse/theme-1/discourse/templates/components/mycomponent"')
end
end
end end

View File

@ -111,7 +111,7 @@ HTML
field.ensure_baked! field.ensure_baked!
expect(field.error).not_to eq(nil) expect(field.error).not_to eq(nil)
expect(field.value_baked).to include("<script defer=\"\" src=\"#{field.javascript_cache.url}\" data-theme-id=\"1\"></script>") expect(field.value_baked).to include("<script defer=\"\" src=\"#{field.javascript_cache.url}\" data-theme-id=\"1\"></script>")
expect(field.javascript_cache.content).to include("Theme Transpilation Error:") expect(field.javascript_cache.content).to include("[THEME 1 'Default'] Compile error")
field.update!(value: '') field.update!(value: '')
field.ensure_baked! field.ensure_baked!
@ -183,15 +183,9 @@ HTML
theme.save! theme.save!
js_field.reload js_field.reload
expect(js_field.value_baked).to include("if ('define' in window) {") expect(js_field.value_baked).to eq("baked")
expect(js_field.value_baked).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery\"") expect(js_field.value_baked).to eq("baked")
expect(js_field.value_baked).to include("console.log('hello from .js.es6');") expect(js_field.value_baked).to eq("baked")
expect(hbs_field.reload.value_baked).to include("define(\"discourse/theme-#{theme.id}/discourse/templates/discovery\", [\"exports\", \"@ember/template-factory\"]")
expect(raw_hbs_field.reload.value_baked).to include('addRawTemplate("discovery"')
expect(hbr_field.reload.value_baked).to include('addRawTemplate("other_discovery"')
expect(unknown_field.reload.value_baked).to eq("")
expect(unknown_field.reload.error).to eq(I18n.t("themes.compile_error.unrecognized_extension", extension: "blah"))
# All together # All together
expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/discourse/templates/discovery\", [\"exports\", \"@ember/template-factory\"]") expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/discourse/templates/discovery\", [\"exports\", \"@ember/template-factory\"]")
@ -199,6 +193,7 @@ HTML
expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery\"") expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery\"")
expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery-2\"") expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery-2\"")
expect(theme.javascript_cache.content).to include("const settings =") expect(theme.javascript_cache.content).to include("const settings =")
expect(theme.javascript_cache.content).to include("[THEME #{theme.id} '#{theme.name}'] Compile error: unknown file extension 'blah' (discourse/controllers/discovery.blah)")
end end
def create_upload_theme_field!(name) def create_upload_theme_field!(name)