DEV: Simplify ember-cli proxy strategy (#24242)
Previously, the app HTML served by the Ember-CLI proxy was generated based on a 'bootstrap json' payload generated by Rails. This inevitably leads to differences between the Rails HTML and the Ember-CLI HTML. This commit overhauls our proxying strategy. Now, we totally ignore the ember-cli `index.html` file. Instead, we take the full HTML from Rails and surgically replace script URLs based on a `data-discourse-entrypoint` attribute. This should be faster (only one request to Rails), more robust, and less confusing for developers.
This commit is contained in:
parent
80208d0ab6
commit
ac896755bb
|
@ -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(`<html lang="${bootstrap.html_lang}"${classList}>`);
|
||||
}
|
||||
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(`<meta name="csrf-param" content="authenticity_token">`);
|
||||
buffer.push(`<meta name="csrf-token" content="${bootstrap.csrf_token}">`);
|
||||
}
|
||||
for (const el of elements) {
|
||||
const entrypointName = el.dataset.discourseEntrypoint;
|
||||
|
||||
if (bootstrap.theme_id) {
|
||||
buffer.push(
|
||||
`<meta name="discourse_theme_id" content="${bootstrap.theme_id}">`
|
||||
);
|
||||
}
|
||||
if (handledEntrypoints.has(entrypointName)) {
|
||||
el.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bootstrap.theme_color) {
|
||||
buffer.push(`<meta name="theme-color" content="${bootstrap.theme_color}">`);
|
||||
}
|
||||
let chunks = chunkInfos[`assets/${entrypointName}.js`]?.assets;
|
||||
|
||||
if (bootstrap.authentication_data) {
|
||||
buffer.push(
|
||||
`<meta id="data-authentication" data-authentication-data="${encode(
|
||||
bootstrap.authentication_data
|
||||
)}">`
|
||||
);
|
||||
}
|
||||
|
||||
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(`<meta id="data-discourse-setup"${setupData} />`);
|
||||
|
||||
if (bootstrap.preloaded.currentUser) {
|
||||
const user = JSON.parse(bootstrap.preloaded.currentUser);
|
||||
let { admin, staff } = user;
|
||||
|
||||
if (staff) {
|
||||
buffer.push(`<script defer src="${baseURL}assets/admin.js"></script>`);
|
||||
}
|
||||
|
||||
if (admin) {
|
||||
buffer.push(`<script defer src="${baseURL}assets/wizard.js"></script>`);
|
||||
}
|
||||
}
|
||||
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(`<script defer src="${src}"></script>`)
|
||||
);
|
||||
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(`<script defer src="${bootstrap.locale_script}"></script>`);
|
||||
(bootstrap.extra_locales || []).forEach((l) =>
|
||||
buffer.push(`<script defer src="${l}"></script>`)
|
||||
);
|
||||
}
|
||||
|
||||
function beforeScriptLoad(buffer, bootstrap) {
|
||||
buffer.push(bootstrap.html.before_script_load);
|
||||
}
|
||||
|
||||
function discoursePreloadStylesheets(buffer, bootstrap) {
|
||||
(bootstrap.stylesheets || []).forEach((s) => {
|
||||
let link = `<link rel="preload" as="style" href="${s.href}">`;
|
||||
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 = `<link rel="stylesheet" type="text/css" href="${
|
||||
s.href
|
||||
}" ${attrs.join(" ")}>`;
|
||||
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(`
|
||||
<script
|
||||
async
|
||||
type="text/javascript"
|
||||
id="mini-profiler"
|
||||
src="/mini-profiler-resources/includes.js?v=${v}"
|
||||
data-css-url="/mini-profiler-resources/includes.css?v=${v}"
|
||||
data-version="${v}"
|
||||
data-path="/mini-profiler-resources/"
|
||||
data-horizontal-position="right"
|
||||
data-vertical-position="top"
|
||||
data-trivial="false"
|
||||
data-children="false"
|
||||
data-max-traces="20"
|
||||
data-controls="false"
|
||||
data-total-sql-count="false"
|
||||
data-authorized="true"
|
||||
data-toggle-shortcut="alt+p"
|
||||
data-start-hidden="false"
|
||||
data-collapse-results="true"
|
||||
data-html-container="body"
|
||||
data-hidden-custom-fields="x"
|
||||
data-ids="${headers.get("x-miniprofiler-ids")}"
|
||||
></script>
|
||||
`);
|
||||
}
|
||||
|
||||
function hiddenLoginForm(buffer, bootstrap) {
|
||||
if (!bootstrap.preloaded.currentUser) {
|
||||
buffer.push(`
|
||||
<form id='hidden-login-form' method="post" action="${bootstrap.login_path}" style="display: none;">
|
||||
<input name="username" type="text" id="signin_username">
|
||||
<input name="password" type="password" id="signin_password">
|
||||
<input name="redirect" type="hidden">
|
||||
<input type="submit" id="signin-button">
|
||||
</form>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function preloaded(buffer, bootstrap) {
|
||||
buffer.push(
|
||||
`<div class="hidden" id="data-preloaded" data-preloaded="${encode(
|
||||
JSON.stringify(bootstrap.preloaded)
|
||||
)}"></div>`
|
||||
);
|
||||
}
|
||||
|
||||
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(`<html>`, contents);
|
||||
} else {
|
||||
return template.replace(`<!-- bootstrap-content ${id} -->`, 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 }) =>
|
||||
`<script src="${config.rootURL}assets/${src}" data-discourse-plugin="${name}"></script>`
|
||||
)
|
||||
.join("\n");
|
||||
} else if (shouldLoadPlugins() && type === "test-plugin-tests-js") {
|
||||
return this.app.project
|
||||
.findAddonByName("discourse-plugins")
|
||||
.pluginInfos()
|
||||
.filter(({ hasTests }) => hasTests)
|
||||
.map(
|
||||
({ directoryName, pluginName }) =>
|
||||
`<script src="${config.rootURL}assets/plugins/test/${directoryName}_tests.js" data-discourse-plugin="${pluginName}"></script>`
|
||||
)
|
||||
.join("\n");
|
||||
} else if (shouldLoadPlugins() && type === "test-plugin-css") {
|
||||
return `<link rel="stylesheet" href="${config.rootURL}bootstrap/plugin-css-for-tests.css" data-discourse-plugin="_all" />`;
|
||||
}
|
||||
},
|
||||
|
||||
serverMiddleware(config) {
|
||||
const app = config.app;
|
||||
let { proxy, rootURL, baseURL } = config.options;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }) =>
|
||||
`<script src="${config.rootURL}assets/${src}" data-discourse-plugin="${name}"></script>`
|
||||
)
|
||||
.join("\n");
|
||||
},
|
||||
|
||||
pluginTestScriptTags(config) {
|
||||
return this.pluginInfos()
|
||||
.filter(({ hasTests }) => hasTests)
|
||||
.map(
|
||||
({ directoryName, pluginName }) =>
|
||||
`<script src="${config.rootURL}assets/plugins/test/${directoryName}_tests.js" data-discourse-plugin="${pluginName}"></script>`
|
||||
)
|
||||
.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 `<link rel="stylesheet" href="${config.rootURL}bootstrap/plugin-css-for-tests.css" data-discourse-plugin="_all" />`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,57 +2,23 @@
|
|||
<html>
|
||||
<head>
|
||||
<!--
|
||||
👋 Greetings Discourse Developer. This HTML was generated by the ember-cli proxy. If you're looking for
|
||||
<head> content generated by Rails, you'll need to start the server with `ALLOW_EMBER_CLI_PROXY_BYPASS=1`
|
||||
and then visit the Rails port (e.g. `localhost:3000`) directly. Be sure to keep ember-cli running so
|
||||
that JS assets continue to be re-compiled when changes are made.
|
||||
👋 Greetings Discourse Developer. This html file is used by ember-cli/embroider to define our JS entrypoints,
|
||||
but it is never actually used in development or production. Instead, we generate an `assets.json` file which
|
||||
is ingested by Rails and used to generate the correct <script> tags.
|
||||
|
||||
When ember-cli is used as a proxy, we use the rails-generated HTML and replace urls in script/link tags
|
||||
with the local ember-cli versions.
|
||||
-->
|
||||
<meta charset="utf-8">
|
||||
<title>Discourse - Ember CLI</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||||
|
||||
<!-- bootstrap-content before-script-load -->
|
||||
{{content-for "before-script-load"}}
|
||||
|
||||
<!-- bootstrap-content discourse-preload-stylesheets -->
|
||||
{{content-for "discourse-preload-stylesheets"}}
|
||||
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/discourse.css" />
|
||||
|
||||
<!-- bootstrap-content head -->
|
||||
{{content-for "head"}}
|
||||
|
||||
<script defer src="{{rootURL}}assets/vendor.js"></script>
|
||||
|
||||
<script defer src="{{rootURL}}assets/discourse.js"></script>
|
||||
|
||||
<!-- bootstrap-content locale-script -->
|
||||
</head>
|
||||
<body>
|
||||
<discourse-assets>
|
||||
<discourse-assets-stylesheets>
|
||||
<!-- bootstrap-content discourse-stylesheets -->
|
||||
{{content-for "discourse-stylesheets"}}
|
||||
</discourse-assets-stylesheets>
|
||||
<discourse-assets-json>
|
||||
<!-- bootstrap-content preloaded -->
|
||||
</discourse-assets-json>
|
||||
<discourse-assets-icons></discourse-assets-icons>
|
||||
</discourse-assets>
|
||||
|
||||
<!-- bootstrap-content body -->
|
||||
{{content-for "body"}}
|
||||
|
||||
<section id='main'>
|
||||
</section>
|
||||
|
||||
<!-- bootstrap-content hidden-login-form -->
|
||||
|
||||
<script defer src="{{rootURL}}assets/start-discourse.js" data-embroider-ignore></script>
|
||||
|
||||
<!-- bootstrap-content body-footer -->
|
||||
{{content-for "body-footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
<script defer src="#{url}"></script>
|
||||
<script defer src="#{url}" #{entrypoint_attribute}></script>
|
||||
HTML
|
||||
else
|
||||
<<~HTML.html_safe
|
||||
<link rel="preload" href="#{url}" as="script">
|
||||
<script defer src="#{url}"></script>
|
||||
<link rel="preload" href="#{url}" as="script" #{entrypoint_attribute}>
|
||||
<script defer src="#{url}" #{entrypoint_attribute}></script>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
RSpec.describe ApplicationHelper do
|
||||
describe "preload_script" do
|
||||
def script_tag(url)
|
||||
def script_tag(url, entrypoint)
|
||||
<<~HTML
|
||||
<link rel="preload" href="#{url}" as="script">
|
||||
<script defer src="#{url}"></script>
|
||||
<link rel="preload" href="#{url}" as="script" data-discourse-entrypoint="#{entrypoint}">
|
||||
<script defer src="#{url}" data-discourse-entrypoint="#{entrypoint}"></script>
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") { "<b>wat</b>" }
|
||||
theme.set_field(target: :desktop, name: :header, value: "<h1>custom header</h1>").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("<b>wat</b>")
|
||||
|
||||
expect(bootstrap["theme_html"]).to be_present
|
||||
expect(bootstrap["theme_html"]["header"]).to eq("<h1>custom header</h1>")
|
||||
|
||||
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
|
Loading…
Reference in New Issue