0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/spec/lib/theme_javascript_compiler_spec.rb
David Taylor d42c648fc7
PERF: Move theme settings from js bundle to preload data (#40137)
This means that we no longer need to recompile theme JS bundles when
theme settings are changed. It has parity with site settings, which are
already included in the preload data. Theme#cached_settings is already
cached in-memory, so performance impact is minimal.

Since theme JS bundles are now only dependent on `extra_js` ThemeFields,
we can make their caching much more stricter, meaning that they don't
need to be recompiled on every single `theme.save!`. Now they will only
be invalidated when extra_js fields actually change, or when the theme
compiler_version changes.
2026-06-08 16:04:56 +01:00

267 lines
11 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe ThemeJavascriptCompiler do
let(:compiler) { ThemeJavascriptCompiler.new(1, "marks", minify: false) }
describe "#append_ember_template" do
it "maintains module names" do
compiler.append_tree({ "connectors/blah-1.hbs" => "{{var}}" })
compiler.append_tree({ "connectors/blah-2.hbs" => "{{var}}" })
compiler.append_tree({ "javascripts/connectors/blah-3.hbs" => "{{var}}" })
expect(compiler.content.to_s).to include("compatModules[\"templates/connectors/blah-1\"]")
expect(compiler.content.to_s).to include("compatModules[\"templates/connectors/blah-2\"]")
expect(compiler.content.to_s).to include(
"compatModules[\"javascripts/templates/connectors/blah-3\"]",
)
end
end
describe "connector module name handling" do
it "separates colocated connectors to avoid module name clash" do
# Colocated under `/connectors`
compiler = ThemeJavascriptCompiler.new(1, "marks", minify: false)
compiler.append_tree(
{
"connectors/outlet/blah-1.hbs" => "{{var}}",
"connectors/outlet/blah-1.js" => "export default class MyComponent {};",
},
)
expect(compiler.content.to_s).to include('compatModules["connectors/outlet/blah-1"]').once
expect(compiler.content.to_s).to include("templates/connectors/outlet/blah-1")
expect(compiler.content.to_s).not_to include("setComponentTemplate")
expect(compiler.content.to_s).to include("createTemplateFactory")
expect(JSON.parse(compiler.source_map)["sources"]).to include(
"theme-1/connectors/outlet/blah-1.js",
"theme-1/connectors/outlet/blah-1.hbs",
)
# Colocated under `/templates/connectors`
compiler = ThemeJavascriptCompiler.new(1, "marks", minify: false)
compiler.append_tree(
{
"templates/connectors/outlet/blah-1.hbs" => "{{var}}",
"templates/connectors/outlet/blah-1.js" => "export default {};",
},
)
expect(compiler.content.to_s).to include('compatModules["connectors/outlet/blah-1"]').once
expect(compiler.content.to_s).to include("templates/connectors/outlet/blah-1")
expect(compiler.content.to_s).not_to include("setComponentTemplate")
expect(compiler.content.to_s).to include("createTemplateFactory")
expect(JSON.parse(compiler.source_map)["sources"]).to include(
"theme-1/templates/connectors/outlet/blah-1.js",
"theme-1/templates/connectors/outlet/blah-1.hbs",
)
# Not colocated
compiler = ThemeJavascriptCompiler.new(1, "marks", minify: false)
compiler.append_tree(
{
"templates/connectors/outlet/blah-1.hbs" => "{{var}}",
"connectors/outlet/blah-1.js" => "export default {};",
},
)
expect(compiler.content.to_s).to include('compatModules["connectors/outlet/blah-1"]').once
expect(compiler.content.to_s).to include("templates/connectors/outlet/blah-1")
expect(compiler.content.to_s).not_to include("setComponentTemplate")
expect(compiler.content.to_s).to include("createTemplateFactory")
expect(JSON.parse(compiler.source_map)["sources"]).to include(
"theme-1/connectors/outlet/blah-1.js",
"theme-1/templates/connectors/outlet/blah-1.hbs",
)
# colocation in discourse directory
compiler = ThemeJavascriptCompiler.new(1, "marks", minify: false)
compiler.append_tree(
{
"discourse/connectors/outlet/blah-1.hbs" => "{{var}}",
"discourse/connectors/outlet/blah-1.js" => "export default {};",
},
)
expect(compiler.content.to_s).to include(
'compatModules["discourse/connectors/outlet/blah-1"]',
).once
expect(compiler.content.to_s).to include("discourse/templates/connectors/outlet/blah-1")
expect(compiler.content.to_s).not_to include("setComponentTemplate")
expect(JSON.parse(compiler.source_map)["sources"]).to include(
"theme-1/discourse/connectors/outlet/blah-1.js",
)
end
end
describe "error handling" do
it "handles syntax errors in ember templates" do
compiler.append_tree({ "sometemplate.hbs" => "{{invalidtemplate" })
expect(compiler.content).to include("Parse error on line 1")
end
end
describe "#append_tree" do
it "can handle multiple modules" do
compiler.append_tree(
{
"discourse/initializers/my-initializer.js" => <<~JS,
import MyComponent from "../components/mycomponent";
export default {
name: "my-initializer",
initialize() {
console.log("my-initializer", MyComponent);
},
};
JS
"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('compatModules["discourse/components/mycomponent"]')
expect(compiler.content).to include(
'compatModules["discourse/templates/components/mycomponent"]',
)
end
it "handles colocated components" do
compiler.append_tree(
{
"discourse/components/mycomponent.js" => <<~JS,
import Component from "@glimmer/component";
export default class MyComponent extends Component {}
JS
"discourse/components/mycomponent.hbs" => "{{my-component-template}}",
},
)
expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.content).to include("setComponentTemplate")
expect(compiler.content).to include("createTemplateFactory")
end
it "handles colocated admin components" do
compiler.append_tree(
{
"admin/components/mycomponent.js" => <<~JS,
import Component from "@glimmer/component";
export default class MyComponent extends Component {}
JS
"admin/components/mycomponent.hbs" => "{{my-component-template}}",
},
)
expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.content).to include("setComponentTemplate")
end
it "applies theme AST transforms to colocated components" do
compiler = ThemeJavascriptCompiler.new(12_345_678_910, "my theme name", minify: false)
compiler.append_tree(
{ "discourse/components/mycomponent.hbs" => '{{theme-i18n "my_translation_key"}}' },
)
template_compiled_line = compiler.content.lines.find { |l| l.include?('"block":') }
expect(template_compiled_line).to include("12345678910")
end
it "handles template-only components" do
compiler.append_tree(
{ "discourse/components/mycomponent.hbs" => "{{my-component-template}}" },
)
expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.content).to include("setComponentTemplate")
expect(compiler.content).to include("@ember/component/template-only")
end
end
describe "terser compilation" do
let(:compiler) { ThemeJavascriptCompiler.new(1, "marks", minify: true) }
it "applies terser and provides sourcemaps" do
sources = {
"multiply.js" =>
"export const multiply = (firstValue, secondValue) => firstValue * secondValue;",
"add.js" => "export const add = (firstValue, secondValue) => firstValue + secondValue;",
}
compiler.append_tree(sources)
expect(compiler.content).to include("multiply")
expect(compiler.content).to include("add")
expect(compiler.content).not_to include("firstValue")
expect(compiler.content).not_to include("secondValue")
map = JSON.parse(compiler.source_map)
expect(map["sources"]).to include("theme-1/multiply.js", "theme-1/add.js")
expect(map["sourcesContent"].to_s).to include("const multiply")
expect(map["sourcesContent"].to_s).to include("const add")
expect(map["sourcesContent"].to_s).to include("firstValue")
expect(map["sourcesContent"].to_s).to include("secondValue")
end
it "handles invalid JS" do
compiler.append_tree({ "filename.js" => "if(someCondition" })
expect(compiler.content).to include('throw new Error("[THEME 1')
expect(compiler.content).to include("Unexpected token")
end
end
describe "ember-this-fallback" do
it "applies its transforms" do
compiler.append_tree(
{
"discourse/components/my-component.js" => <<~JS,
import Component from "@glimmer/component";
export default class MyComponent extends Component {
value = "foo";
}
JS
"discourse/components/my-component.hbs" => "{{value}}",
},
)
expect(compiler.content).to include("ember-this-fallback")
expect(compiler.content).to include(
"The `value` property path was used in the `theme-1/discourse/components/my-component.hbs` template without using `this`. This fallback behavior has been deprecated, all properties must be looked up on `this` when used in the template: {{this.value}}",
)
end
end
describe "ember-template-imports" do
it "applies its transforms" do
compiler.append_tree({ "discourse/components/my-component.gjs" => <<~JS })
import Component from "@glimmer/component";
export default class MyComponent extends Component {
<template>
{{this.value}}
</template>
value = "foo";
}
JS
expect(compiler.content).to include("compatModules[\"discourse/components/my-component\"]")
expect(compiler.content).to include('value = "foo";')
expect(compiler.content).to include("setComponentTemplate")
expect(compiler.content).to include("createTemplateFactory")
end
end
describe ".js.es6 extension deprecation" do
it "appends a deprecation warning for .js.es6 files" do
compiler.append_tree(
{ "discourse/components/my-component.js.es6" => "export default class MyComponent {}" },
)
expect(compiler.content).to include("discourse.es6-extension")
expect(compiler.content).to include(
"The file 'discourse/components/my-component.js.es6' uses the deprecated `.js.es6` extension. Use `.js` instead.",
)
end
it "does not add deprecation for regular .js files" do
compiler.append_tree(
{ "discourse/components/my-component.js" => "export default class MyComponent {}" },
)
expect(compiler.content).not_to include("discourse.es6-extension")
end
end
end