diff --git a/app/assets/javascripts/discourse/app/index.html b/app/assets/javascripts/discourse/app/index.html index 35176355a3..28558df7de 100644 --- a/app/assets/javascripts/discourse/app/index.html +++ b/app/assets/javascripts/discourse/app/index.html @@ -1,5 +1,5 @@ - +{{bootstrap-content-for "html-tag"}} @@ -7,29 +7,32 @@ + {{bootstrap-content-for "before-script-load"}} + {{content-for "before-script-load"}} + + + + + + {{bootstrap-content-for "head"}} {{content-for "head"}} + {{bootstrap-content-for "body"}} {{content-for "body"}}
- - - +
+
+ + {{bootstrap-content-for "hidden-login-form"}} + {{bootstrap-content-for "preloaded"}} + - - + {{bootstrap-content-for "body-footer"}} {{content-for "body-footer"}} diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js index be37c27df4..ceb7796a18 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js @@ -9,7 +9,6 @@ import I18n from "I18n"; import PreloadStore from "discourse/lib/preload-store"; import RSVP from "rsvp"; import Session from "discourse/models/session"; -import { camelize } from "@ember/string"; import deprecated from "discourse-common/lib/deprecated"; import { setDefaultOwner } from "discourse-common/lib/get-owner"; import { setIconList } from "discourse-common/lib/icon-library"; @@ -29,21 +28,12 @@ export default { } let setupData; - let preloaded; - if (app.bootstrap) { - // This is annoying but our old way of using `data-*` attributes used camelCase by default - setupData = {}; - Object.keys(app.bootstrap.setup_data).forEach((k) => { - setupData[camelize(k)] = app.bootstrap.setup_data[k]; - }); - preloaded = app.bootstrap.preloaded; - } - const setupDataElement = document.getElementById("data-discourse-setup"); if (setupDataElement) { setupData = setupDataElement.dataset; } + let preloaded; const preloadedDataElement = document.getElementById("data-preloaded"); if (preloadedDataElement) { preloaded = JSON.parse(preloadedDataElement.dataset.preloaded); diff --git a/app/assets/javascripts/discourse/config/environment.js b/app/assets/javascripts/discourse/config/environment.js index c704e44c56..e004e1eaba 100644 --- a/app/assets/javascripts/discourse/config/environment.js +++ b/app/assets/javascripts/discourse/config/environment.js @@ -6,6 +6,7 @@ module.exports = function (environment) { environment, rootURL: "/", locationType: "auto", + historySupportMiddleware: false, EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 22cc046d42..bd6fbbccb6 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -28,7 +28,9 @@ module.exports = function (defaults) { let discourseRoot = resolve("../../../.."); let vendorJs = discourseRoot + "/vendor/assets/javascripts/"; - let app = new EmberApp(defaults, { autoRun: false }); + let app = new EmberApp(defaults, { + autoRun: false, + }); // WARNING: We should only import scripts here if they are not in NPM. // For example: our very specific version of bootstrap-modal. diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js new file mode 100644 index 0000000000..d75ea398fa --- /dev/null +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -0,0 +1,234 @@ +"use strict"; + +const bent = require("bent"); +const getJSON = bent("json"); +const { encode } = require("html-entities"); +const cleanBaseURL = require("clean-base-url"); +const path = require("path"); +const fs = require("fs"); + +const IGNORE_PATHS = [ + /\/ember-cli-live-reload\.js$/, + /\/session\/[^\/]+\/become$/, +]; + +function htmlTag(buffer, bootstrap) { + let classList = ""; + if (bootstrap.html_classes) { + classList = ` class="${bootstrap.html_classes}"`; + } + buffer.push(``); +} + +function head(buffer, bootstrap) { + if (bootstrap.csrf_token) { + buffer.push(``); + buffer.push(``); + } + if (bootstrap.theme_ids) { + 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); + } else { + val = val.toString(); + } + setupData += ` data-${sd.replace(/\_/g, "-")}="${encode(val)}"`; + } + }); + buffer.push(``); + + (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}"`); + } + let link = `\n`; + buffer.push(link); + }); + + bootstrap.plugin_js.forEach((src) => + buffer.push(``) + ); + + 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 beforeScriptLoad(buffer, bootstrap) { + buffer.push(bootstrap.html.before_script_load); + buffer.push(``); + (bootstrap.extra_locales || []).forEach((l) => + buffer.push(``) + ); +} + +function body(buffer, bootstrap) { + buffer.push(bootstrap.theme_html.header); + buffer.push(bootstrap.html.header); +} + +function bodyFooter(buffer, bootstrap) { + buffer.push(bootstrap.theme_html.body_tag); + buffer.push(bootstrap.html.before_body_close); +} + +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, + head: head, + body: body, + "hidden-login-form": hiddenLoginForm, + preloaded: preloaded, + "body-footer": bodyFooter, +}; + +function replaceIn(bootstrap, template, id) { + let buffer = []; + BUILDERS[id](buffer, bootstrap); + let contents = buffer.filter((b) => b && b.length > 0).join("\n"); + + return template.replace(`{{bootstrap-content-for "${id}"}}`, contents); +} + +function applyBootstrap(bootstrap, template) { + Object.keys(BUILDERS).forEach((id) => { + template = replaceIn(bootstrap, template, id); + }); + return template; +} + +function decorateIndex(baseUrl, headers) { + // eslint-disable-next-line + return new Promise((resolve, reject) => { + fs.readFile( + path.join(process.cwd(), "dist", "index.html"), + "utf8", + (err, template) => { + getJSON(`${baseUrl}/bootstrap.json`, null, headers) + .then((json) => { + resolve(applyBootstrap(json.bootstrap, template)); + }) + .catch(() => { + reject(`Could not get ${baseUrl}/bootstrap.json`); + }); + } + ); + }); +} + +module.exports = { + name: require("./package").name, + + isDevelopingAddon() { + return true; + }, + + serverMiddleware(config) { + let proxy = config.options.proxy; + let app = config.app; + let options = config.options; + + let watcher = options.watcher; + + let baseURL = + options.rootURL === "" + ? "/" + : cleanBaseURL(options.rootURL || options.baseURL); + + app.use(async (req, res, next) => { + try { + const results = await watcher; + if (this.shouldHandleRequest(req, options)) { + let assetPath = req.path.slice(baseURL.length); + let isFile = false; + + try { + isFile = fs + .statSync(path.join(results.directory, assetPath)) + .isFile(); + } catch (err) { + /* ignore */ + } + + if (!isFile) { + let template; + try { + template = await decorateIndex(proxy, req.headers); + } catch (e) { + template = ` + +

Discourse Build Error

+

${e.toString()}

+ + `; + } + res.send(template); + } + } + } finally { + next(); + } + }); + }, + + shouldHandleRequest(req, options) { + let acceptHeaders = req.headers.accept || []; + let hasHTMLHeader = acceptHeaders.indexOf("text/html") !== -1; + if (req.method !== "GET") { + return false; + } + if (!hasHTMLHeader) { + return false; + } + + if (IGNORE_PATHS.some((ip) => ip.test(req.path))) { + return false; + } + + let baseURL = + options.rootURL === "" + ? "/" + : cleanBaseURL(options.rootURL || options.baseURL); + let baseURLRegexp = new RegExp(`^${baseURL}`); + return baseURLRegexp.test(req.path); + }, +}; diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/package.json b/app/assets/javascripts/discourse/lib/bootstrap-json/package.json new file mode 100644 index 0000000000..c79e0bc382 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/package.json @@ -0,0 +1,13 @@ +{ + "name": "bootstrap-json", + "keywords": [ + "ember-addon" + ], + "ember-addon": { + "before": [ + "serve-files-middleware", + "history-support-middleware", + "proxy-server-middleware" + ] + } +} diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 97a11a3d6d..27f98930dc 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -20,6 +20,7 @@ "@glimmer/component": "^1.0.0", "@popperjs/core": "^2.4.4", "admin": "^1.0.0", + "bent": "^7.3.12", "broccoli-asset-rev": "^3.0.0", "discourse-common": "^1.0.0", "discourse-hbr": "^1.0.0", @@ -39,6 +40,7 @@ "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.6.0", "ember-source": "~3.15.0", + "html-entities": "^2.1.0", "loader.js": "^4.7.0", "message-bus-client": "^3.3.0", "mousetrap": "^1.6.5", @@ -55,5 +57,10 @@ }, "ember": { "edition": "default" + }, + "ember-addon": { + "paths": [ + "lib/bootstrap-json" + ] } } diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js index 6d53b78cf8..903de3dba6 100644 --- a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js +++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js @@ -163,117 +163,20 @@ } }); + define("I18n", ["exports"], function (exports) { + return I18n; + }); + window.__widget_helpers = require("discourse-widget-hbs/helpers").default; + // TODO: Eliminate this global window.virtualDom = require("virtual-dom"); - let head = document.getElementsByTagName("head")[0]; - function loadScript(src) { - return new Promise((resolve, reject) => { - let script = document.createElement("script"); - script.onload = () => resolve(); - script.src = src; - head.appendChild(script); - }); - } - - let isTesting = require("discourse-common/config/environment").isTesting; - let element = document.querySelector( `meta[name="discourse/config/environment"]` ); const config = JSON.parse( decodeURIComponent(element.getAttribute("content")) ); - fetch("/bootstrap.json") - .then((res) => res.json()) - .then((data) => { - let bootstrap = data.bootstrap; - - config.bootstrap = bootstrap; - - // We know better, we packaged this. - config.bootstrap.setup_data.markdown_it_url = - "/assets/discourse-markdown.js"; - - let locale = bootstrap.locale_script; - - if (bootstrap.csrf_token) { - const csrfParam = document.createElement("meta"); - csrfParam.setAttribute("name", "csrf-param"); - csrfParam.setAttribute("content", "authenticity_token"); - head.append(csrfParam); - const csrfToken = document.createElement("meta"); - csrfToken.setAttribute("name", "csrf-token"); - csrfToken.setAttribute("content", bootstrap.csrf_token); - head.append(csrfToken); - } - (bootstrap.stylesheets || []).forEach((s) => { - let link = document.createElement("link"); - link.setAttribute("rel", "stylesheet"); - link.setAttribute("type", "text/css"); - link.setAttribute("href", s.href); - if (s.media) { - link.setAttribute("media", s.media); - } - if (s.target) { - link.setAttribute("data-target", s.target); - } - if (s.theme_id) { - link.setAttribute("data-theme-id", s.theme_id); - } - head.append(link); - }); - - let pluginJs = bootstrap.plugin_js; - if (isTesting()) { - // pluginJs = pluginJs.concat(bootstrap.plugin_test_js); - } - - pluginJs.forEach((src) => { - let script = document.createElement("script"); - script.setAttribute("src", src); - head.append(script); - }); - - if (bootstrap.theme_ids) { - let theme_ids = document.createElement("meta"); - theme_ids.setAttribute("name", "discourse_theme_ids"); - theme_ids.setAttribute("content", bootstrap.theme_ids); - head.append(theme_ids); - } - - let htmlElement = document.getElementsByTagName("html")[0]; - htmlElement.classList = bootstrap.html_classes; - htmlElement.setAttribute("lang", bootstrap.html_lang); - - let themeHtml = bootstrap.theme_html; - let html = bootstrap.html; - - head.insertAdjacentHTML("beforeend", themeHtml.translations || ""); - head.insertAdjacentHTML("beforeend", themeHtml.js || ""); - head.insertAdjacentHTML("beforeend", themeHtml.head_tag || ""); - - head.insertAdjacentHTML("afterbegin", html.before_script_load || ""); - head.insertAdjacentHTML("beforeend", html.before_head_close || ""); - - let main = document.getElementById("main"); - main.insertAdjacentHTML("beforebegin", themeHtml.header || ""); - main.insertAdjacentHTML("beforebegin", html.header || ""); - - let body = document.getElementsByTagName("body")[0]; - body.insertAdjacentHTML("beforeend", themeHtml.body_tag || ""); - body.insertAdjacentHTML("beforeend", html.before_body_close || ""); - - loadScript(locale).then(() => { - define("I18n", ["exports"], function (exports) { - return I18n; - }); - window.__widget_helpers = require("discourse-widget-hbs/helpers").default; - let extras = (bootstrap.extra_locales || []).map(loadScript); - return Promise.all(extras).then(() => { - const event = new CustomEvent("discourse-booted", { detail: config }); - document.dispatchEvent(event); - }); - }); - }); + const app = require(`${config.modulePrefix}/app`)["default"].create(config); + app.start(); })(); diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index c95e456820..f524a81984 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -3222,6 +3222,15 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bent@^7.3.12: + version "7.3.12" + resolved "https://registry.yarnpkg.com/bent/-/bent-7.3.12.tgz#e0a2775d4425e7674c64b78b242af4f49da6b035" + integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== + dependencies: + bytesish "^0.4.1" + caseless "~0.12.0" + is-stream "^2.0.0" + better-assert@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" @@ -4052,6 +4061,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytesish@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" + integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== + cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -7140,6 +7154,11 @@ html-encoding-sniffer@^1.0.2: dependencies: whatwg-encoding "^1.0.1" +html-entities@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.1.0.tgz#f5de1f8d5e1f16859a74aa73a90f0db502ca723a" + integrity sha512-u+OHVGMH5P1HlaTFp3M4HolRnWepgx5rAnYBo+7/TrBZahuJjgQ4TMv2GjQ4IouGDzkgXYeOI/NQuF95VOUOsQ== + http-cache-semantics@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb index 5cd0edacdd..9a52f741a2 100644 --- a/app/controllers/bootstrap_controller.rb +++ b/app/controllers/bootstrap_controller.rb @@ -61,7 +61,8 @@ class BootstrapController < ApplicationController html: create_html, theme_html: create_theme_html, html_classes: html_classes, - html_lang: html_lang + html_lang: html_lang, + login_path: main_app.login_path } bootstrap[:extra_locales] = extra_locales if extra_locales.present? bootstrap[:csrf_token] = form_authenticity_token if current_user