discourse/spec/lib/theme_javascript_compiler_spec.rb
David Taylor 82b16f4f47
DEV: Do not manipulate theme module paths at build-time (#23148)
Manipulating theme module paths means that the paths you author are not the ones used at runtime. This can lead to some very unexpected behavior and potential module name clashes. It also meant that the refactor in 16c6ab8661 was unable to correctly match up theme connector js/templates.

While this could technically be a breaking change, I think it is reasonably safe because:

1. Themes are already forced to use relative paths when referencing their own modules (since they're namespaced based on the site-specific id). The only time this might be problematic is when theme tests reference modules in the theme's main `javascripts` directory

2. For things like components/services/controllers/etc. our custom Ember resolver works backwards from the end of the path, so adding `discourse/` in the middle will not affect resolution.
2023-08-18 18:15:23 +01:00

217 lines
8.2 KiB
Ruby

# frozen_string_literal: true
RSpec.describe ThemeJavascriptCompiler do
let(:compiler) { ThemeJavascriptCompiler.new(1, "marks") }
describe "#append_raw_template" do
it "uses the correct template paths" do
template = "<h1>hello</h1>"
name = "/path/to/templates1"
compiler.append_raw_template("#{name}.raw", template)
expect(compiler.raw_content.to_s).to include("addRawTemplate(\"#{name}\"")
name = "/path/to/templates2"
compiler.append_raw_template("#{name}.hbr", template)
expect(compiler.raw_content.to_s).to include("addRawTemplate(\"#{name}\"")
name = "/path/to/templates3"
compiler.append_raw_template("#{name}.hbs", template)
expect(compiler.raw_content.to_s).to include("addRawTemplate(\"#{name}.hbs\"")
end
end
describe "#append_ember_template" do
it "maintains module names so that discourse-boot.js can correct them" do
compiler.append_ember_template("/connectors/blah-1", "{{var}}")
expect(compiler.raw_content.to_s).to include(
"define(\"discourse/theme-1/connectors/blah-1\", [\"exports\", \"@ember/template-factory\"]",
)
compiler.append_ember_template("connectors/blah-2", "{{var}}")
expect(compiler.raw_content.to_s).to include(
"define(\"discourse/theme-1/connectors/blah-2\", [\"exports\", \"@ember/template-factory\"]",
)
compiler.append_ember_template("javascripts/connectors/blah-3", "{{var}}")
expect(compiler.raw_content.to_s).to include(
"define(\"discourse/theme-1/javascripts/connectors/blah-3\", [\"exports\", \"@ember/template-factory\"]",
)
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")
compiler.append_tree(
{
"connectors/outlet/blah-1.hbs" => "{{var}}",
"connectors/outlet/blah-1.js" => "console.log('test')",
},
)
expect(compiler.raw_content.to_s).to include("discourse/theme-1/connectors/outlet/blah-1")
expect(compiler.raw_content.to_s).to include(
"discourse/theme-1/templates/connectors/outlet/blah-1",
)
expect(JSON.parse(compiler.source_map)["sources"]).to contain_exactly(
"connectors/outlet/blah-1.js",
"templates/connectors/outlet/blah-1.js",
)
# Colocated under `/templates/connectors`
compiler = ThemeJavascriptCompiler.new(1, "marks")
compiler.append_tree(
{
"templates/connectors/outlet/blah-1.hbs" => "{{var}}",
"templates/connectors/outlet/blah-1.js" => "console.log('test')",
},
)
expect(compiler.raw_content.to_s).to include("discourse/theme-1/connectors/outlet/blah-1")
expect(compiler.raw_content.to_s).to include(
"discourse/theme-1/templates/connectors/outlet/blah-1",
)
expect(JSON.parse(compiler.source_map)["sources"]).to contain_exactly(
"connectors/outlet/blah-1.js",
"templates/connectors/outlet/blah-1.js",
)
# Not colocated
compiler = ThemeJavascriptCompiler.new(1, "marks")
compiler.append_tree(
{
"templates/connectors/outlet/blah-1.hbs" => "{{var}}",
"connectors/outlet/blah-1.js" => "console.log('test')",
},
)
expect(compiler.raw_content.to_s).to include("discourse/theme-1/connectors/outlet/blah-1")
expect(compiler.raw_content.to_s).to include(
"discourse/theme-1/templates/connectors/outlet/blah-1",
)
expect(JSON.parse(compiler.source_map)["sources"]).to contain_exactly(
"connectors/outlet/blah-1.js",
"templates/connectors/outlet/blah-1.js",
)
end
end
describe "error handling" do
it "handles syntax errors in raw templates" do
expect do
compiler.append_raw_template("sometemplate.hbr", "{{invalidtemplate")
end.to raise_error(ThemeJavascriptCompiler::CompileError, /Parse error on line 1/)
end
it "handles syntax errors in ember templates" do
expect do
compiler.append_ember_template("sometemplate", "{{invalidtemplate")
end.to raise_error(ThemeJavascriptCompiler::CompileError, /Parse error on line 1/)
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.raw_content).to include(
'define("discourse/theme-1/discourse/components/mycomponent"',
)
expect(compiler.raw_content).to include(
'define("discourse/theme-1/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.raw_content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.raw_content).to include("setComponentTemplate")
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.raw_content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.raw_content).to include("setComponentTemplate")
end
it "applies theme AST transforms to colocated components" do
compiler = ThemeJavascriptCompiler.new(12_345_678_910, "my theme name")
compiler.append_tree(
{ "discourse/components/mycomponent.hbs" => '{{theme-i18n "my_translation_key"}}' },
)
template_compiled_line = compiler.raw_content.lines.find { |l| l.include?('"block":') }
expect(template_compiled_line).to include("12345678910")
end
it "prints error when default export missing" do
compiler.append_tree(
{
"discourse/components/mycomponent.js" => <<~JS,
import Component from "@glimmer/component";
class MyComponent extends Component {}
JS
"discourse/components/mycomponent.hbs" => "{{my-component-template}}",
},
)
expect(compiler.raw_content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.raw_content).to include("throw new Error")
end
it "handles template-only components" do
compiler.append_tree(
{ "discourse/components/mycomponent.hbs" => "{{my-component-template}}" },
)
expect(compiler.raw_content).to include("__COLOCATED_TEMPLATE__ =")
expect(compiler.raw_content).to include("setComponentTemplate")
expect(compiler.raw_content).to include("@ember/component/template-only")
end
end
describe "terser compilation" do
it "applies terser and provides sourcemaps" do
sources = {
"multiply.js" => "let multiply = (firstValue, secondValue) => firstValue * secondValue;",
"add.js" => "let add = (firstValue, secondValue) => firstValue + secondValue;",
}
compiler.append_tree(sources)
expect(compiler.content).to include("multiply")
expect(compiler.content).to include("add")
map = JSON.parse(compiler.source_map)
expect(map["sources"]).to contain_exactly(*sources.keys)
expect(map["sourcesContent"].to_s).to include("let multiply")
expect(map["sourcesContent"].to_s).to include("let add")
expect(map["sourceRoot"]).to eq("theme-1/")
end
it "handles invalid JS" do
compiler.append_raw_script("filename.js", "if(someCondition")
expect(compiler.content).to include('console.error("[THEME 1')
expect(compiler.content).to include("Unexpected token")
end
end
end