DEV: Remove `Ember.TEMPLATES` and centralize template resolution rules (#19220)

In the past, the result of template compilation would be stored directly in `Ember.TEMPLATES`. Following the move to more modern ember-cli-based compilation, templates are now compiled to es6 modules. To handle forward/backwards compatibility during these changes we had logic in `discourse-boot` which would extract templates from the es6 modules and store them into the legacy-style `Ember.TEMPLATES` object.

This commit removes that shim, and updates our resolver to fetch templates directly from es6 modules. This is closer to how 'vanilla' Ember handles template resolution. We still have a lot of discourse-specific logic, but now it is centralised in one location and should be easier to understand and normalize in future.

This commit should not introduce any behaviour change.
This commit is contained in:
David Taylor 2022-11-29 10:24:35 +00:00 committed by GitHub
parent 8b2c2e5c34
commit c139767055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 457 additions and 273 deletions

View File

@ -0,0 +1,102 @@
const pluginRegex = /^discourse\/plugins\/([^\/]+)\/(.*)$/;
const themeRegex = /^discourse\/theme-([^\/]+)\/(.*)$/;
function appendToCache(cache, key, value) {
let cachedValue = cache.get(key);
cachedValue ??= [];
cachedValue.push(value);
cache.set(key, cachedValue);
}
const NAMESPACES = ["discourse/", "wizard/", "admin/"];
function isInRecognisedNamespace(moduleName) {
for (const ns of NAMESPACES) {
if (moduleName.startsWith(ns)) {
return true;
}
}
return false;
}
function isTemplate(moduleName) {
return moduleName.includes("/templates/");
}
/**
* This class provides takes set of core/plugin/theme modules, finds the template modules,
* and makes an efficient lookup table for the resolver to use. It takes care of sourcing
* component/route templates from themes/plugins, and also handles template overrides.
*/
class DiscourseTemplateMap {
coreTemplates = new Map();
pluginTemplates = new Map();
themeTemplates = new Map();
prioritizedCaches = [
this.themeTemplates,
this.pluginTemplates,
this.coreTemplates,
];
/**
* Reset the TemplateMap to use the supplied module names. It is expected that the list
* will be generated using `Object.keys(requirejs.entries)`.
*/
setModuleNames(moduleNames) {
this.coreTemplates.clear();
this.pluginTemplates.clear();
this.themeTemplates.clear();
for (const moduleName of moduleNames) {
if (isInRecognisedNamespace(moduleName) && isTemplate(moduleName)) {
this.#add(moduleName);
}
}
}
#add(originalPath) {
let path = originalPath;
let pluginMatch, themeMatch, cache;
if ((pluginMatch = path.match(pluginRegex))) {
path = pluginMatch[2];
cache = this.pluginTemplates;
} else if ((themeMatch = path.match(themeRegex))) {
path = themeMatch[2];
cache = this.themeTemplates;
} else {
cache = this.coreTemplates;
}
path = path.replace(/^discourse\/templates\//, "");
appendToCache(cache, path, originalPath);
}
/**
* Resolve a template name to a module name, taking into account
* theme/plugin namespaces and overrides.
*/
resolve(name) {
for (const cache of this.prioritizedCaches) {
const val = cache.get(name);
if (val) {
return val[val.length - 1];
}
}
}
/**
* List all available template keys, after theme/plugin namespaces have
* been stripped.
*/
keys() {
const uniqueKeys = new Set([
...this.coreTemplates.keys(),
...this.pluginTemplates.keys(),
...this.themeTemplates.keys(),
]);
return [...uniqueKeys];
}
}
export default new DiscourseTemplateMap();

View File

@ -32,11 +32,25 @@ export function findRawTemplate(name) {
export function buildRawConnectorCache(findOutlets) {
let result = {};
findOutlets(__DISCOURSE_RAW_TEMPLATES, (outletName, resource) => {
result[outletName] = result[outletName] || [];
result[outletName].push({
template: __DISCOURSE_RAW_TEMPLATES[resource],
});
});
findOutlets(
Object.keys(__DISCOURSE_RAW_TEMPLATES),
(outletName, resource) => {
result[outletName] ??= [];
result[outletName].push({
template: __DISCOURSE_RAW_TEMPLATES[resource],
});
}
);
return result;
}
export function eagerLoadRawTemplateModules() {
for (const [key, value] of Object.entries(requirejs.entries)) {
if (
key.includes("/templates/") &&
value.deps.includes("discourse-common/lib/raw-templates")
) {
require(key);
}
}
}

View File

@ -1,10 +1,10 @@
import Ember from "ember";
import { dasherize, decamelize } from "@ember/string";
import deprecated from "discourse-common/lib/deprecated";
import { findHelper } from "discourse-common/lib/helpers";
import SuffixTrie from "discourse-common/lib/suffix-trie";
import Resolver from "ember-resolver";
import { buildResolver as buildLegacyResolver } from "discourse-common/lib/legacy-resolver";
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _options = {};
let moduleSuffixTrie = null;
@ -287,21 +287,19 @@ export function buildResolver(baseName) {
resolveTemplate(parsedName) {
return (
this.findPluginMobileTemplate(parsedName) ||
this.findPluginTemplate(parsedName) ||
this.findMobileTemplate(parsedName) ||
this.findTemplate(parsedName) ||
this.findAdminTemplate(parsedName) ||
this.findWizardTemplate(parsedName) ||
this.findLoadingTemplate(parsedName) ||
this.findConnectorTemplate(parsedName) ||
Ember.TEMPLATES.not_found
this.discourseTemplateModule("not_found")
);
}
findLoadingTemplate(parsedName) {
if (parsedName.fullNameWithoutType.match(/loading$/)) {
return Ember.TEMPLATES.loading;
return this.discourseTemplateModule("loading");
}
}
@ -312,17 +310,7 @@ export function buildResolver(baseName) {
.replace("template:connectors/", "template:")
.replace("components/", "")
);
return this.findTemplate(connectorParsedName, "javascripts/");
}
}
findPluginTemplate(parsedName) {
return this.findTemplate(parsedName, "javascripts/");
}
findPluginMobileTemplate(parsedName) {
if (_options.mobileView) {
return this.findTemplate(parsedName, "javascripts/mobile/");
return this.findTemplate(connectorParsedName);
}
}
@ -332,31 +320,43 @@ export function buildResolver(baseName) {
}
}
/**
* Given a template path, this function will return a template, taking into account
* priority rules for theme and plugin overrides. See `lib/discourse-template-map.js`
*/
discourseTemplateModule(name) {
const resolvedName = DiscourseTemplateMap.resolve(name);
if (resolvedName) {
return require(resolvedName).default;
}
}
findTemplate(parsedName, prefix) {
prefix = prefix || "";
const withoutType = parsedName.fullNameWithoutType,
underscored = decamelize(withoutType).replace(/-/g, "_"),
segments = withoutType.split("/"),
templates = Ember.TEMPLATES;
segments = withoutType.split("/");
return (
// Convert dots and dashes to slashes
templates[prefix + withoutType.replace(/[\.-]/g, "/")] ||
this.discourseTemplateModule(
prefix + withoutType.replace(/[\.-]/g, "/")
) ||
// Default unmodified behavior of original resolveTemplate.
templates[prefix + withoutType] ||
this.discourseTemplateModule(prefix + withoutType) ||
// Underscored without namespace
templates[prefix + underscored] ||
this.discourseTemplateModule(prefix + underscored) ||
// Underscored with first segment as directory
templates[prefix + underscored.replace("_", "/")] ||
this.discourseTemplateModule(prefix + underscored.replace("_", "/")) ||
// Underscore only the last segment
templates[
this.discourseTemplateModule(
`${prefix}${segments.slice(0, -1).join("/")}/${segments[
segments.length - 1
].replace(/-/g, "_")}`
] ||
) ||
// All dasherized
templates[prefix + withoutType.replace(/\//g, "-")]
this.discourseTemplateModule(prefix + withoutType.replace(/\//g, "-"))
);
}
@ -364,17 +364,15 @@ export function buildResolver(baseName) {
// (similar to how discourse lays out templates)
findAdminTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "admin") {
return Ember.TEMPLATES["admin/templates/admin"];
return this.discourseTemplateModule("admin/templates/admin");
}
let namespaced, match;
if (parsedName.fullNameWithoutType.startsWith("components/")) {
return (
// Built-in
this.findTemplate(parsedName, "admin/templates/") ||
// Plugin
this.findTemplate(parsedName, "javascripts/admin/")
this.findTemplate(parsedName, "admin/") // Nested under discourse/templates/admin (e.g. from plugins)
);
} else if (/^admin[_\.-]/.test(parsedName.fullNameWithoutType)) {
namespaced = parsedName.fullNameWithoutType.slice(6);
@ -389,11 +387,9 @@ export function buildResolver(baseName) {
if (namespaced) {
let adminParsedName = this.parseName(`template:${namespaced}`);
resolved =
// Built-in
this.findTemplate(adminParsedName, "admin/templates/") ||
this.findTemplate(parsedName, "admin/templates/") ||
// Plugin
this.findTemplate(adminParsedName, "javascripts/admin/");
this.findTemplate(adminParsedName, "admin/"); // Nested under discourse/templates/admin (e.g. from plugin)
}
return resolved;
@ -401,7 +397,7 @@ export function buildResolver(baseName) {
findWizardTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "wizard") {
return Ember.TEMPLATES["wizard/templates/wizard"];
return this.discourseTemplateModule("wizard/templates/wizard");
}
let namespaced;
@ -415,10 +411,10 @@ export function buildResolver(baseName) {
}
if (namespaced) {
let adminParsedName = this.parseName(
let wizardParsedName = this.parseName(
`template:wizard/templates/${namespaced}`
);
return this.findTemplate(adminParsedName);
return this.findTemplate(wizardParsedName);
}
}
};

View File

@ -9,7 +9,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { action, set } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import Ember from "ember";
import { getOwner } from "discourse-common/lib/get-owner";
let _components = {};
@ -106,12 +106,11 @@ export default Component.extend({
return _components[type];
}
let dasherized = dasherize(type);
let templatePath = `components/${dasherized}`;
let template =
Ember.TEMPLATES[`${templatePath}`] ||
Ember.TEMPLATES[`javascripts/${templatePath}`];
_components[type] = template ? dasherized : null;
const dasherized = dasherize(type);
const componentExists = getOwner(this).hasRegistration(
`component:${dasherized}`
);
_components[type] = componentExists ? dasherized : null;
return _components[type];
},

View File

@ -0,0 +1,9 @@
import { eagerLoadRawTemplateModules } from "discourse-common/lib/raw-templates";
export default {
name: "eager-load-raw-templates",
initialize() {
eagerLoadRawTemplateModules();
},
};

View File

@ -0,0 +1,8 @@
import discourseTemplateMap from "discourse-common/lib/discourse-template-map";
export default {
name: "populate-template-map",
initialize() {
discourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
},
};

View File

@ -1,6 +1,6 @@
import { buildRawConnectorCache } from "discourse-common/lib/raw-templates";
import deprecated from "discourse-common/lib/deprecated";
import Ember from "ember";
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _connectorCache;
let _rawConnectorCache;
@ -25,11 +25,11 @@ const DefaultConnectorClass = {
teardownComponent() {},
};
function findOutlets(collection, callback) {
Object.keys(collection).forEach(function (res) {
if (res.includes("/connectors/")) {
const segments = res.split("/");
let outletName = segments[segments.length - 2];
function findOutlets(keys, callback) {
keys.forEach(function (res) {
const segments = res.split("/");
if (segments.includes("connectors")) {
const outletName = segments[segments.length - 2];
const uniqueName = segments[segments.length - 1];
callback(outletName, res, uniqueName);
@ -45,7 +45,7 @@ export function clearCache() {
function findClass(outletName, uniqueName) {
if (!_classPaths) {
_classPaths = {};
findOutlets(require._eak_seen, (outlet, res, un) => {
findOutlets(Object.keys(require._eak_seen), (outlet, res, un) => {
const possibleConnectorClass = requirejs(res).default;
if (possibleConnectorClass.__id) {
// This is the template, not the connector class
@ -63,20 +63,31 @@ function findClass(outletName, uniqueName) {
: DefaultConnectorClass;
}
/**
* Clear the cache of connectors. Should only be used in tests when
* `requirejs.entries` is changed.
*/
export function expireConnectorCache() {
_connectorCache = null;
}
function buildConnectorCache() {
_connectorCache = {};
findOutlets(Ember.TEMPLATES, (outletName, resource, uniqueName) => {
_connectorCache[outletName] = _connectorCache[outletName] || [];
findOutlets(
DiscourseTemplateMap.keys(),
(outletName, resource, uniqueName) => {
_connectorCache[outletName] = _connectorCache[outletName] || [];
_connectorCache[outletName].push({
outletName,
templateName: resource.replace("javascripts/", ""),
template: Ember.TEMPLATES[resource],
classNames: `${outletName}-outlet ${uniqueName}`,
connectorClass: findClass(outletName, uniqueName),
});
});
_connectorCache[outletName].push({
outletName,
templateName: resource,
template: require(DiscourseTemplateMap.resolve(resource)).default,
classNames: `${outletName}-outlet ${uniqueName}`,
connectorClass: findClass(outletName, uniqueName),
});
}
);
}
export function connectorsFor(outletName) {

View File

@ -3,50 +3,6 @@
throw "Unsupported browser detected";
}
// TODO: Remove this and have resolver find the templates
const discoursePrefix = "discourse/templates/";
const adminPrefix = "admin/templates/";
const wizardPrefix = "wizard/templates/";
const discoursePrefixLength = discoursePrefix.length;
const pluginRegex = /^discourse\/plugins\/([^\/]+)\//;
const themeRegex = /^discourse\/theme-([^\/]+)\//;
Object.keys(requirejs.entries).forEach(function (key) {
let templateKey;
let pluginName;
let themeId;
if (key.startsWith(discoursePrefix)) {
templateKey = key.slice(discoursePrefixLength);
} else if (key.startsWith(adminPrefix) || key.startsWith(wizardPrefix)) {
templateKey = key;
} else if (
(pluginName = key.match(pluginRegex)?.[1]) &&
key.includes("/templates/") &&
require(key).default.__id // really is a template
) {
// This logic mimics the old sprockets compilation system which used to
// output templates directly to `Ember.TEMPLATES` with this naming logic
templateKey = key.slice(`discourse/plugins/${pluginName}/`.length);
templateKey = templateKey.replace("discourse/templates/", "");
templateKey = `javascripts/${templateKey}`;
} else if (
(themeId = key.match(themeRegex)?.[1]) &&
key.includes("/templates/")
) {
// And likewise for themes - this mimics the old logic
templateKey = key.slice(`discourse/theme-${themeId}/`.length);
templateKey = templateKey.replace("discourse/templates/", "");
if (!templateKey.startsWith("javascripts/")) {
templateKey = `javascripts/${templateKey}`;
}
}
if (templateKey) {
Ember.TEMPLATES[templateKey] = require(key).default;
}
});
window.__widget_helpers = require("discourse-widget-hbs/helpers").default;
// TODO: Eliminate this global

View File

@ -2,15 +2,14 @@ import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
import Ember from "ember";
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
acceptance("CustomHTML template", function (needs) {
needs.hooks.beforeEach(() => {
Ember.TEMPLATES["top"] = hbs`<span class='top-span'>TOP</span>`;
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES["top"];
registerTemplateModule(
"discourse/templates/top",
hbs`<span class='top-span'>TOP</span>`
);
});
test("renders custom template", async function (assert) {

View File

@ -10,7 +10,7 @@ import { test } from "qunit";
import I18n from "I18n";
import { hbs } from "ember-cli-htmlbars";
import showModal from "discourse/lib/show-modal";
import Ember from "ember";
import { registerTemplateModule } from "../helpers/template-module-helper";
acceptance("Modal", function (needs) {
let _translations;
@ -54,9 +54,10 @@ acceptance("Modal", function (needs) {
await triggerKeyEvent("#main-outlet", "keydown", "Escape");
assert.ok(!exists(".d-modal:visible"), "ESC should close the modal");
Ember.TEMPLATES[
"modal/not-dismissable"
] = hbs`{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}`;
registerTemplateModule(
"discourse/templates/modal/not-dismissable",
hbs`{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}`
);
showModal("not-dismissable", {});
await settled();
@ -78,7 +79,10 @@ acceptance("Modal", function (needs) {
});
test("rawTitle in modal panels", async function (assert) {
Ember.TEMPLATES["modal/test-raw-title-panels"] = hbs``;
registerTemplateModule(
"discourse/templates/modal/test-raw-title-panels",
hbs``
);
const panels = [
{ id: "test1", rawTitle: "Test 1" },
{ id: "test2", rawTitle: "Test 2" },
@ -96,10 +100,11 @@ acceptance("Modal", function (needs) {
});
test("modal title", async function (assert) {
Ember.TEMPLATES["modal/test-title"] = hbs``;
Ember.TEMPLATES[
"modal/test-title-with-body"
] = hbs`{{#d-modal-body}}test{{/d-modal-body}}`;
registerTemplateModule("discourse/templates/modal/test-title", hbs``);
registerTemplateModule(
"discourse/templates/modal/test-title-with-body",
hbs`{{#d-modal-body}}test{{/d-modal-body}}`
);
await visit("/");

View File

@ -9,9 +9,9 @@ import { action } from "@ember/object";
import { extraConnectorClass } from "discourse/lib/plugin-connectors";
import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import Ember from "ember";
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
const PREFIX = "javascripts/single-test/connectors";
const PREFIX = "discourse/plugins/some-plugin/templates/connectors";
acceptance("Plugin Outlet - Connector Class", function (needs) {
needs.hooks.beforeEach(() => {
@ -49,25 +49,22 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
},
});
Ember.TEMPLATES[
`${PREFIX}/user-profile-primary/hello`
] = hbs`<span class='hello-username'>{{model.username}}</span>
registerTemplateModule(
`${PREFIX}/user-profile-primary/hello`,
hbs`<span class='hello-username'>{{model.username}}</span>
<button class='say-hello' {{on "click" (action "sayHello")}}></button>
<button class='say-hello-using-this' {{on "click" this.sayHello}}></button>
<span class='hello-result'>{{hello}}</span>`;
Ember.TEMPLATES[
`${PREFIX}/user-profile-primary/hi`
] = hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button>
<span class='hi-result'>{{hi}}</span>`;
Ember.TEMPLATES[
`${PREFIX}/user-profile-primary/dont-render`
] = hbs`I'm not rendered!`;
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[`${PREFIX}/user-profile-primary/hello`];
delete Ember.TEMPLATES[`${PREFIX}/user-profile-primary/hi`];
delete Ember.TEMPLATES[`${PREFIX}/user-profile-primary/dont-render`];
<span class='hello-result'>{{hello}}</span>`
);
registerTemplateModule(
`${PREFIX}/user-profile-primary/hi`,
hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button>
<span class='hi-result'>{{hi}}</span>`
);
registerTemplateModule(
`${PREFIX}/user-profile-primary/dont-render`,
hbs`I'm not rendered!`
);
});
test("Renders a template into the outlet", async function (assert) {

View File

@ -7,16 +7,22 @@ import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
import { withPluginApi } from "discourse/lib/plugin-api";
import Ember from "ember";
import { registerTemplateModule } from "../helpers/template-module-helper";
const PREFIX = "javascripts/single-test/connectors";
const PREFIX = "discourse/plugins/some-plugin/templates/connectors";
acceptance("Plugin Outlet - Decorator", function (needs) {
needs.user();
needs.hooks.beforeEach(() => {
Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/foo`] = hbs`FOO`;
Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/bar`] = hbs`BAR`;
registerTemplateModule(
`${PREFIX}/discovery-list-container-top/foo`,
hbs`FOO`
);
registerTemplateModule(
`${PREFIX}/discovery-list-container-top/bar`,
hbs`BAR`
);
withPluginApi("0.8.38", (api) => {
api.decoratePluginOutlet(
@ -37,11 +43,6 @@ acceptance("Plugin Outlet - Decorator", function (needs) {
});
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/foo`];
delete Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/bar`];
});
test("Calls the plugin callback with the rendered outlet", async function (assert) {
await visit("/");

View File

@ -6,21 +6,17 @@ import {
import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
import Ember from "ember";
import { registerTemplateModule } from "../helpers/template-module-helper";
const HELLO = "javascripts/multi-test/connectors/user-profile-primary/hello";
const HELLO =
"discourse/plugins/my-plugin/templates/connectors/user-profile-primary/hello";
const GOODBYE =
"javascripts/multi-test/connectors/user-profile-primary/goodbye";
"discourse/plugins/my-plugin/templates/connectors/user-profile-primary/goodbye";
acceptance("Plugin Outlet - Multi Template", function (needs) {
needs.hooks.beforeEach(() => {
Ember.TEMPLATES[HELLO] = hbs`<span class='hello-span'>Hello</span>`;
Ember.TEMPLATES[GOODBYE] = hbs`<span class='bye-span'>Goodbye</span>`;
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[HELLO];
delete Ember.TEMPLATES[GOODBYE];
registerTemplateModule(HELLO, hbs`<span class='hello-span'>Hello</span>`);
registerTemplateModule(GOODBYE, hbs`<span class='bye-span'>Goodbye</span>`);
});
test("Renders a template into the outlet", async function (assert) {

View File

@ -6,20 +6,17 @@ import {
import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
import Ember from "ember";
import { registerTemplateModule } from "../helpers/template-module-helper";
const CONNECTOR =
"javascripts/single-test/connectors/user-profile-primary/hello";
const CONNECTOR_MODULE =
"discourse/theme-12/templates/connectors/user-profile-primary/hello";
acceptance("Plugin Outlet - Single Template", function (needs) {
needs.hooks.beforeEach(() => {
Ember.TEMPLATES[
CONNECTOR
] = hbs`<span class='hello-username'>{{model.username}}</span>`;
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[CONNECTOR];
registerTemplateModule(
CONNECTOR_MODULE,
hbs`<span class='hello-username'>{{model.username}}</span>`
);
});
test("Renders a template into the outlet", async function (assert) {

View File

@ -76,6 +76,7 @@ import { resetNotificationTypeRenderers } from "discourse/lib/notification-types
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
import { resetModelTransformers } from "discourse/lib/model-transformers";
import { cleanupTemporaryTemplateRegistrations } from "./template-module-helper";
export function currentUser() {
return User.create(sessionFixtures["/session/current.json"].current_user);
@ -207,6 +208,7 @@ export function testCleanup(container, app) {
resetUserMenuTabs();
resetLinkLookup();
resetModelTransformers();
cleanupTemporaryTemplateRegistrations();
}
export function discourseModule(name, options) {

View File

@ -0,0 +1,40 @@
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
import { expireConnectorCache } from "discourse/lib/plugin-connectors";
const modifications = [];
function generateTemplateModule(template) {
return function (_exports) {
Object.defineProperty(_exports, "__esModule", {
value: true,
});
_exports.default = template;
};
}
export function registerTemplateModule(moduleName, template) {
const modificationData = {
moduleName,
existingModule: requirejs.entries[moduleName],
};
delete requirejs.entries[moduleName];
define(moduleName, ["exports"], generateTemplateModule(template));
modifications.push(modificationData);
expireConnectorCache();
DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
}
export function cleanupTemporaryTemplateRegistrations() {
for (const modificationData of modifications.reverse()) {
const { moduleName, existingModule } = modificationData;
delete requirejs.entries[moduleName];
if (existingModule) {
requirejs.entries[moduleName] = existingModule;
}
}
if (modifications.length) {
expireConnectorCache();
DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
}
modifications.clear();
}

View File

@ -1,8 +1,8 @@
import { buildResolver, setResolverOption } from "discourse-common/resolver";
import { module, test } from "qunit";
import Ember from "ember";
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let originalTemplates;
let resolver;
function lookupTemplate(assert, name, expectedTemplate, message) {
@ -11,57 +11,71 @@ function lookupTemplate(assert, name, expectedTemplate, message) {
assert.strictEqual(result, expectedTemplate, message);
}
function setTemplates(lookupTemplateStrings) {
lookupTemplateStrings.forEach(function (lookupTemplateString) {
Ember.TEMPLATES[lookupTemplateString] = lookupTemplateString;
});
function setTemplates(templateModuleNames) {
for (const name of templateModuleNames) {
registerTemplateModule(name, name);
}
}
const DiscourseResolver = buildResolver("discourse");
module("Unit | Ember | resolver", function (hooks) {
hooks.beforeEach(function () {
originalTemplates = Ember.TEMPLATES;
Ember.TEMPLATES = {};
DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
resolver = DiscourseResolver.create({
namespace: { modulePrefix: "discourse" },
});
});
hooks.afterEach(function () {
Ember.TEMPLATES = originalTemplates;
});
test("finds templates in top level dir", function (assert) {
setTemplates(["foobar", "fooBar", "foo_bar", "foo.bar"]);
setTemplates([
"discourse/templates/foobar",
"discourse/templates/fooBar",
"discourse/templates/foo_bar",
"discourse/templates/foo.bar",
]);
// Default unmodified behavior
lookupTemplate(assert, "template:foobar", "foobar", "by lowcased name");
lookupTemplate(
assert,
"template:foobar",
"discourse/templates/foobar",
"by lowcased name"
);
// Default unmodified behavior
lookupTemplate(assert, "template:fooBar", "fooBar", "by camel cased name");
lookupTemplate(
assert,
"template:fooBar",
"discourse/templates/fooBar",
"by camel cased name"
);
// Default unmodified behavior
lookupTemplate(
assert,
"template:foo_bar",
"foo_bar",
"discourse/templates/foo_bar",
"by underscored name"
);
// Default unmodified behavior
lookupTemplate(assert, "template:foo.bar", "foo.bar", "by dotted name");
lookupTemplate(
assert,
"template:foo.bar",
"discourse/templates/foo.bar",
"by dotted name"
);
});
test("finds templates in first-level subdir", function (assert) {
setTemplates(["foo/bar_baz"]);
setTemplates(["discourse/templates/foo/bar_baz"]);
// Default unmodified behavior
lookupTemplate(
assert,
"template:foo/bar_baz",
"foo/bar_baz",
"discourse/templates/foo/bar_baz",
"with subdir defined by slash"
);
@ -69,7 +83,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo.bar_baz",
"foo/bar_baz",
"discourse/templates/foo/bar_baz",
"with subdir defined by dot"
);
@ -77,7 +91,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo-bar_baz",
"foo/bar_baz",
"discourse/templates/foo/bar_baz",
"with subdir defined by dash"
);
@ -85,7 +99,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:fooBarBaz",
"foo/bar_baz",
"discourse/templates/foo/bar_baz",
"with subdir defined by first camel case and the rest of camel cases converted to underscores"
);
@ -93,19 +107,25 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo_bar_baz",
"foo/bar_baz",
"discourse/templates/foo/bar_baz",
"with subdir defined by first underscore"
);
});
test("resolves precedence between overlapping top level dir and first level subdir templates", function (assert) {
setTemplates(["fooBar", "foo_bar", "foo.bar", "foo/bar", "baz/qux"]);
setTemplates([
"discourse/templates/fooBar",
"discourse/templates/foo_bar",
"discourse/templates/foo.bar",
"discourse/templates/foo/bar",
"discourse/templates/baz/qux",
]);
// Directories are prioritized when dotted
lookupTemplate(
assert,
"template:foo.bar",
"foo/bar",
"discourse/templates/foo/bar",
"preferring first level subdir for dotted name"
);
@ -113,7 +133,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo-bar",
"foo/bar",
"discourse/templates/foo/bar",
"preferring first level subdir for dotted name"
);
@ -121,7 +141,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:fooBar",
"fooBar",
"discourse/templates/fooBar",
"preferring top level dir for camel cased name"
);
@ -129,7 +149,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo_bar",
"foo_bar",
"discourse/templates/foo_bar",
"preferring top level dir for underscored name"
);
@ -137,19 +157,19 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:baz-qux",
"baz/qux",
"discourse/templates/baz/qux",
"fallback subdir for dashed name"
);
});
test("finds templates in subdir deeper than one level", function (assert) {
setTemplates(["foo/bar/baz/qux"]);
setTemplates(["discourse/templates/foo/bar/baz/qux"]);
// Default unmodified
lookupTemplate(
assert,
"template:foo/bar/baz/qux",
"foo/bar/baz/qux",
"discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes"
);
@ -157,7 +177,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo.bar.baz.qux",
"foo/bar/baz/qux",
"discourse/templates/foo/bar/baz/qux",
"for subdirs defined by dots"
);
@ -165,7 +185,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo/bar/bazQux",
"foo/bar/baz/qux",
"discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes plus one camel case"
);
@ -173,7 +193,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo/bar/baz_qux",
"foo/bar/baz/qux",
"discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes plus one underscore"
);
@ -211,7 +231,12 @@ module("Unit | Ember | resolver", function (hooks) {
});
test("resolves mobile templates to 'mobile/' namespace", function (assert) {
setTemplates(["mobile/foo", "bar", "mobile/bar", "baz"]);
setTemplates([
"discourse/templates/mobile/foo",
"discourse/templates/bar",
"discourse/templates/mobile/bar",
"discourse/templates/baz",
]);
setResolverOption("mobileView", true);
@ -219,7 +244,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:foo",
"mobile/foo",
"discourse/templates/mobile/foo",
"finding mobile version even if normal one is not present"
);
@ -227,7 +252,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:bar",
"mobile/bar",
"discourse/templates/mobile/bar",
"preferring mobile version when both mobile and normal versions are present"
);
@ -235,71 +260,87 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:baz",
"baz",
"discourse/templates/baz",
"falling back to a normal version when mobile version is not present"
);
});
test("resolves plugin templates to 'javascripts/' namespace", function (assert) {
setTemplates(["javascripts/foo", "bar", "javascripts/bar", "baz"]);
test("resolves templates to plugin and theme namespaces", function (assert) {
setTemplates([
"discourse/plugins/my-plugin/discourse/templates/foo",
"discourse/templates/bar",
"discourse/plugins/my-plugin/discourse/templates/bar",
"discourse/templates/baz",
"discourse/plugins/my-plugin/discourse/templates/baz",
"discourse/theme-12/discourse/templates/baz",
"discourse/templates/qux",
]);
// Default with javascripts/ added
// Defined in plugin only
lookupTemplate(
assert,
"template:foo",
"javascripts/foo",
"discourse/plugins/my-plugin/discourse/templates/foo",
"finding plugin version even if normal one is not present"
);
// Default with javascripts/ added, takes precedence
// Defined in core and plugin
lookupTemplate(
assert,
"template:bar",
"javascripts/bar",
"preferring plugin version when both versions are present"
"discourse/plugins/my-plugin/discourse/templates/bar",
"prefers plugin version over core"
);
// Default when javascripts version not present
// Defined in core and plugin and theme
lookupTemplate(
assert,
"template:baz",
"baz",
"falling back to a normal version when plugin version is not present"
"discourse/theme-12/discourse/templates/baz",
"prefers theme version over plugin and core"
);
// Defined in core only
lookupTemplate(
assert,
"template:qux",
"discourse/templates/qux",
"uses core if there are no theme/plugin definitions"
);
});
test("resolves plugin mobile templates to 'javascripts/mobile/' namespace", function (assert) {
test("resolves plugin mobile templates", function (assert) {
setTemplates([
"javascripts/mobile/foo",
"javascripts/mobile/bar",
"javascripts/bar",
"javascripts/mobile/baz",
"mobile/baz",
"discourse/plugins/my-plugin/discourse/templates/mobile/foo",
"discourse/plugins/my-plugin/discourse/templates/mobile/bar",
"discourse/plugins/my-plugin/discourse/templates/bar",
"discourse/plugins/my-plugin/discourse/templates/mobile/baz",
"discourse/templates/mobile/baz",
]);
setResolverOption("mobileView", true);
// Default with javascripts/mobile/ added
// Default with plugin template override
lookupTemplate(
assert,
"template:foo",
"javascripts/mobile/foo",
"discourse/plugins/my-plugin/discourse/templates/mobile/foo",
"finding plugin version even if normal one is not present"
);
// Default with javascripts/mobile added, takes precedence over non-mobile
// Default with plugin mobile added, takes precedence over non-mobile
lookupTemplate(
assert,
"template:bar",
"javascripts/mobile/bar",
"discourse/plugins/my-plugin/discourse/templates/mobile/bar",
"preferring plugin mobile version when both non-mobile plugin version is also present"
);
// Default with javascripts/mobile when non-plugin mobile version is present
// Default with when non-plugin mobile version is present
lookupTemplate(
assert,
"template:baz",
"javascripts/mobile/baz",
"discourse/plugins/my-plugin/discourse/templates/mobile/baz",
"preferring plugin mobile version over non-plugin mobile version"
);
});
@ -307,13 +348,13 @@ module("Unit | Ember | resolver", function (hooks) {
test("resolves templates with 'admin' prefix", function (assert) {
setTemplates([
"admin/templates/foo",
"adminBar",
"admin_bar",
"admin.bar",
"discourse/templates/adminBar",
"discourse/templates/admin_bar",
"discourse/templates/admin.bar",
"admin/templates/bar",
"admin/templates/dashboard_general",
"admin-baz-qux",
"javascripts/admin/plugin-template",
"discourse/templates/admin-baz-qux",
"discourse/plugins/my-plugin/discourse/templates/admin/plugin-template",
"admin/templates/components/my-admin-component",
]);
@ -353,7 +394,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:adminBar",
"adminBar",
"discourse/templates/adminBar",
"but not when template with the exact camel cased name exists"
);
@ -361,7 +402,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:admin_bar",
"admin_bar",
"discourse/templates/admin_bar",
"but not when template with the exact underscored name exists"
);
@ -369,7 +410,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:admin.bar",
"admin.bar",
"discourse/templates/admin.bar",
"but not when template with the exact dotted name exists"
);
@ -383,14 +424,14 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:admin-baz/qux",
"admin-baz-qux",
"discourse/templates/admin-baz-qux",
"also tries dasherized"
);
lookupTemplate(
assert,
"template:admin-plugin/template",
"javascripts/admin/plugin-template",
"discourse/plugins/my-plugin/discourse/templates/admin/plugin-template",
"looks up templates in plugins"
);
@ -412,7 +453,7 @@ module("Unit | Ember | resolver", function (hooks) {
test("resolves component templates with 'admin' prefix to 'admin/templates/' namespace", function (assert) {
setTemplates([
"admin/templates/components/foo",
"components/bar",
"discourse/templates/components/bar",
"admin/templates/components/bar",
]);
@ -428,7 +469,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:components/bar",
"components/bar",
"discourse/templates/components/bar",
"uses standard match when both exist"
);
});
@ -437,54 +478,59 @@ module("Unit | Ember | resolver", function (hooks) {
// close to Ember's default behavior.
// See https://guides.emberjs.com/release/routing/loading-and-error-substates/
test("resolves loading templates", function (assert) {
setTemplates(["fooloading", "foo/loading", "foo_loading", "loading"]);
setTemplates([
"discourse/templates/fooloading",
"discourse/templates/foo/loading",
"discourse/templates/foo_loading",
"discourse/templates/loading",
]);
lookupTemplate(
assert,
"template:fooloading",
"fooloading",
"discourse/templates/fooloading",
"exact match without separator"
);
lookupTemplate(
assert,
"template:foo/loading",
"foo/loading",
"discourse/templates/foo/loading",
"exact match with slash"
);
lookupTemplate(
assert,
"template:foo_loading",
"foo_loading",
"discourse/templates/foo_loading",
"exact match underscore"
);
lookupTemplate(
assert,
"template:barloading",
"loading",
"discourse/templates/loading",
"fallback without separator"
);
lookupTemplate(
assert,
"template:bar/loading",
"loading",
"discourse/templates/loading",
"fallback with slash"
);
lookupTemplate(
assert,
"template:bar.loading",
"loading",
"discourse/templates/loading",
"fallback with dot"
);
lookupTemplate(
assert,
"template:bar_loading",
"loading",
"discourse/templates/loading",
"fallback underscore"
);
@ -493,61 +539,66 @@ module("Unit | Ember | resolver", function (hooks) {
test("resolves connector templates", function (assert) {
setTemplates([
"javascripts/foo",
"javascripts/connectors/foo-bar/baz_qux",
"javascripts/connectors/foo-bar/camelCase",
"discourse/plugins/my-plugin/discourse/templates/foo",
"discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/baz_qux",
"discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/camelCase",
]);
lookupTemplate(
assert,
"template:connectors/foo",
"javascripts/foo",
"looks up in javascripts/ namespace"
"discourse/plugins/my-plugin/discourse/templates/foo",
"looks up in plugin namespace"
);
lookupTemplate(
assert,
"template:connectors/components/foo",
"javascripts/foo",
"discourse/plugins/my-plugin/discourse/templates/foo",
"removes components segment"
);
lookupTemplate(
assert,
"template:connectors/foo-bar/baz-qux",
"javascripts/connectors/foo-bar/baz_qux",
"discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/baz_qux",
"underscores last segment"
);
lookupTemplate(
assert,
"template:connectors/foo-bar/camelCase",
"javascripts/connectors/foo-bar/camelCase",
"discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/camelCase",
"handles camelcase file names"
);
lookupTemplate(
assert,
resolver.normalize("template:connectors/foo-bar/camelCase"),
"javascripts/connectors/foo-bar/camelCase",
"discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/camelCase",
"handles camelcase file names when normalized"
);
});
test("returns 'not_found' template when template name cannot be resolved", function (assert) {
setTemplates(["not_found"]);
setTemplates(["discourse/templates/not_found"]);
lookupTemplate(assert, "template:foo/bar/baz", "not_found", "");
lookupTemplate(
assert,
"template:foo/bar/baz",
"discourse/templates/not_found",
""
);
});
test("resolves templates with 'wizard' prefix", function (assert) {
setTemplates([
"wizard/templates/foo",
"wizard_bar",
"wizard.bar",
"discourse/templates/wizard_bar",
"discourse/templates/wizard.bar",
"wizard/templates/bar",
"wizard/templates/dashboard_general",
"wizard-baz-qux",
"discourse/templates/wizard-baz-qux",
"javascripts/wizard/plugin-template",
]);
@ -579,7 +630,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:wizard_bar",
"wizard_bar",
"discourse/templates/wizard_bar",
"but not when template with the exact underscored name exists"
);
@ -587,7 +638,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:wizard.bar",
"wizard.bar",
"discourse/templates/wizard.bar",
"but not when template with the exact dotted name exists"
);
@ -601,7 +652,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:wizard-baz/qux",
"wizard-baz-qux",
"discourse/templates/wizard-baz-qux",
"also tries dasherized"
);
});
@ -609,7 +660,7 @@ module("Unit | Ember | resolver", function (hooks) {
test("resolves component templates with 'wizard' prefix to 'wizard/templates/' namespace", function (assert) {
setTemplates([
"wizard/templates/components/foo",
"components/bar",
"discourse/templates/components/bar",
"wizard/templates/components/bar",
]);
@ -625,7 +676,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate(
assert,
"template:components/bar",
"components/bar",
"discourse/templates/components/bar",
"uses standard match when both exist"
);
});

View File

@ -1,3 +1,4 @@
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _allCategories = null;
let _sectionsById = {};
let _notes = {};
@ -30,7 +31,7 @@ export function allCategories() {
// Find a list of sections based on what templates are available
// eslint-disable-next-line no-undef
Object.keys(Ember.TEMPLATES).forEach((e) => {
DiscourseTemplateMap.keys().forEach((e) => {
let regexp = new RegExp(`styleguide\/(${paths})\/(\\d+)?\\-?([^\\/]+)$`);
let matches = e.match(regexp);
if (matches) {