diff --git a/app/assets/javascripts/bootstrap-json/index.js b/app/assets/javascripts/bootstrap-json/index.js
index a069821aee7..1ea7bca783f 100644
--- a/app/assets/javascripts/bootstrap-json/index.js
+++ b/app/assets/javascripts/bootstrap-json/index.js
@@ -1,279 +1,71 @@
"use strict";
const express = require("express");
-const { encode } = require("html-entities");
const cleanBaseURL = require("clean-base-url");
const path = require("path");
const fs = require("fs");
const fsPromises = fs.promises;
const { JSDOM } = require("jsdom");
-const { shouldLoadPlugins } = require("discourse-plugins");
const { Buffer } = require("node:buffer");
-const { cwd, env } = require("node:process");
+const { env } = require("node:process");
+const { glob } = require("glob");
-// via https://stackoverflow.com/a/6248722/165668
-function generateUID() {
- let firstPart = (Math.random() * 46656) | 0; // eslint-disable-line no-bitwise
- let secondPart = (Math.random() * 46656) | 0; // eslint-disable-line no-bitwise
- firstPart = ("000" + firstPart.toString(36)).slice(-3);
- secondPart = ("000" + secondPart.toString(36)).slice(-3);
- return firstPart + secondPart;
+async function listDistAssets() {
+ const files = await glob("**/*.js", { nodir: true, cwd: "dist/assets" });
+ return new Set(files);
}
-function htmlTag(buffer, bootstrap) {
- let classList = "";
- if (bootstrap.html_classes) {
- classList = ` class="${bootstrap.html_classes}"`;
- }
- buffer.push(``);
-}
+function updateScriptReferences({
+ chunkInfos,
+ dom,
+ selector,
+ attribute,
+ baseURL,
+ distAssets,
+}) {
+ const elements = dom.window.document.querySelectorAll(selector);
+ const handledEntrypoints = new Set();
-function head(buffer, bootstrap, headers, baseURL) {
- if (bootstrap.csrf_token) {
- buffer.push(``);
- buffer.push(``);
- }
+ for (const el of elements) {
+ const entrypointName = el.dataset.discourseEntrypoint;
- if (bootstrap.theme_id) {
- buffer.push(
- ``
- );
- }
+ if (handledEntrypoints.has(entrypointName)) {
+ el.remove();
+ continue;
+ }
- if (bootstrap.theme_color) {
- buffer.push(``);
- }
+ let chunks = chunkInfos[`assets/${entrypointName}.js`]?.assets;
- if (bootstrap.authentication_data) {
- buffer.push(
- ``
- );
- }
-
- let setupData = "";
- Object.keys(bootstrap.setup_data).forEach((sd) => {
- let val = bootstrap.setup_data[sd];
- if (val) {
- if (Array.isArray(val)) {
- val = JSON.stringify(val);
+ if (!chunks) {
+ if (distAssets.has(`${entrypointName}.js`)) {
+ chunks = [`assets/${entrypointName}.js`];
} else {
- val = val.toString();
+ // Not an ember-cli asset, do not rewrite
+ continue;
}
- setupData += ` data-${sd.replace(/\_/g, "-")}="${encode(val)}"`;
- }
- });
- buffer.push(``);
-
- if (bootstrap.preloaded.currentUser) {
- const user = JSON.parse(bootstrap.preloaded.currentUser);
- let { admin, staff } = user;
-
- if (staff) {
- buffer.push(``);
}
- if (admin) {
- buffer.push(``);
- }
- }
+ const newElements = chunks.map((chunk) => {
+ const newElement = el.cloneNode(true);
+ newElement[attribute] = `${baseURL}${chunk}`;
+ newElement.dataset.emberCliRewritten = "true";
- bootstrap.plugin_js.forEach((src) =>
- buffer.push(``)
- );
+ return newElement;
+ });
- buffer.push(bootstrap.theme_html.translations);
- buffer.push(bootstrap.theme_html.js);
- buffer.push(bootstrap.theme_html.head_tag);
- buffer.push(bootstrap.html.before_head_close);
-}
-
-function localeScript(buffer, bootstrap) {
- buffer.push(``);
- (bootstrap.extra_locales || []).forEach((l) =>
- buffer.push(``)
- );
-}
-
-function beforeScriptLoad(buffer, bootstrap) {
- buffer.push(bootstrap.html.before_script_load);
-}
-
-function discoursePreloadStylesheets(buffer, bootstrap) {
- (bootstrap.stylesheets || []).forEach((s) => {
- let link = ``;
- buffer.push(link);
- });
-}
-
-function discourseStylesheets(buffer, bootstrap) {
- (bootstrap.stylesheets || []).forEach((s) => {
- let attrs = [];
- if (s.media) {
- attrs.push(`media="${s.media}"`);
- }
- if (s.target) {
- attrs.push(`data-target="${s.target}"`);
- }
- if (s.theme_id) {
- attrs.push(`data-theme-id="${s.theme_id}"`);
- }
- if (s.class) {
- attrs.push(`class="${s.class}"`);
- }
- let link = ``;
- buffer.push(link);
- });
-}
-
-function body(buffer, bootstrap) {
- buffer.push(bootstrap.theme_html.header);
- buffer.push(bootstrap.html.header);
-}
-
-function bodyFooter(buffer, bootstrap, headers) {
- buffer.push(bootstrap.theme_html.body_tag);
- buffer.push(bootstrap.html.before_body_close);
-
- let v = generateUID();
- buffer.push(`
-
- `);
-}
-
-function hiddenLoginForm(buffer, bootstrap) {
- if (!bootstrap.preloaded.currentUser) {
- buffer.push(`
-
- `);
- }
-}
-
-function preloaded(buffer, bootstrap) {
- buffer.push(
- ``
- );
-}
-
-const BUILDERS = {
- "html-tag": htmlTag,
- "before-script-load": beforeScriptLoad,
- "discourse-preload-stylesheets": discoursePreloadStylesheets,
- head,
- body,
- "discourse-stylesheets": discourseStylesheets,
- "hidden-login-form": hiddenLoginForm,
- preloaded,
- "body-footer": bodyFooter,
- "locale-script": localeScript,
-};
-
-function replaceIn(bootstrap, template, id, headers, baseURL) {
- let buffer = [];
- BUILDERS[id](buffer, bootstrap, headers, baseURL);
- let contents = buffer.filter((b) => b && b.length > 0).join("\n");
-
- if (id === "html-tag") {
- return template.replace(``, contents);
- } else {
- return template.replace(``, contents);
- }
-}
-
-function extractPreloadJson(html) {
- const dom = new JSDOM(html);
- const dataElement = dom.window.document.querySelector("#data-preloaded");
-
- if (!dataElement || !dataElement.dataset) {
- return;
- }
-
- return dataElement.dataset.preloaded;
-}
-
-async function applyBootstrap(bootstrap, template, response, baseURL, preload) {
- bootstrap.preloaded = Object.assign(JSON.parse(preload), bootstrap.preloaded);
-
- Object.keys(BUILDERS).forEach((id) => {
- template = replaceIn(bootstrap, template, id, response.headers, baseURL);
- });
- return template;
-}
-
-async function buildFromBootstrap(proxy, baseURL, req, response, preload) {
- try {
- const template = await fsPromises.readFile(
- path.join(cwd(), "dist", "index.html"),
- "utf8"
- );
-
- let url = new URL(`${proxy}${baseURL}bootstrap.json`);
- url.searchParams.append("for_url", req.url);
-
- const forUrlSearchParams = new URL(req.url, "https://dummy-origin.invalid")
- .searchParams;
-
- const mobileView = forUrlSearchParams.get("mobile_view");
- if (mobileView) {
- url.searchParams.append("mobile_view", mobileView);
+ if (
+ entrypointName === "discourse" &&
+ el.tagName.toLowerCase() === "script"
+ ) {
+ const liveReload = dom.window.document.createElement("script");
+ liveReload.setAttribute("async", "");
+ liveReload.src = `${baseURL}ember-cli-live-reload.js`;
+ newElements.unshift(liveReload);
}
- const reqUrlSafeMode = forUrlSearchParams.get("safe_mode");
- if (reqUrlSafeMode) {
- url.searchParams.append("safe_mode", reqUrlSafeMode);
- }
+ el.replaceWith(...newElements);
- const navigationMenu = forUrlSearchParams.get("navigation_menu");
- if (navigationMenu) {
- url.searchParams.append("navigation_menu", navigationMenu);
- }
-
- const reqUrlPreviewThemeId = forUrlSearchParams.get("preview_theme_id");
- if (reqUrlPreviewThemeId) {
- url.searchParams.append("preview_theme_id", reqUrlPreviewThemeId);
- }
-
- const { default: fetch } = await import("node-fetch");
- const res = await fetch(url, { headers: req.headers });
- const json = await res.json();
-
- return applyBootstrap(json.bootstrap, template, response, baseURL, preload);
- } catch (error) {
- throw new Error(
- `Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}`
- );
+ handledEntrypoints.add(entrypointName);
}
}
@@ -364,22 +156,35 @@ async function handleRequest(proxy, baseURL, req, res) {
res.status(response.status);
if (isHTML) {
- const responseText = await response.text();
- const preloadJson = isHTML ? extractPreloadJson(responseText) : null;
+ const [responseText, chunkInfoText, distAssets] = await Promise.all([
+ response.text(),
+ fsPromises.readFile("dist/assets.json", "utf-8"),
+ listDistAssets(),
+ ]);
- if (preloadJson) {
- const html = await buildFromBootstrap(
- proxy,
- baseURL,
- req,
- response,
- extractPreloadJson(responseText)
- );
- res.set("content-type", "text/html");
- res.send(html);
- } else {
- res.send(responseText);
- }
+ const chunkInfos = JSON.parse(chunkInfoText);
+
+ const dom = new JSDOM(responseText);
+
+ updateScriptReferences({
+ chunkInfos,
+ dom,
+ selector: "script[data-discourse-entrypoint]",
+ attribute: "src",
+ baseURL,
+ distAssets,
+ });
+
+ updateScriptReferences({
+ chunkInfos,
+ dom,
+ selector: "link[rel=preload][data-discourse-entrypoint]",
+ attribute: "href",
+ baseURL,
+ distAssets,
+ });
+
+ res.send(dom.serialize());
} else {
res.send(Buffer.from(await response.arrayBuffer()));
}
@@ -392,63 +197,6 @@ module.exports = {
return true;
},
- contentFor(type, config) {
- if (shouldLoadPlugins() && type === "test-plugin-js") {
- const scripts = [];
-
- const pluginInfos = this.app.project
- .findAddonByName("discourse-plugins")
- .pluginInfos();
-
- for (const {
- pluginName,
- directoryName,
- hasJs,
- hasAdminJs,
- } of pluginInfos) {
- if (hasJs) {
- scripts.push({
- src: `plugins/${directoryName}.js`,
- name: pluginName,
- });
- }
-
- if (fs.existsSync(`../plugins/${directoryName}_extras.js.erb`)) {
- scripts.push({
- src: `plugins/${directoryName}_extras.js`,
- name: pluginName,
- });
- }
-
- if (hasAdminJs) {
- scripts.push({
- src: `plugins/${directoryName}_admin.js`,
- name: pluginName,
- });
- }
- }
-
- return scripts
- .map(
- ({ src, name }) =>
- ``
- )
- .join("\n");
- } else if (shouldLoadPlugins() && type === "test-plugin-tests-js") {
- return this.app.project
- .findAddonByName("discourse-plugins")
- .pluginInfos()
- .filter(({ hasTests }) => hasTests)
- .map(
- ({ directoryName, pluginName }) =>
- ``
- )
- .join("\n");
- } else if (shouldLoadPlugins() && type === "test-plugin-css") {
- return ``;
- }
- },
-
serverMiddleware(config) {
const app = config.app;
let { proxy, rootURL, baseURL } = config.options;
diff --git a/app/assets/javascripts/bootstrap-json/package.json b/app/assets/javascripts/bootstrap-json/package.json
index 505d4317184..5cd29882e68 100644
--- a/app/assets/javascripts/bootstrap-json/package.json
+++ b/app/assets/javascripts/bootstrap-json/package.json
@@ -1,7 +1,7 @@
{
"name": "bootstrap-json",
"version": "1.0.0",
- "description": "Express.js middleware that proxies ember cli requests and fetches bootstrap json",
+ "description": "Express.js middleware which injects ember-cli asset URLs into Discourse's HTML",
"author": "Discourse",
"license": "GPL-2.0-only",
"keywords": [
@@ -16,8 +16,8 @@
},
"devDependencies": {
"clean-base-url": "^1.0.0",
- "discourse-plugins": "1.0.0",
"express": "^4.18.2",
+ "glob": "^10.3.10",
"html-entities": "^2.4.0",
"jsdom": "^22.1.0",
"node-fetch": "^3.3.2"
diff --git a/app/assets/javascripts/discourse-plugins/index.js b/app/assets/javascripts/discourse-plugins/index.js
index 0c44dfde5ca..851a94da7c4 100644
--- a/app/assets/javascripts/discourse-plugins/index.js
+++ b/app/assets/javascripts/discourse-plugins/index.js
@@ -267,4 +267,72 @@ module.exports = {
return true;
}
},
+
+ pluginScriptTags(config) {
+ const scripts = [];
+
+ const pluginInfos = this.pluginInfos();
+
+ for (const {
+ pluginName,
+ directoryName,
+ hasJs,
+ hasAdminJs,
+ } of pluginInfos) {
+ if (hasJs) {
+ scripts.push({
+ src: `plugins/${directoryName}.js`,
+ name: pluginName,
+ });
+ }
+
+ if (fs.existsSync(`../plugins/${directoryName}_extras.js.erb`)) {
+ scripts.push({
+ src: `plugins/${directoryName}_extras.js`,
+ name: pluginName,
+ });
+ }
+
+ if (hasAdminJs) {
+ scripts.push({
+ src: `plugins/${directoryName}_admin.js`,
+ name: pluginName,
+ });
+ }
+ }
+
+ return scripts
+ .map(
+ ({ src, name }) =>
+ ``
+ )
+ .join("\n");
+ },
+
+ pluginTestScriptTags(config) {
+ return this.pluginInfos()
+ .filter(({ hasTests }) => hasTests)
+ .map(
+ ({ directoryName, pluginName }) =>
+ ``
+ )
+ .join("\n");
+ },
+
+ contentFor(type, config) {
+ if (!this.shouldLoadPlugins()) {
+ return;
+ }
+
+ switch (type) {
+ case "test-plugin-js":
+ return this.pluginScriptTags(config);
+
+ case "test-plugin-tests-js":
+ return this.pluginTestScriptTags(config);
+
+ case "test-plugin-css":
+ return ``;
+ }
+ },
};
diff --git a/app/assets/javascripts/discourse/app/index.html b/app/assets/javascripts/discourse/app/index.html
index 3b465595f3b..1eaa9ff9915 100644
--- a/app/assets/javascripts/discourse/app/index.html
+++ b/app/assets/javascripts/discourse/app/index.html
@@ -2,57 +2,23 @@
Discourse - Ember CLI
-
-
-
-
- {{content-for "before-script-load"}}
-
-
- {{content-for "discourse-preload-stylesheets"}}
-
- {{content-for "head"}}
-
-
-
-
-
-
- {{content-for "discourse-stylesheets"}}
-
-
-
-
-
-
-
-
- {{content-for "body"}}
-
-
-
-
-
-
-
-
- {{content-for "body-footer"}}
diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock
index 173c9e38b19..a6331dff10f 100644
--- a/app/assets/javascripts/yarn.lock
+++ b/app/assets/javascripts/yarn.lock
@@ -1470,6 +1470,18 @@
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-2.0.0.tgz#5e8b7298f31ff8f7b260e6b7363c7e9ceed7d9c5"
integrity sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==
+"@isaacs/cliui@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
+ integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+ dependencies:
+ string-width "^5.1.2"
+ string-width-cjs "npm:string-width@^4.2.0"
+ strip-ansi "^7.0.1"
+ strip-ansi-cjs "npm:strip-ansi@^6.0.1"
+ wrap-ansi "^8.1.0"
+ wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
@@ -1536,6 +1548,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@pkgjs/parseargs@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
+ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
+
"@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
@@ -2372,6 +2389,11 @@ ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+ansi-regex@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
+ integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
+
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@@ -2391,6 +2413,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
+ansi-styles@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
ansi-to-html@^0.6.15, ansi-to-html@^0.6.6:
version "0.6.15"
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7"
@@ -4607,6 +4634,11 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
editions@^1.1.1:
version "1.3.4"
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
@@ -5344,6 +5376,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@@ -6111,6 +6148,14 @@ for-in@^1.0.2:
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
+foreground-child@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
+ integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
+ dependencies:
+ cross-spawn "^7.0.0"
+ signal-exit "^4.0.1"
+
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -6395,6 +6440,17 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+glob@^10.3.10:
+ version "10.3.10"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
+ integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
+ dependencies:
+ foreground-child "^3.1.0"
+ jackspeak "^2.3.5"
+ minimatch "^9.0.1"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+ path-scurry "^1.10.1"
+
glob@^5.0.10:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -7344,6 +7400,15 @@ istextorbinary@^2.5.1:
editions "^2.2.0"
textextensions "^2.5.0"
+jackspeak@^2.3.5:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
+ integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+ optionalDependencies:
+ "@pkgjs/parseargs" "^0.11.0"
+
jest-worker@^27.4.5:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
@@ -7904,6 +7969,11 @@ lru-cache@^7.5.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
+"lru-cache@^9.1.1 || ^10.0.0":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
+ integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
+
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
@@ -8157,6 +8227,13 @@ minimatch@^7.4.3:
dependencies:
brace-expansion "^2.0.1"
+minimatch@^9.0.1:
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
+ integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
+ dependencies:
+ brace-expansion "^2.0.1"
+
minimatch@~0.2.11:
version "0.2.14"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a"
@@ -8178,6 +8255,11 @@ minipass@^2.2.0:
safe-buffer "^5.1.2"
yallist "^3.0.0"
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
+ integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
+
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -8789,6 +8871,14 @@ path-root@^0.1.1:
dependencies:
path-root-regex "^0.1.0"
+path-scurry@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
+ integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
+ dependencies:
+ lru-cache "^9.1.1 || ^10.0.0"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
@@ -9683,6 +9773,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+signal-exit@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
silent-error@^1.0.0, silent-error@^1.0.1, silent-error@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/silent-error/-/silent-error-1.1.1.tgz#f72af5b0d73682a2ba1778b7e32cd8aa7c2d8662"
@@ -9939,7 +10034,7 @@ string-template@~0.2.0, string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9956,6 +10051,15 @@ string-width@^2.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
+string-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
string.prototype.matchall@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
@@ -10009,6 +10113,13 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -10030,12 +10141,12 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+strip-ansi@^7.0.1:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
- ansi-regex "^5.0.1"
+ ansi-regex "^6.0.1"
strip-bom@^4.0.0:
version "4.0.0"
@@ -11020,6 +11131,16 @@ workerpool@^6.0.0, workerpool@^6.0.2, workerpool@^6.4.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.1.tgz#1398eb5f8f44fb2d21ed9225cf34bb0131504c1d"
integrity sha512-zIK7qRgM1Mk+ySxOJl7ZpjX6SlKt5gugxzl8eXHPdbpXX8iDAaVIxYJz4Apn6JdDxP2buY/Ekqg0bOLNSf0u0g==
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+ name wrap-ansi-cjs
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -11029,14 +11150,14 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+wrap-ansi@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"
wrappy@1:
version "1.0.2"
diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb
index 575dd25f78d..4770ee17cc2 100644
--- a/app/controllers/bootstrap_controller.rb
+++ b/app/controllers/bootstrap_controller.rb
@@ -1,103 +1,8 @@
# frozen_string_literal: true
class BootstrapController < ApplicationController
- include ApplicationHelper
-
skip_before_action :redirect_to_login_if_required, :check_xhr
- # This endpoint allows us to produce the data required to start up Discourse via JSON API,
- # so that you don't have to scrape the HTML for `data-*` payloads
- def index
- locale = script_asset_path("locales/#{I18n.locale}")
-
- preload_anonymous_data
- if current_user
- current_user.sync_notification_channel_position
- preload_current_user_data
- end
-
- @stylesheets = []
-
- add_scheme(scheme_id, "all", "light-scheme")
- add_scheme(dark_scheme_id, "(prefers-color-scheme: dark)", "dark-scheme")
-
- if rtl?
- add_style(mobile_view? ? :mobile_rtl : :desktop_rtl)
- else
- add_style(mobile_view? ? :mobile : :desktop)
- end
- add_style(rtl? ? :admin_rtl : :admin) if staff?
- add_style(rtl? ? :wizard_rtl : :wizard) if admin?
-
- assets_fake_request = ActionDispatch::Request.new(request.env.dup)
- assets_for_url = params[:for_url]
- if assets_for_url
- path, query = assets_for_url.split("?", 2)
- assets_fake_request.env["PATH_INFO"] = path
- assets_fake_request.env["QUERY_STRING"] = query
- end
-
- Discourse
- .find_plugin_css_assets(
- include_official: allow_plugins?,
- include_unofficial: allow_third_party_plugins?,
- mobile_view: mobile_view?,
- desktop_view: !mobile_view?,
- request: assets_fake_request,
- rtl: rtl?,
- )
- .each { |file| add_style(file, plugin: true) }
- add_style(mobile_view? ? :mobile_theme : :desktop_theme) if theme_id.present?
-
- extra_locales = []
- if ExtraLocalesController.client_overrides_exist?
- extra_locales << ExtraLocalesController.url("overrides")
- end
-
- extra_locales << ExtraLocalesController.url("admin") if staff?
-
- extra_locales << ExtraLocalesController.url("wizard") if admin?
-
- plugin_js =
- Discourse
- .find_plugin_js_assets(
- include_official: allow_plugins?,
- include_unofficial: allow_third_party_plugins?,
- request: assets_fake_request,
- )
- .map { |f| script_asset_path(f) }
-
- plugin_test_js =
- if Rails.env != "production"
- script_asset_path("plugin-tests")
- else
- []
- end
-
- bootstrap = {
- theme_id: theme_id,
- theme_color: "##{ColorScheme.hex_for_name("header_background", scheme_id)}",
- title: SiteSetting.title,
- current_homepage: current_homepage,
- locale_script: locale,
- stylesheets: @stylesheets,
- plugin_js: plugin_js,
- plugin_test_js: plugin_test_js,
- setup_data: client_side_setup_data,
- preloaded: @preloaded,
- html: create_html,
- theme_html: create_theme_html,
- html_classes: html_classes,
- html_lang: html_lang,
- login_path: main_app.login_path,
- authentication_data: authentication_data,
- }
- bootstrap[:extra_locales] = extra_locales if extra_locales.present?
- bootstrap[:csrf_token] = form_authenticity_token if current_user
-
- render_json_dump(bootstrap: bootstrap)
- end
-
def plugin_css_for_tests
urls =
Discourse
@@ -114,74 +19,4 @@ class BootstrapController < ApplicationController
render plain: stylesheet, content_type: "text/css"
end
-
- private
-
- def add_scheme(scheme_id, media, css_class)
- return if scheme_id.to_i == -1
-
- if style =
- Stylesheet::Manager.new(theme_id: theme_id).color_scheme_stylesheet_details(
- scheme_id,
- media,
- )
- @stylesheets << { href: style[:new_href], media: media, class: css_class }
- end
- end
-
- def add_style(target, opts = nil)
- if styles = Stylesheet::Manager.new(theme_id: theme_id).stylesheet_details(target, "all")
- styles.each do |style|
- @stylesheets << {
- href: style[:new_href],
- media: "all",
- theme_id: style[:theme_id],
- target: style[:target],
- }.merge(opts || {})
- end
- end
- end
-
- def create_html
- html = {}
- return html unless allow_plugins?
-
- add_plugin_html(html, :before_body_close)
- add_plugin_html(html, :before_head_close)
- add_plugin_html(html, :before_script_load)
- add_plugin_html(html, :header)
-
- html
- end
-
- def add_plugin_html(html, key)
- add_if_present(
- html,
- key,
- DiscoursePluginRegistry.build_html("server:#{key.to_s.dasherize}", self),
- )
- end
-
- def create_theme_html
- theme_html = {}
- return theme_html if customization_disabled?
-
- theme_view = mobile_view? ? :mobile : :desktop
-
- add_if_present(theme_html, :body_tag, Theme.lookup_field(theme_id, theme_view, "body_tag"))
- add_if_present(theme_html, :head_tag, Theme.lookup_field(theme_id, theme_view, "head_tag"))
- add_if_present(theme_html, :header, Theme.lookup_field(theme_id, theme_view, "header"))
- add_if_present(
- theme_html,
- :translations,
- Theme.lookup_field(theme_id, :translations, I18n.locale),
- )
- add_if_present(theme_html, :js, Theme.lookup_field(theme_id, :extra_js, nil))
-
- theme_html
- end
-
- def add_if_present(hash, key, val)
- hash[key] = val if val.present?
- end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5528194c713..b9c3f0680fd 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -142,22 +142,24 @@ module ApplicationHelper
scripts
.map do |name|
path = script_asset_path(name)
- preload_script_url(path)
+ preload_script_url(path, entrypoint: script)
end
.join("\n")
.html_safe
end
- def preload_script_url(url)
+ def preload_script_url(url, entrypoint: nil)
+ entrypoint_attribute = entrypoint ? "data-discourse-entrypoint=\"#{entrypoint}\"" : ""
+
add_resource_preload_list(url, "script")
if GlobalSetting.preload_link_header
<<~HTML.html_safe
-
+
HTML
else
<<~HTML.html_safe
-
-
+
+
HTML
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 6f90e17fa90..56ddd26069e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,8 +27,6 @@ Discourse::Application.routes.draw do
match "/404", to: "exceptions#not_found", via: %i[get post]
get "/404-body" => "exceptions#not_found_body"
- get "/bootstrap" => "bootstrap#index"
-
if Rails.env.test? || Rails.env.development?
get "/bootstrap/plugin-css-for-tests.css" => "bootstrap#plugin_css_for_tests"
end
diff --git a/lib/ember_cli.rb b/lib/ember_cli.rb
index 7e1b9a4097d..7c35936c6ca 100644
--- a/lib/ember_cli.rb
+++ b/lib/ember_cli.rb
@@ -2,19 +2,18 @@
class EmberCli < ActiveSupport::CurrentAttributes
# Cache which persists for the duration of a request
- attribute :request_cached_script_chunks
+ attribute :request_cache
def self.dist_dir
"#{Rails.root}/app/assets/javascripts/discourse/dist"
end
def self.assets
- @assets ||= Dir.glob("**/*.{js,map,txt}", base: "#{dist_dir}/assets")
+ cache[:assets] ||= Dir.glob("**/*.{js,map,txt}", base: "#{dist_dir}/assets")
end
def self.script_chunks
- return @production_chunk_infos if @production_chunk_infos
- return self.request_cached_script_chunks if self.request_cached_script_chunks
+ return cache[:script_chunks] if cache[:script_chunks]
chunk_infos = JSON.parse(File.read("#{dist_dir}/assets.json"))
@@ -30,8 +29,7 @@ class EmberCli < ActiveSupport::CurrentAttributes
chunk_infos["vendor"] = [fingerprinted.delete_suffix(".js")]
end
- @production_chunk_infos = chunk_infos if Rails.env.production?
- self.request_cached_script_chunks = chunk_infos
+ cache[:script_chunks] = chunk_infos
rescue Errno::ENOENT
{}
end
@@ -62,9 +60,16 @@ class EmberCli < ActiveSupport::CurrentAttributes
File.exist?("#{dist_dir}/tests/index.html")
end
+ def self.cache
+ if Rails.env.development?
+ self.request_cache ||= {}
+ else
+ @production_cache ||= {}
+ end
+ end
+
def self.clear_cache!
- @production_chunk_infos = nil
- @assets = nil
- self.request_cached_script_chunks = nil
+ self.request.cache = nil
+ @production_cache = nil
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 29ed92dcd54..113fff78d44 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -3,10 +3,10 @@
RSpec.describe ApplicationHelper do
describe "preload_script" do
- def script_tag(url)
+ def script_tag(url, entrypoint)
<<~HTML
-
-
+
+
HTML
end
@@ -57,33 +57,44 @@ RSpec.describe ApplicationHelper do
helper.request.env["HTTP_ACCEPT_ENCODING"] = "br"
link = helper.preload_script("start-discourse")
- expect(link).to eq(script_tag("https://s3cdn.com/assets/start-discourse.br.js"))
+ expect(link).to eq(
+ script_tag("https://s3cdn.com/assets/start-discourse.br.js", "start-discourse"),
+ )
end
it "gives s3 cdn if asset host is not set" do
link = helper.preload_script("start-discourse")
- expect(link).to eq(script_tag("https://s3cdn.com/assets/start-discourse.js"))
+ expect(link).to eq(
+ script_tag("https://s3cdn.com/assets/start-discourse.js", "start-discourse"),
+ )
end
it "can fall back to gzip compression" do
helper.request.env["HTTP_ACCEPT_ENCODING"] = "gzip"
link = helper.preload_script("start-discourse")
- expect(link).to eq(script_tag("https://s3cdn.com/assets/start-discourse.gz.js"))
+ expect(link).to eq(
+ script_tag("https://s3cdn.com/assets/start-discourse.gz.js", "start-discourse"),
+ )
end
it "gives s3 cdn even if asset host is set" do
set_cdn_url "https://awesome.com"
link = helper.preload_script("start-discourse")
- expect(link).to eq(script_tag("https://s3cdn.com/assets/start-discourse.js"))
+ expect(link).to eq(
+ script_tag("https://s3cdn.com/assets/start-discourse.js", "start-discourse"),
+ )
end
it "gives s3 cdn but without brotli/gzip extensions for theme tests assets" do
helper.request.env["HTTP_ACCEPT_ENCODING"] = "gzip, br"
link = helper.preload_script("discourse/tests/theme_qunit_ember_jquery")
expect(link).to eq(
- script_tag("https://s3cdn.com/assets/discourse/tests/theme_qunit_ember_jquery.js"),
+ script_tag(
+ "https://s3cdn.com/assets/discourse/tests/theme_qunit_ember_jquery.js",
+ "discourse/tests/theme_qunit_ember_jquery",
+ ),
)
end
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index ea992fdd838..bd7b0700a78 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -900,6 +900,13 @@ RSpec.describe ApplicationController do
{ HTTP_ACCEPT_LANGUAGE: locale }
end
+ def locale_scripts(body)
+ Nokogiri::HTML5
+ .parse(body)
+ .css('script[src*="assets/locales/"]')
+ .map { |script| script.attributes["src"].value }
+ end
+
context "with allow_user_locale disabled" do
context "when accept-language header differs from default locale" do
before do
@@ -909,9 +916,9 @@ RSpec.describe ApplicationController do
context "with an anonymous user" do
it "uses the default locale" do
- get "/bootstrap.json", headers: headers("fr")
+ get "/latest", headers: headers("fr")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("en.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/en.js")
end
end
@@ -920,9 +927,9 @@ RSpec.describe ApplicationController do
user = Fabricate(:user, locale: :fr)
sign_in(user)
- get "/bootstrap.json", headers: headers("fr")
+ get "/latest", headers: headers("fr")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("en.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/en.js")
end
end
end
@@ -938,15 +945,15 @@ RSpec.describe ApplicationController do
context "with an anonymous user" do
it "uses the locale from the headers" do
- get "/bootstrap.json", headers: headers("fr")
+ get "/latest", headers: headers("fr")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("fr.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/fr.js")
end
it "doesn't leak after requests" do
- get "/bootstrap.json", headers: headers("fr")
+ get "/latest", headers: headers("fr")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("fr.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/fr.js")
expect(I18n.locale.to_s).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE)
end
end
@@ -957,9 +964,9 @@ RSpec.describe ApplicationController do
before { sign_in(user) }
it "uses the user's preferred locale" do
- get "/bootstrap.json", headers: headers("fr")
+ get "/latest", headers: headers("fr")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("fr.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/fr.js")
end
it "serves a 404 page in the preferred locale" do
@@ -983,9 +990,9 @@ RSpec.describe ApplicationController do
SiteSetting.set_locale_from_accept_language_header = true
SiteSetting.default_locale = "en"
- get "/bootstrap.json", headers: headers("zh-CN")
+ get "/latest", headers: headers("zh-CN")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("zh_CN.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/zh_CN.js")
end
end
@@ -994,9 +1001,9 @@ RSpec.describe ApplicationController do
SiteSetting.allow_user_locale = true
SiteSetting.default_locale = "en"
- get "/bootstrap.json", headers: headers("")
+ get "/latest", headers: headers("")
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("en.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/en.js")
end
end
end
@@ -1011,18 +1018,18 @@ RSpec.describe ApplicationController do
context "with an anonymous user" do
it "uses the locale from the cookie" do
- get "/bootstrap.json", headers: { Cookie: "locale=es" }
+ get "/latest", headers: { Cookie: "locale=es" }
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("es.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/es.js")
expect(I18n.locale.to_s).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) # doesn't leak after requests
end
end
context "when the preferred locale includes a region" do
it "returns the locale and region separated by an underscore" do
- get "/bootstrap.json", headers: { Cookie: "locale=zh-CN" }
+ get "/latest", headers: { Cookie: "locale=zh-CN" }
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("zh_CN.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/zh_CN.js")
end
end
end
@@ -1032,9 +1039,9 @@ RSpec.describe ApplicationController do
SiteSetting.allow_user_locale = true
SiteSetting.default_locale = "en"
- get "/bootstrap.json", headers: { Cookie: "" }
+ get "/latest", headers: { Cookie: "" }
expect(response.status).to eq(200)
- expect(response.parsed_body["bootstrap"]["locale_script"]).to end_with("en.js")
+ expect(locale_scripts(response.body)).to contain_exactly("/assets/locales/en.js")
end
end
end
diff --git a/spec/requests/bootstrap_controller_spec.rb b/spec/requests/bootstrap_controller_spec.rb
deleted file mode 100644
index e229f47f866..00000000000
--- a/spec/requests/bootstrap_controller_spec.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe BootstrapController do
- let(:theme) { Fabricate(:theme, enabled: true) }
-
- before do
- DiscoursePluginRegistry.register_html_builder("server:before-head-close") { "wat" }
- theme.set_field(target: :desktop, name: :header, value: "custom header
").save
- SiteSetting.default_theme_id = theme.id
- end
-
- after do
- DiscoursePluginRegistry.reset!
- ExtraLocalesController.clear_cache!
- end
-
- it "returns data as anonymous" do
- get "/bootstrap.json"
- expect(response.status).to eq(200)
-
- json = response.parsed_body
- expect(json).to be_present
-
- bootstrap = json["bootstrap"]
- expect(bootstrap).to be_present
- expect(bootstrap["title"]).to be_present
- expect(bootstrap["theme_id"]).to eq(theme.id)
- expect(bootstrap["setup_data"]["base_url"]).to eq(Discourse.base_url)
- expect(bootstrap["stylesheets"]).to be_present
-
- expect(bootstrap["html"]).to be_present
- expect(bootstrap["html"]["before_head_close"]).to eq("wat")
-
- expect(bootstrap["theme_html"]).to be_present
- expect(bootstrap["theme_html"]["header"]).to eq("custom header
")
-
- preloaded = bootstrap["preloaded"]
- expect(preloaded["site"]).to be_present
- expect(preloaded["siteSettings"]).to be_present
- expect(preloaded["currentUser"]).to be_blank
- expect(preloaded["topicTrackingStates"]).to be_blank
-
- expect(bootstrap["html_classes"]).to eq("desktop-view not-mobile-device text-size-normal anon")
- expect(bootstrap["html_lang"]).to eq("en")
- end
-
- it "returns user data when authenticated" do
- user = Fabricate(:user)
- sign_in(user)
- get "/bootstrap.json"
- expect(response.status).to eq(200)
-
- json = response.parsed_body
- expect(json).to be_present
-
- bootstrap = json["bootstrap"]
- preloaded = bootstrap["preloaded"]
- expect(preloaded["currentUser"]).to be_present
- expect(preloaded["topicTrackingStates"]).to be_present
- end
-
- it "returns extra locales (admin) when staff" do
- user = Fabricate(:admin)
- sign_in(user)
- get "/bootstrap.json"
- expect(response.status).to eq(200)
-
- json = response.parsed_body
- expect(json).to be_present
-
- bootstrap = json["bootstrap"]
- expect(bootstrap["extra_locales"]).to be_present
- end
-
- it "returns data when login_required is enabled" do
- SiteSetting.login_required = true
- get "/bootstrap.json"
- expect(response.status).to eq(200)
- expect(response.parsed_body).to be_present
- end
-
- context "when authentication data is present" do
- it "returns authentication data" do
- cookie_data = "someauthenticationdata"
- cookies["authentication_data"] = cookie_data
-
- get "/bootstrap.json"
-
- bootstrap = response.parsed_body["bootstrap"]
- expect(bootstrap["authentication_data"]).to eq(cookie_data)
- end
- end
-
- context "with a plugin asset filter" do
- let :plugin do
- plugin = plugin_from_fixtures("my_plugin")
- plugin.register_asset_filter do |type, request|
- next true if request.path == "/mypluginroute"
- false
- end
- plugin
- end
-
- before do
- Discourse.plugins << plugin
- plugin.activate!
- end
-
- after { Discourse.plugins.delete plugin }
-
- it "filters assets using the given path" do
- get "/bootstrap.json"
- expect(response.status).to eq(200)
- plugin_assets = response.parsed_body.dig("bootstrap", "plugin_js")
- expect(plugin_assets).not_to include(a_string_matching "my_plugin")
-
- get "/bootstrap.json?for_url=/mypluginroute"
- expect(response.status).to eq(200)
- plugin_assets = response.parsed_body.dig("bootstrap", "plugin_js")
- expect(plugin_assets).to include(a_string_matching "my_plugin")
- end
- end
-end