mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 10:20:58 -06:00
97847f6cd8
This reverts commit 0f4520867b
.
This has led to two problems:
1. An incompatibility with Cloudflare's "auto minify" feature. They've deprecated this feature because of incompatibility with modern JS syntax. But unfortunately it will remain enabled on existing properties until 2024-08-05.
2. Discourse fails to boot in Safari 15. This is strange, because Safari does support all the required features in our production JS bundles. Even more strangely, things start working as soon as you open the developer tools. That suggests the cause could be a Safari bug rather than a simple incompatibility.
Reverting while we work out a path forward on both those issues.
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("_applyDecoratedDescriptor")
|
|
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
|