From ac896755bb0194e638c771dcca12261cb5e4a81a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 10 Nov 2023 11:16:06 +0000 Subject: [PATCH] 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. --- .../javascripts/bootstrap-json/index.js | 394 ++++-------------- .../javascripts/bootstrap-json/package.json | 4 +- .../javascripts/discourse-plugins/index.js | 68 +++ .../javascripts/discourse/app/index.html | 46 +- app/assets/javascripts/yarn.lock | 147 ++++++- app/controllers/bootstrap_controller.rb | 165 -------- app/helpers/application_helper.rb | 12 +- config/routes.rb | 2 - lib/ember_cli.rb | 23 +- spec/helpers/application_helper_spec.rb | 27 +- spec/requests/application_controller_spec.rb | 47 ++- spec/requests/bootstrap_controller_spec.rb | 123 ------ 12 files changed, 348 insertions(+), 710 deletions(-) delete mode 100644 spec/requests/bootstrap_controller_spec.rb 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