diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 6f76f84e3ea..24dfadd41b1 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -16,6 +16,9 @@ const withSideWatch = require("./lib/with-side-watch"); const crypto = require("crypto"); const commonBabelConfig = require("./lib/common-babel-config"); const TerserPlugin = require("terser-webpack-plugin"); +const { + CustomizeChunkUrlPlugin, +} = require("./lib/webpack-customize-chunk-url-plugin"); process.env.BROCCOLI_ENABLED_MEMOIZE = true; @@ -217,6 +220,7 @@ module.exports = function (defaults) { return JSON.stringify(output, null, 2); }, }), + new CustomizeChunkUrlPlugin(), new RetryChunkLoadPlugin({ retryDelay: 200, maxRetries: 2, diff --git a/app/assets/javascripts/discourse/lib/webpack-customize-chunk-url-plugin.js b/app/assets/javascripts/discourse/lib/webpack-customize-chunk-url-plugin.js new file mode 100644 index 00000000000..e1d505597c7 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/webpack-customize-chunk-url-plugin.js @@ -0,0 +1,40 @@ +const pluginName = "CustomizeChunkUrlPlugin"; + +export class CustomizeChunkUrlPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + const { mainTemplate } = compilation; + mainTemplate.hooks.localVars.tap( + { name: pluginName, stage: 1 }, + (source) => { + return ` + ${source} + (function () { + // Rewrite chunk URLs to match the encoding of the current script + if ( + typeof __webpack_require__ !== "undefined" && + typeof document !== "undefined" && + document.currentScript + ) { + const currentScriptUrl = document.currentScript.src; + + let targetExt = ".js"; + if (currentScriptUrl.endsWith(".br.js")) { + targetExt = ".br.js"; + } else if (currentScriptUrl.endsWith(".gz.js")) { + targetExt = ".gz.js"; + } + + let oldGetScript = __webpack_require__.u; + __webpack_require__.u = function (chunkId) { + let result = oldGetScript(chunkId); + return result.replace(/\.js$/, targetExt); + }; + } + })(); + `; + } + ); + }); + } +} diff --git a/spec/system/script_encoding_spec.rb b/spec/system/script_encoding_spec.rb new file mode 100644 index 00000000000..b675e74da50 --- /dev/null +++ b/spec/system/script_encoding_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +describe "script encoding" do + let(:js_cdn_requests) { [] } + + before { stub_and_log_cdn_requests } + + def stub_and_log_cdn_requests + page.driver.with_playwright_page do |page| + page.route( + "http://cdn.example.com/**/*", + ->(route, request) do + js_cdn_requests << request.url if request.url.end_with?(".js") + origin_uri = URI(request.frame.url) + request_uri = URI(request.url) + + # We don't actually have .br.js files or a CDN, so invisibly + # rewrite the request to the regular assets + mocked_result = + URI::HTTP.build( + scheme: origin_uri.scheme, + host: origin_uri.host, + port: origin_uri.port, + path: request_uri.path.sub(".br.js", ".js"), + ) + route.continue(url: mocked_result.to_s) + end, + ) + end + end + + context "without s3 assets" do + before { set_cdn_url "http://cdn.example.com" } + + it "loads JS chunks with the .js extension" do + user = Fabricate(:admin) + sign_in user + + visit "/latest" + + expect(page).to have_css("#site-logo") + + expect(js_cdn_requests.length).to be > 1 + expect(js_cdn_requests.any? { |r| r.end_with?(".br.js") }).to eq(false) + expect(js_cdn_requests.all? { |r| r.end_with?(".js") }).to eq(true) + + js_cdn_requests.clear + + # Use the composer to trigger an async chunk load + find("#create-topic").click + find(".d-editor-input").fill_in(with: "This is a test") + expect(page).to have_css(".d-editor-preview", text: "This is a test") + + expect(js_cdn_requests.length).to be > 1 + expect(js_cdn_requests.any? { |r| r.end_with?(".br.js") }).to eq(false) + expect(js_cdn_requests.all? { |r| r.end_with?(".js") }).to eq(true) + end + end + + context "with s3 assets" do + before do + global_setting :s3_bucket, "test_bucket" + global_setting :s3_region, "ap-australia" + global_setting :s3_access_key_id, "123" + global_setting :s3_secret_access_key, "123" + global_setting :s3_cdn_url, "http://cdn.example.com" + end + + it "loads JS chunks with the .br.js extension" do + user = Fabricate(:admin) + sign_in user + + visit "/latest" + + expect(page).to have_css("#site-logo") + + expect(js_cdn_requests.length).to be > 1 + expect(js_cdn_requests.all? { |r| r.end_with?(".br.js") }).to eq(true) + + js_cdn_requests.clear + + # Use the composer to trigger an async chunk load + find("#create-topic").click + find(".d-editor-input").fill_in(with: "This is a test") + expect(page).to have_css(".d-editor-preview", text: "This is a test") + + expect(js_cdn_requests.length).to be > 1 + expect(js_cdn_requests.all? { |r| r.end_with?(".br.js") }).to eq(true) + end + end +end