mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 10:20:58 -06:00
565c753dd2
decorator-transforms (https://github.com/ef4/decorator-transforms) is a modern replacement for babel's plugin-proposal-decorators. It provides a decorator implementation using modern browser features, without needing to enable babel's full suite of class feature transformations. This improves the developer experience and performance.
In local testing with Google's 'tachometer' tool, this reduces Discourse's 'init-to-render' time by around 3-4% (230ms -> 222ms).
It reduces our initial gzip'd JS payloads by 3.2% (2.43MB -> 2.35MB), or 7.5% (14.5MB -> 13.4MB) uncompressed.
This was previously reverted in 97847f6
. This version includes a babel transformation which works around the bug in Safari <= 15.
For Cloudflare compatibility issues, check https://meta.discourse.org/t/311390
274 lines
8.7 KiB
Ruby
274 lines
8.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "discourse_js_processor"
|
|
|
|
RSpec.describe DiscourseJsProcessor do
|
|
describe "should_transpile?" do
|
|
it "returns false for empty strings" do
|
|
expect(DiscourseJsProcessor.should_transpile?(nil)).to eq(false)
|
|
expect(DiscourseJsProcessor.should_transpile?("")).to eq(false)
|
|
end
|
|
|
|
it "returns false for a regular js file" do
|
|
expect(DiscourseJsProcessor.should_transpile?("file.js")).to eq(false)
|
|
end
|
|
|
|
it "returns true for deprecated .es6 files" do
|
|
expect(DiscourseJsProcessor.should_transpile?("file.es6")).to eq(true)
|
|
expect(DiscourseJsProcessor.should_transpile?("file.js.es6")).to eq(true)
|
|
expect(DiscourseJsProcessor.should_transpile?("file.js.es6.erb")).to eq(true)
|
|
end
|
|
end
|
|
|
|
describe "skip_module?" do
|
|
it "returns false for empty strings" do
|
|
expect(DiscourseJsProcessor.skip_module?(nil)).to eq(false)
|
|
expect(DiscourseJsProcessor.skip_module?("")).to eq(false)
|
|
end
|
|
|
|
it "returns true if the header is present" do
|
|
expect(DiscourseJsProcessor.skip_module?("// cool comment\n// discourse-skip-module")).to eq(
|
|
true,
|
|
)
|
|
end
|
|
|
|
it "returns false if the header is not present" do
|
|
expect(DiscourseJsProcessor.skip_module?("// just some JS\nconsole.log()")).to eq(false)
|
|
end
|
|
|
|
it "works end-to-end" do
|
|
source = <<~JS.chomp
|
|
// discourse-skip-module
|
|
console.log("hello world");
|
|
JS
|
|
expect(DiscourseJsProcessor.transpile(source, "test", "test")).to eq(source)
|
|
end
|
|
end
|
|
|
|
it "passes through modern JS syntaxes which are supported in our target browsers" do
|
|
script = <<~JS.chomp
|
|
optional?.chaining;
|
|
const template = func`test`;
|
|
let numericSeparator = 100_000_000;
|
|
logicalAssignment ||= 2;
|
|
nullishCoalescing ?? 'works';
|
|
try {
|
|
"optional catch binding";
|
|
} catch {
|
|
"works";
|
|
}
|
|
async function* asyncGeneratorFunction() {
|
|
yield await Promise.resolve('a');
|
|
}
|
|
let a = {
|
|
x,
|
|
y,
|
|
...spreadRest
|
|
};
|
|
JS
|
|
|
|
result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule")
|
|
expect(result).to eq <<~JS.strip
|
|
define("blah/mymodule", [], function () {
|
|
"use strict";
|
|
|
|
#{script.indent(2)}
|
|
});
|
|
JS
|
|
end
|
|
|
|
it "supports decorators and class properties without error" do
|
|
script = <<~JS.chomp
|
|
class MyClass {
|
|
classProperty = 1;
|
|
#privateProperty = 1;
|
|
#privateMethod() {
|
|
console.log("hello world");
|
|
}
|
|
@decorated
|
|
myMethod(){
|
|
}
|
|
}
|
|
JS
|
|
|
|
result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule")
|
|
expect(result).to include("static #_ = (() => dt7948.n")
|
|
end
|
|
|
|
it "correctly transpiles widget hbs" do
|
|
result = DiscourseJsProcessor.transpile(<<~JS, "blah", "blah/mymodule")
|
|
import hbs from "discourse/widgets/hbs-compiler";
|
|
const template = hbs`{{somevalue}}`;
|
|
JS
|
|
expect(result).to eq <<~JS.strip
|
|
define("blah/mymodule", [], function () {
|
|
"use strict";
|
|
|
|
const template = function (attrs, state) {
|
|
var _r = [];
|
|
_r.push(somevalue);
|
|
return _r;
|
|
};
|
|
});
|
|
JS
|
|
end
|
|
|
|
it "correctly transpiles ember hbs" do
|
|
result = DiscourseJsProcessor.transpile(<<~JS, "blah", "blah/mymodule")
|
|
import { hbs } from 'ember-cli-htmlbars';
|
|
const template = hbs`{{somevalue}}`;
|
|
JS
|
|
expect(result).to eq <<~JS.strip
|
|
define("blah/mymodule", ["@ember/template-factory"], function (_templateFactory) {
|
|
"use strict";
|
|
|
|
const template = (0, _templateFactory.createTemplateFactory)(
|
|
/*
|
|
{{somevalue}}
|
|
*/
|
|
{
|
|
"id": null,
|
|
"block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
|
|
"moduleName": "/blah/mymodule",
|
|
"isStrictMode": false
|
|
});
|
|
});
|
|
JS
|
|
end
|
|
|
|
describe "Raw template theme transformations" do
|
|
# For the raw templates, we can easily render them serverside, so let's do that
|
|
|
|
let(:compiler) { DiscourseJsProcessor::Transpiler.new }
|
|
let(:theme_id) { 22 }
|
|
|
|
let(:helpers) { <<~JS }
|
|
Handlebars.registerHelper('theme-prefix', function(themeId, string) {
|
|
return `theme_translations.${themeId}.${string}`
|
|
})
|
|
Handlebars.registerHelper('theme-i18n', function(themeId, string) {
|
|
return `translated(theme_translations.${themeId}.${string})`
|
|
})
|
|
Handlebars.registerHelper('theme-setting', function(themeId, string) {
|
|
return `setting(${themeId}:${string})`
|
|
})
|
|
Handlebars.registerHelper('dummy-helper', function(string) {
|
|
return `dummy(${string})`
|
|
})
|
|
JS
|
|
|
|
let(:mini_racer) do
|
|
ctx = MiniRacer::Context.new
|
|
ctx.eval(File.open("#{Rails.root}/node_modules/handlebars/dist/handlebars.js").read)
|
|
ctx.eval(helpers)
|
|
ctx
|
|
end
|
|
|
|
def render(template)
|
|
compiled = compiler.compile_raw_template(template, theme_id: theme_id)
|
|
mini_racer.eval "Handlebars.template(#{compiled.squish})({})"
|
|
end
|
|
|
|
it "adds the theme id to the helpers" do
|
|
# Works normally
|
|
expect(render("{{theme-prefix 'translation_key'}}")).to eq(
|
|
"theme_translations.22.translation_key",
|
|
)
|
|
expect(render("{{theme-i18n 'translation_key'}}")).to eq(
|
|
"translated(theme_translations.22.translation_key)",
|
|
)
|
|
expect(render("{{theme-setting 'setting_key'}}")).to eq("setting(22:setting_key)")
|
|
|
|
# Works when used inside other statements
|
|
expect(render("{{dummy-helper (theme-prefix 'translation_key')}}")).to eq(
|
|
"dummy(theme_translations.22.translation_key)",
|
|
)
|
|
end
|
|
|
|
it "doesn't duplicate number parameter inside {{each}}" do
|
|
expect(
|
|
compiler.compile_raw_template(
|
|
"{{#each item as |test test2|}}{{theme-setting 'setting_key'}}{{/each}}",
|
|
theme_id: theme_id,
|
|
),
|
|
).to include(
|
|
'{"name":"theme-setting","hash":{},"hashTypes":{},"hashContexts":{},"types":["NumberLiteral","StringLiteral"]',
|
|
)
|
|
# Fail would be if theme-setting is defined with types:["NumberLiteral","NumberLiteral","StringLiteral"]
|
|
end
|
|
end
|
|
|
|
describe "Ember template transformations" do
|
|
# For the Ember (Glimmer) templates, serverside rendering is not trivial,
|
|
# so we compile the expected result with the standard compiler and compare to the theme compiler
|
|
let(:theme_id) { 22 }
|
|
|
|
def theme_compile(template)
|
|
script = <<~JS
|
|
import { hbs } from 'ember-cli-htmlbars';
|
|
export default hbs(#{template.to_json});
|
|
JS
|
|
result = DiscourseJsProcessor.transpile(script, "", "theme/blah", theme_id: theme_id)
|
|
result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */")
|
|
end
|
|
|
|
def standard_compile(template)
|
|
script = <<~JS
|
|
import { hbs } from 'ember-cli-htmlbars';
|
|
export default hbs(#{template.to_json});
|
|
JS
|
|
result = DiscourseJsProcessor.transpile(script, "", "theme/blah")
|
|
result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */")
|
|
end
|
|
|
|
it "adds the theme id to the helpers" do
|
|
expect(theme_compile "{{theme-prefix 'translation_key'}}").to eq(
|
|
standard_compile "{{theme-prefix #{theme_id} 'translation_key'}}"
|
|
)
|
|
|
|
expect(theme_compile "{{theme-i18n 'translation_key'}}").to eq(
|
|
standard_compile "{{theme-i18n #{theme_id} 'translation_key'}}"
|
|
)
|
|
|
|
expect(theme_compile "{{theme-setting 'setting_key'}}").to eq(
|
|
standard_compile "{{theme-setting #{theme_id} 'setting_key'}}"
|
|
)
|
|
|
|
# Works when used inside other statements
|
|
expect(theme_compile "{{dummy-helper (theme-prefix 'translation_key')}}").to eq(
|
|
standard_compile "{{dummy-helper (theme-prefix #{theme_id} 'translation_key')}}"
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "Transpiler#terser" do
|
|
it "can minify code and provide sourcemaps" do
|
|
sources = {
|
|
"multiply.js" => "let multiply = (firstValue, secondValue) => firstValue * secondValue;",
|
|
"add.js" => "let add = (firstValue, secondValue) => firstValue + secondValue;",
|
|
}
|
|
|
|
result =
|
|
DiscourseJsProcessor::Transpiler.new.terser(
|
|
sources,
|
|
{ sourceMap: { includeSources: true } },
|
|
)
|
|
expect(result.keys).to contain_exactly("code", "decoded_map", "map")
|
|
|
|
begin
|
|
# Check the code still works
|
|
ctx = MiniRacer::Context.new
|
|
ctx.eval(result["code"])
|
|
expect(ctx.eval("multiply(2, 3)")).to eq(6)
|
|
expect(ctx.eval("add(2, 3)")).to eq(5)
|
|
ensure
|
|
ctx.dispose
|
|
end
|
|
|
|
map = JSON.parse(result["map"])
|
|
expect(map["sources"]).to contain_exactly(*sources.keys)
|
|
expect(map["sourcesContent"]).to contain_exactly(*sources.values)
|
|
end
|
|
end
|
|
end
|