mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 13:06:56 +08:00

We are no longer using any of the transpilation/bundling features of Sprockets. We only use it to serve assets in development, and then collect & fingerprint them in production. This commit switches us to use the more modern "Propshaft" gem for that functionality. Propshaft is much simpler than Sprockets. Instead of taking a combination of paths + "precompile" list, Propshaft simply assumes all files in the configured directory are required in production. Previously we had some base paths configured quite high in the directory structure, and then only precompiled selected assets within the directory. That's no longer possible, so this commit refactors those places (mostly plugin-related) to use dedicated directories under `app/assets/generated/`. Another difference is that Propshaft applies asset digests in development as well as production. This is great for caching & dev/prod consistency, but does mean some small changes were required in tests. We previously had some freedom-patches applied to Sprockets. Some of those had to be ported across to Propshaft. We now have three patches: 1. Skip adding digest hashes to webpack-generated chunks (which are already digested, and referred to from other js files) 2. Avoid raising errors for missing assets in test mode. We don't always compile assets before running basic RSpec tests. 3. Maintain relative paths for sourcemap URLs, so that files don't need to be recompiled depending on their CDN path Significant refactors are made to the `assets.rake` and `s3.rake` tasks, which rely on implementation details of Sprockets/Propshaft.
195 lines
5.8 KiB
Ruby
195 lines
5.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "discourse_js_processor"
|
|
|
|
RSpec.describe DiscourseJsProcessor do
|
|
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,[35,0]]],[],false,[\\"somevalue\\"]]",
|
|
"moduleName": "/blah/mymodule",
|
|
"isStrictMode": false
|
|
});
|
|
});
|
|
JS
|
|
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
|