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) { export function buildRawConnectorCache(findOutlets) {
let result = {}; let result = {};
findOutlets(__DISCOURSE_RAW_TEMPLATES, (outletName, resource) => { findOutlets(
result[outletName] = result[outletName] || []; Object.keys(__DISCOURSE_RAW_TEMPLATES),
(outletName, resource) => {
result[outletName] ??= [];
result[outletName].push({ result[outletName].push({
template: __DISCOURSE_RAW_TEMPLATES[resource], template: __DISCOURSE_RAW_TEMPLATES[resource],
}); });
}); }
);
return result; 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 { dasherize, decamelize } from "@ember/string";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import { findHelper } from "discourse-common/lib/helpers"; import { findHelper } from "discourse-common/lib/helpers";
import SuffixTrie from "discourse-common/lib/suffix-trie"; import SuffixTrie from "discourse-common/lib/suffix-trie";
import Resolver from "ember-resolver"; import Resolver from "ember-resolver";
import { buildResolver as buildLegacyResolver } from "discourse-common/lib/legacy-resolver"; import { buildResolver as buildLegacyResolver } from "discourse-common/lib/legacy-resolver";
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _options = {}; let _options = {};
let moduleSuffixTrie = null; let moduleSuffixTrie = null;
@ -287,21 +287,19 @@ export function buildResolver(baseName) {
resolveTemplate(parsedName) { resolveTemplate(parsedName) {
return ( return (
this.findPluginMobileTemplate(parsedName) ||
this.findPluginTemplate(parsedName) ||
this.findMobileTemplate(parsedName) || this.findMobileTemplate(parsedName) ||
this.findTemplate(parsedName) || this.findTemplate(parsedName) ||
this.findAdminTemplate(parsedName) || this.findAdminTemplate(parsedName) ||
this.findWizardTemplate(parsedName) || this.findWizardTemplate(parsedName) ||
this.findLoadingTemplate(parsedName) || this.findLoadingTemplate(parsedName) ||
this.findConnectorTemplate(parsedName) || this.findConnectorTemplate(parsedName) ||
Ember.TEMPLATES.not_found this.discourseTemplateModule("not_found")
); );
} }
findLoadingTemplate(parsedName) { findLoadingTemplate(parsedName) {
if (parsedName.fullNameWithoutType.match(/loading$/)) { 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("template:connectors/", "template:")
.replace("components/", "") .replace("components/", "")
); );
return this.findTemplate(connectorParsedName, "javascripts/"); return this.findTemplate(connectorParsedName);
}
}
findPluginTemplate(parsedName) {
return this.findTemplate(parsedName, "javascripts/");
}
findPluginMobileTemplate(parsedName) {
if (_options.mobileView) {
return this.findTemplate(parsedName, "javascripts/mobile/");
} }
} }
@ -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) { findTemplate(parsedName, prefix) {
prefix = prefix || ""; prefix = prefix || "";
const withoutType = parsedName.fullNameWithoutType, const withoutType = parsedName.fullNameWithoutType,
underscored = decamelize(withoutType).replace(/-/g, "_"), underscored = decamelize(withoutType).replace(/-/g, "_"),
segments = withoutType.split("/"), segments = withoutType.split("/");
templates = Ember.TEMPLATES;
return ( return (
// Convert dots and dashes to slashes // Convert dots and dashes to slashes
templates[prefix + withoutType.replace(/[\.-]/g, "/")] || this.discourseTemplateModule(
prefix + withoutType.replace(/[\.-]/g, "/")
) ||
// Default unmodified behavior of original resolveTemplate. // Default unmodified behavior of original resolveTemplate.
templates[prefix + withoutType] || this.discourseTemplateModule(prefix + withoutType) ||
// Underscored without namespace // Underscored without namespace
templates[prefix + underscored] || this.discourseTemplateModule(prefix + underscored) ||
// Underscored with first segment as directory // Underscored with first segment as directory
templates[prefix + underscored.replace("_", "/")] || this.discourseTemplateModule(prefix + underscored.replace("_", "/")) ||
// Underscore only the last segment // Underscore only the last segment
templates[ this.discourseTemplateModule(
`${prefix}${segments.slice(0, -1).join("/")}/${segments[ `${prefix}${segments.slice(0, -1).join("/")}/${segments[
segments.length - 1 segments.length - 1
].replace(/-/g, "_")}` ].replace(/-/g, "_")}`
] || ) ||
// All dasherized // 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) // (similar to how discourse lays out templates)
findAdminTemplate(parsedName) { findAdminTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "admin") { if (parsedName.fullNameWithoutType === "admin") {
return Ember.TEMPLATES["admin/templates/admin"]; return this.discourseTemplateModule("admin/templates/admin");
} }
let namespaced, match; let namespaced, match;
if (parsedName.fullNameWithoutType.startsWith("components/")) { if (parsedName.fullNameWithoutType.startsWith("components/")) {
return ( return (
// Built-in
this.findTemplate(parsedName, "admin/templates/") || this.findTemplate(parsedName, "admin/templates/") ||
// Plugin this.findTemplate(parsedName, "admin/") // Nested under discourse/templates/admin (e.g. from plugins)
this.findTemplate(parsedName, "javascripts/admin/")
); );
} else if (/^admin[_\.-]/.test(parsedName.fullNameWithoutType)) { } else if (/^admin[_\.-]/.test(parsedName.fullNameWithoutType)) {
namespaced = parsedName.fullNameWithoutType.slice(6); namespaced = parsedName.fullNameWithoutType.slice(6);
@ -389,11 +387,9 @@ export function buildResolver(baseName) {
if (namespaced) { if (namespaced) {
let adminParsedName = this.parseName(`template:${namespaced}`); let adminParsedName = this.parseName(`template:${namespaced}`);
resolved = resolved =
// Built-in
this.findTemplate(adminParsedName, "admin/templates/") || this.findTemplate(adminParsedName, "admin/templates/") ||
this.findTemplate(parsedName, "admin/templates/") || this.findTemplate(parsedName, "admin/templates/") ||
// Plugin this.findTemplate(adminParsedName, "admin/"); // Nested under discourse/templates/admin (e.g. from plugin)
this.findTemplate(adminParsedName, "javascripts/admin/");
} }
return resolved; return resolved;
@ -401,7 +397,7 @@ export function buildResolver(baseName) {
findWizardTemplate(parsedName) { findWizardTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "wizard") { if (parsedName.fullNameWithoutType === "wizard") {
return Ember.TEMPLATES["wizard/templates/wizard"]; return this.discourseTemplateModule("wizard/templates/wizard");
} }
let namespaced; let namespaced;
@ -415,10 +411,10 @@ export function buildResolver(baseName) {
} }
if (namespaced) { if (namespaced) {
let adminParsedName = this.parseName( let wizardParsedName = this.parseName(
`template:wizard/templates/${namespaced}` `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 { action, set } from "@ember/object";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import Ember from "ember"; import { getOwner } from "discourse-common/lib/get-owner";
let _components = {}; let _components = {};
@ -106,12 +106,11 @@ export default Component.extend({
return _components[type]; return _components[type];
} }
let dasherized = dasherize(type); const dasherized = dasherize(type);
let templatePath = `components/${dasherized}`; const componentExists = getOwner(this).hasRegistration(
let template = `component:${dasherized}`
Ember.TEMPLATES[`${templatePath}`] || );
Ember.TEMPLATES[`javascripts/${templatePath}`]; _components[type] = componentExists ? dasherized : null;
_components[type] = template ? dasherized : null;
return _components[type]; 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 { buildRawConnectorCache } from "discourse-common/lib/raw-templates";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import Ember from "ember"; import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _connectorCache; let _connectorCache;
let _rawConnectorCache; let _rawConnectorCache;
@ -25,11 +25,11 @@ const DefaultConnectorClass = {
teardownComponent() {}, teardownComponent() {},
}; };
function findOutlets(collection, callback) { function findOutlets(keys, callback) {
Object.keys(collection).forEach(function (res) { keys.forEach(function (res) {
if (res.includes("/connectors/")) {
const segments = res.split("/"); const segments = res.split("/");
let outletName = segments[segments.length - 2]; if (segments.includes("connectors")) {
const outletName = segments[segments.length - 2];
const uniqueName = segments[segments.length - 1]; const uniqueName = segments[segments.length - 1];
callback(outletName, res, uniqueName); callback(outletName, res, uniqueName);
@ -45,7 +45,7 @@ export function clearCache() {
function findClass(outletName, uniqueName) { function findClass(outletName, uniqueName) {
if (!_classPaths) { if (!_classPaths) {
_classPaths = {}; _classPaths = {};
findOutlets(require._eak_seen, (outlet, res, un) => { findOutlets(Object.keys(require._eak_seen), (outlet, res, un) => {
const possibleConnectorClass = requirejs(res).default; const possibleConnectorClass = requirejs(res).default;
if (possibleConnectorClass.__id) { if (possibleConnectorClass.__id) {
// This is the template, not the connector class // This is the template, not the connector class
@ -63,20 +63,31 @@ function findClass(outletName, uniqueName) {
: DefaultConnectorClass; : DefaultConnectorClass;
} }
/**
* Clear the cache of connectors. Should only be used in tests when
* `requirejs.entries` is changed.
*/
export function expireConnectorCache() {
_connectorCache = null;
}
function buildConnectorCache() { function buildConnectorCache() {
_connectorCache = {}; _connectorCache = {};
findOutlets(Ember.TEMPLATES, (outletName, resource, uniqueName) => { findOutlets(
DiscourseTemplateMap.keys(),
(outletName, resource, uniqueName) => {
_connectorCache[outletName] = _connectorCache[outletName] || []; _connectorCache[outletName] = _connectorCache[outletName] || [];
_connectorCache[outletName].push({ _connectorCache[outletName].push({
outletName, outletName,
templateName: resource.replace("javascripts/", ""), templateName: resource,
template: Ember.TEMPLATES[resource], template: require(DiscourseTemplateMap.resolve(resource)).default,
classNames: `${outletName}-outlet ${uniqueName}`, classNames: `${outletName}-outlet ${uniqueName}`,
connectorClass: findClass(outletName, uniqueName), connectorClass: findClass(outletName, uniqueName),
}); });
}); }
);
} }
export function connectorsFor(outletName) { export function connectorsFor(outletName) {

View File

@ -3,50 +3,6 @@
throw "Unsupported browser detected"; 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; window.__widget_helpers = require("discourse-widget-hbs/helpers").default;
// TODO: Eliminate this global // 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 { hbs } from "ember-cli-htmlbars";
import { test } from "qunit"; import { test } from "qunit";
import { visit } from "@ember/test-helpers"; import { visit } from "@ember/test-helpers";
import Ember from "ember"; import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
acceptance("CustomHTML template", function (needs) { acceptance("CustomHTML template", function (needs) {
needs.hooks.beforeEach(() => { needs.hooks.beforeEach(() => {
Ember.TEMPLATES["top"] = hbs`<span class='top-span'>TOP</span>`; registerTemplateModule(
}); "discourse/templates/top",
hbs`<span class='top-span'>TOP</span>`
needs.hooks.afterEach(() => { );
delete Ember.TEMPLATES["top"];
}); });
test("renders custom template", async function (assert) { test("renders custom template", async function (assert) {

View File

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

View File

@ -9,9 +9,9 @@ import { action } from "@ember/object";
import { extraConnectorClass } from "discourse/lib/plugin-connectors"; import { extraConnectorClass } from "discourse/lib/plugin-connectors";
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit"; 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) { acceptance("Plugin Outlet - Connector Class", function (needs) {
needs.hooks.beforeEach(() => { needs.hooks.beforeEach(() => {
@ -49,25 +49,22 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
}, },
}); });
Ember.TEMPLATES[ registerTemplateModule(
`${PREFIX}/user-profile-primary/hello` `${PREFIX}/user-profile-primary/hello`,
] = hbs`<span class='hello-username'>{{model.username}}</span> hbs`<span class='hello-username'>{{model.username}}</span>
<button class='say-hello' {{on "click" (action "sayHello")}}></button> <button class='say-hello' {{on "click" (action "sayHello")}}></button>
<button class='say-hello-using-this' {{on "click" this.sayHello}}></button> <button class='say-hello-using-this' {{on "click" this.sayHello}}></button>
<span class='hello-result'>{{hello}}</span>`; <span class='hello-result'>{{hello}}</span>`
Ember.TEMPLATES[ );
`${PREFIX}/user-profile-primary/hi` registerTemplateModule(
] = hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button> `${PREFIX}/user-profile-primary/hi`,
<span class='hi-result'>{{hi}}</span>`; hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button>
Ember.TEMPLATES[ <span class='hi-result'>{{hi}}</span>`
`${PREFIX}/user-profile-primary/dont-render` );
] = hbs`I'm not rendered!`; registerTemplateModule(
}); `${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`];
}); });
test("Renders a template into the outlet", async function (assert) { 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 { test } from "qunit";
import { visit } from "@ember/test-helpers"; import { visit } from "@ember/test-helpers";
import { withPluginApi } from "discourse/lib/plugin-api"; 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) { acceptance("Plugin Outlet - Decorator", function (needs) {
needs.user(); needs.user();
needs.hooks.beforeEach(() => { needs.hooks.beforeEach(() => {
Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/foo`] = hbs`FOO`; registerTemplateModule(
Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/bar`] = hbs`BAR`; `${PREFIX}/discovery-list-container-top/foo`,
hbs`FOO`
);
registerTemplateModule(
`${PREFIX}/discovery-list-container-top/bar`,
hbs`BAR`
);
withPluginApi("0.8.38", (api) => { withPluginApi("0.8.38", (api) => {
api.decoratePluginOutlet( 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) { test("Calls the plugin callback with the rendered outlet", async function (assert) {
await visit("/"); await visit("/");

View File

@ -6,21 +6,17 @@ import {
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit"; import { test } from "qunit";
import { visit } from "@ember/test-helpers"; 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 = 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) { acceptance("Plugin Outlet - Multi Template", function (needs) {
needs.hooks.beforeEach(() => { needs.hooks.beforeEach(() => {
Ember.TEMPLATES[HELLO] = hbs`<span class='hello-span'>Hello</span>`; registerTemplateModule(HELLO, hbs`<span class='hello-span'>Hello</span>`);
Ember.TEMPLATES[GOODBYE] = hbs`<span class='bye-span'>Goodbye</span>`; registerTemplateModule(GOODBYE, hbs`<span class='bye-span'>Goodbye</span>`);
});
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[HELLO];
delete Ember.TEMPLATES[GOODBYE];
}); });
test("Renders a template into the outlet", async function (assert) { test("Renders a template into the outlet", async function (assert) {

View File

@ -6,20 +6,17 @@ import {
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit"; import { test } from "qunit";
import { visit } from "@ember/test-helpers"; import { visit } from "@ember/test-helpers";
import Ember from "ember"; import { registerTemplateModule } from "../helpers/template-module-helper";
const CONNECTOR = const CONNECTOR_MODULE =
"javascripts/single-test/connectors/user-profile-primary/hello"; "discourse/theme-12/templates/connectors/user-profile-primary/hello";
acceptance("Plugin Outlet - Single Template", function (needs) { acceptance("Plugin Outlet - Single Template", function (needs) {
needs.hooks.beforeEach(() => { needs.hooks.beforeEach(() => {
Ember.TEMPLATES[ registerTemplateModule(
CONNECTOR CONNECTOR_MODULE,
] = hbs`<span class='hello-username'>{{model.username}}</span>`; hbs`<span class='hello-username'>{{model.username}}</span>`
}); );
needs.hooks.afterEach(() => {
delete Ember.TEMPLATES[CONNECTOR];
}); });
test("Renders a template into the outlet", async function (assert) { 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 { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
import { reset as resetLinkLookup } from "discourse/lib/link-lookup"; import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
import { resetModelTransformers } from "discourse/lib/model-transformers"; import { resetModelTransformers } from "discourse/lib/model-transformers";
import { cleanupTemporaryTemplateRegistrations } from "./template-module-helper";
export function currentUser() { export function currentUser() {
return User.create(sessionFixtures["/session/current.json"].current_user); return User.create(sessionFixtures["/session/current.json"].current_user);
@ -207,6 +208,7 @@ export function testCleanup(container, app) {
resetUserMenuTabs(); resetUserMenuTabs();
resetLinkLookup(); resetLinkLookup();
resetModelTransformers(); resetModelTransformers();
cleanupTemporaryTemplateRegistrations();
} }
export function discourseModule(name, options) { 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 { buildResolver, setResolverOption } from "discourse-common/resolver";
import { module, test } from "qunit"; 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; let resolver;
function lookupTemplate(assert, name, expectedTemplate, message) { function lookupTemplate(assert, name, expectedTemplate, message) {
@ -11,57 +11,71 @@ function lookupTemplate(assert, name, expectedTemplate, message) {
assert.strictEqual(result, expectedTemplate, message); assert.strictEqual(result, expectedTemplate, message);
} }
function setTemplates(lookupTemplateStrings) { function setTemplates(templateModuleNames) {
lookupTemplateStrings.forEach(function (lookupTemplateString) { for (const name of templateModuleNames) {
Ember.TEMPLATES[lookupTemplateString] = lookupTemplateString; registerTemplateModule(name, name);
}); }
} }
const DiscourseResolver = buildResolver("discourse"); const DiscourseResolver = buildResolver("discourse");
module("Unit | Ember | resolver", function (hooks) { module("Unit | Ember | resolver", function (hooks) {
hooks.beforeEach(function () { hooks.beforeEach(function () {
originalTemplates = Ember.TEMPLATES; DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
Ember.TEMPLATES = {};
resolver = DiscourseResolver.create({ resolver = DiscourseResolver.create({
namespace: { modulePrefix: "discourse" }, namespace: { modulePrefix: "discourse" },
}); });
}); });
hooks.afterEach(function () {
Ember.TEMPLATES = originalTemplates;
});
test("finds templates in top level dir", function (assert) { 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 // Default unmodified behavior
lookupTemplate(assert, "template:foobar", "foobar", "by lowcased name"); lookupTemplate(
assert,
"template:foobar",
"discourse/templates/foobar",
"by lowcased name"
);
// Default unmodified behavior // 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 // Default unmodified behavior
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo_bar", "template:foo_bar",
"foo_bar", "discourse/templates/foo_bar",
"by underscored name" "by underscored name"
); );
// Default unmodified behavior // 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) { test("finds templates in first-level subdir", function (assert) {
setTemplates(["foo/bar_baz"]); setTemplates(["discourse/templates/foo/bar_baz"]);
// Default unmodified behavior // Default unmodified behavior
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo/bar_baz", "template:foo/bar_baz",
"foo/bar_baz", "discourse/templates/foo/bar_baz",
"with subdir defined by slash" "with subdir defined by slash"
); );
@ -69,7 +83,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo.bar_baz", "template:foo.bar_baz",
"foo/bar_baz", "discourse/templates/foo/bar_baz",
"with subdir defined by dot" "with subdir defined by dot"
); );
@ -77,7 +91,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo-bar_baz", "template:foo-bar_baz",
"foo/bar_baz", "discourse/templates/foo/bar_baz",
"with subdir defined by dash" "with subdir defined by dash"
); );
@ -85,7 +99,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:fooBarBaz", "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" "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( lookupTemplate(
assert, assert,
"template:foo_bar_baz", "template:foo_bar_baz",
"foo/bar_baz", "discourse/templates/foo/bar_baz",
"with subdir defined by first underscore" "with subdir defined by first underscore"
); );
}); });
test("resolves precedence between overlapping top level dir and first level subdir templates", function (assert) { 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 // Directories are prioritized when dotted
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo.bar", "template:foo.bar",
"foo/bar", "discourse/templates/foo/bar",
"preferring first level subdir for dotted name" "preferring first level subdir for dotted name"
); );
@ -113,7 +133,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo-bar", "template:foo-bar",
"foo/bar", "discourse/templates/foo/bar",
"preferring first level subdir for dotted name" "preferring first level subdir for dotted name"
); );
@ -121,7 +141,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:fooBar", "template:fooBar",
"fooBar", "discourse/templates/fooBar",
"preferring top level dir for camel cased name" "preferring top level dir for camel cased name"
); );
@ -129,7 +149,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo_bar", "template:foo_bar",
"foo_bar", "discourse/templates/foo_bar",
"preferring top level dir for underscored name" "preferring top level dir for underscored name"
); );
@ -137,19 +157,19 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:baz-qux", "template:baz-qux",
"baz/qux", "discourse/templates/baz/qux",
"fallback subdir for dashed name" "fallback subdir for dashed name"
); );
}); });
test("finds templates in subdir deeper than one level", function (assert) { 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 // Default unmodified
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo/bar/baz/qux", "template:foo/bar/baz/qux",
"foo/bar/baz/qux", "discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes" "for subdirs defined by slashes"
); );
@ -157,7 +177,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo.bar.baz.qux", "template:foo.bar.baz.qux",
"foo/bar/baz/qux", "discourse/templates/foo/bar/baz/qux",
"for subdirs defined by dots" "for subdirs defined by dots"
); );
@ -165,7 +185,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo/bar/bazQux", "template:foo/bar/bazQux",
"foo/bar/baz/qux", "discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes plus one camel case" "for subdirs defined by slashes plus one camel case"
); );
@ -173,7 +193,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo/bar/baz_qux", "template:foo/bar/baz_qux",
"foo/bar/baz/qux", "discourse/templates/foo/bar/baz/qux",
"for subdirs defined by slashes plus one underscore" "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) { 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); setResolverOption("mobileView", true);
@ -219,7 +244,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo", "template:foo",
"mobile/foo", "discourse/templates/mobile/foo",
"finding mobile version even if normal one is not present" "finding mobile version even if normal one is not present"
); );
@ -227,7 +252,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:bar", "template:bar",
"mobile/bar", "discourse/templates/mobile/bar",
"preferring mobile version when both mobile and normal versions are present" "preferring mobile version when both mobile and normal versions are present"
); );
@ -235,71 +260,87 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:baz", "template:baz",
"baz", "discourse/templates/baz",
"falling back to a normal version when mobile version is not present" "falling back to a normal version when mobile version is not present"
); );
}); });
test("resolves plugin templates to 'javascripts/' namespace", function (assert) { test("resolves templates to plugin and theme namespaces", function (assert) {
setTemplates(["javascripts/foo", "bar", "javascripts/bar", "baz"]); 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( lookupTemplate(
assert, assert,
"template:foo", "template:foo",
"javascripts/foo", "discourse/plugins/my-plugin/discourse/templates/foo",
"finding plugin version even if normal one is not present" "finding plugin version even if normal one is not present"
); );
// Default with javascripts/ added, takes precedence // Defined in core and plugin
lookupTemplate( lookupTemplate(
assert, assert,
"template:bar", "template:bar",
"javascripts/bar", "discourse/plugins/my-plugin/discourse/templates/bar",
"preferring plugin version when both versions are present" "prefers plugin version over core"
); );
// Default when javascripts version not present // Defined in core and plugin and theme
lookupTemplate( lookupTemplate(
assert, assert,
"template:baz", "template:baz",
"baz", "discourse/theme-12/discourse/templates/baz",
"falling back to a normal version when plugin version is not present" "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([ setTemplates([
"javascripts/mobile/foo", "discourse/plugins/my-plugin/discourse/templates/mobile/foo",
"javascripts/mobile/bar", "discourse/plugins/my-plugin/discourse/templates/mobile/bar",
"javascripts/bar", "discourse/plugins/my-plugin/discourse/templates/bar",
"javascripts/mobile/baz", "discourse/plugins/my-plugin/discourse/templates/mobile/baz",
"mobile/baz", "discourse/templates/mobile/baz",
]); ]);
setResolverOption("mobileView", true); setResolverOption("mobileView", true);
// Default with javascripts/mobile/ added // Default with plugin template override
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo", "template:foo",
"javascripts/mobile/foo", "discourse/plugins/my-plugin/discourse/templates/mobile/foo",
"finding plugin version even if normal one is not present" "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( lookupTemplate(
assert, assert,
"template:bar", "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" "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( lookupTemplate(
assert, assert,
"template:baz", "template:baz",
"javascripts/mobile/baz", "discourse/plugins/my-plugin/discourse/templates/mobile/baz",
"preferring plugin mobile version over non-plugin mobile version" "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) { test("resolves templates with 'admin' prefix", function (assert) {
setTemplates([ setTemplates([
"admin/templates/foo", "admin/templates/foo",
"adminBar", "discourse/templates/adminBar",
"admin_bar", "discourse/templates/admin_bar",
"admin.bar", "discourse/templates/admin.bar",
"admin/templates/bar", "admin/templates/bar",
"admin/templates/dashboard_general", "admin/templates/dashboard_general",
"admin-baz-qux", "discourse/templates/admin-baz-qux",
"javascripts/admin/plugin-template", "discourse/plugins/my-plugin/discourse/templates/admin/plugin-template",
"admin/templates/components/my-admin-component", "admin/templates/components/my-admin-component",
]); ]);
@ -353,7 +394,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:adminBar", "template:adminBar",
"adminBar", "discourse/templates/adminBar",
"but not when template with the exact camel cased name exists" "but not when template with the exact camel cased name exists"
); );
@ -361,7 +402,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:admin_bar", "template:admin_bar",
"admin_bar", "discourse/templates/admin_bar",
"but not when template with the exact underscored name exists" "but not when template with the exact underscored name exists"
); );
@ -369,7 +410,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:admin.bar", "template:admin.bar",
"admin.bar", "discourse/templates/admin.bar",
"but not when template with the exact dotted name exists" "but not when template with the exact dotted name exists"
); );
@ -383,14 +424,14 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:admin-baz/qux", "template:admin-baz/qux",
"admin-baz-qux", "discourse/templates/admin-baz-qux",
"also tries dasherized" "also tries dasherized"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:admin-plugin/template", "template:admin-plugin/template",
"javascripts/admin/plugin-template", "discourse/plugins/my-plugin/discourse/templates/admin/plugin-template",
"looks up templates in plugins" "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) { test("resolves component templates with 'admin' prefix to 'admin/templates/' namespace", function (assert) {
setTemplates([ setTemplates([
"admin/templates/components/foo", "admin/templates/components/foo",
"components/bar", "discourse/templates/components/bar",
"admin/templates/components/bar", "admin/templates/components/bar",
]); ]);
@ -428,7 +469,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:components/bar", "template:components/bar",
"components/bar", "discourse/templates/components/bar",
"uses standard match when both exist" "uses standard match when both exist"
); );
}); });
@ -437,54 +478,59 @@ module("Unit | Ember | resolver", function (hooks) {
// close to Ember's default behavior. // close to Ember's default behavior.
// See https://guides.emberjs.com/release/routing/loading-and-error-substates/ // See https://guides.emberjs.com/release/routing/loading-and-error-substates/
test("resolves loading templates", function (assert) { 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( lookupTemplate(
assert, assert,
"template:fooloading", "template:fooloading",
"fooloading", "discourse/templates/fooloading",
"exact match without separator" "exact match without separator"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo/loading", "template:foo/loading",
"foo/loading", "discourse/templates/foo/loading",
"exact match with slash" "exact match with slash"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:foo_loading", "template:foo_loading",
"foo_loading", "discourse/templates/foo_loading",
"exact match underscore" "exact match underscore"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:barloading", "template:barloading",
"loading", "discourse/templates/loading",
"fallback without separator" "fallback without separator"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:bar/loading", "template:bar/loading",
"loading", "discourse/templates/loading",
"fallback with slash" "fallback with slash"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:bar.loading", "template:bar.loading",
"loading", "discourse/templates/loading",
"fallback with dot" "fallback with dot"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:bar_loading", "template:bar_loading",
"loading", "discourse/templates/loading",
"fallback underscore" "fallback underscore"
); );
@ -493,61 +539,66 @@ module("Unit | Ember | resolver", function (hooks) {
test("resolves connector templates", function (assert) { test("resolves connector templates", function (assert) {
setTemplates([ setTemplates([
"javascripts/foo", "discourse/plugins/my-plugin/discourse/templates/foo",
"javascripts/connectors/foo-bar/baz_qux", "discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/baz_qux",
"javascripts/connectors/foo-bar/camelCase", "discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/camelCase",
]); ]);
lookupTemplate( lookupTemplate(
assert, assert,
"template:connectors/foo", "template:connectors/foo",
"javascripts/foo", "discourse/plugins/my-plugin/discourse/templates/foo",
"looks up in javascripts/ namespace" "looks up in plugin namespace"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:connectors/components/foo", "template:connectors/components/foo",
"javascripts/foo", "discourse/plugins/my-plugin/discourse/templates/foo",
"removes components segment" "removes components segment"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:connectors/foo-bar/baz-qux", "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" "underscores last segment"
); );
lookupTemplate( lookupTemplate(
assert, assert,
"template:connectors/foo-bar/camelCase", "template:connectors/foo-bar/camelCase",
"javascripts/connectors/foo-bar/camelCase", "discourse/plugins/my-plugin/discourse/templates/connectors/foo-bar/camelCase",
"handles camelcase file names" "handles camelcase file names"
); );
lookupTemplate( lookupTemplate(
assert, assert,
resolver.normalize("template:connectors/foo-bar/camelCase"), 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" "handles camelcase file names when normalized"
); );
}); });
test("returns 'not_found' template when template name cannot be resolved", function (assert) { 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) { test("resolves templates with 'wizard' prefix", function (assert) {
setTemplates([ setTemplates([
"wizard/templates/foo", "wizard/templates/foo",
"wizard_bar", "discourse/templates/wizard_bar",
"wizard.bar", "discourse/templates/wizard.bar",
"wizard/templates/bar", "wizard/templates/bar",
"wizard/templates/dashboard_general", "wizard/templates/dashboard_general",
"wizard-baz-qux", "discourse/templates/wizard-baz-qux",
"javascripts/wizard/plugin-template", "javascripts/wizard/plugin-template",
]); ]);
@ -579,7 +630,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:wizard_bar", "template:wizard_bar",
"wizard_bar", "discourse/templates/wizard_bar",
"but not when template with the exact underscored name exists" "but not when template with the exact underscored name exists"
); );
@ -587,7 +638,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:wizard.bar", "template:wizard.bar",
"wizard.bar", "discourse/templates/wizard.bar",
"but not when template with the exact dotted name exists" "but not when template with the exact dotted name exists"
); );
@ -601,7 +652,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:wizard-baz/qux", "template:wizard-baz/qux",
"wizard-baz-qux", "discourse/templates/wizard-baz-qux",
"also tries dasherized" "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) { test("resolves component templates with 'wizard' prefix to 'wizard/templates/' namespace", function (assert) {
setTemplates([ setTemplates([
"wizard/templates/components/foo", "wizard/templates/components/foo",
"components/bar", "discourse/templates/components/bar",
"wizard/templates/components/bar", "wizard/templates/components/bar",
]); ]);
@ -625,7 +676,7 @@ module("Unit | Ember | resolver", function (hooks) {
lookupTemplate( lookupTemplate(
assert, assert,
"template:components/bar", "template:components/bar",
"components/bar", "discourse/templates/components/bar",
"uses standard match when both exist" "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 _allCategories = null;
let _sectionsById = {}; let _sectionsById = {};
let _notes = {}; let _notes = {};
@ -30,7 +31,7 @@ export function allCategories() {
// Find a list of sections based on what templates are available // Find a list of sections based on what templates are available
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
Object.keys(Ember.TEMPLATES).forEach((e) => { DiscourseTemplateMap.keys().forEach((e) => {
let regexp = new RegExp(`styleguide\/(${paths})\/(\\d+)?\\-?([^\\/]+)$`); let regexp = new RegExp(`styleguide\/(${paths})\/(\\d+)?\\-?([^\\/]+)$`);
let matches = e.match(regexp); let matches = e.match(regexp);
if (matches) { if (matches) {