diff --git a/app/assets/javascripts/discourse/app/initializers/colocated-template-overrides.js b/app/assets/javascripts/discourse/app/initializers/colocated-template-overrides.js new file mode 100644 index 00000000000..0aed290e533 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/colocated-template-overrides.js @@ -0,0 +1,58 @@ +import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map"; +import * as GlimmerManager from "@glimmer/manager"; + +const COLOCATED_TEMPLATE_OVERRIDES = new Map(); + +// This patch is not ideal, but Ember does not allow us to change a component template after initial association +// https://github.com/glimmerjs/glimmer-vm/blob/03a4b55c03/packages/%40glimmer/manager/lib/public/template.ts#L14-L20 +const originalGetTemplate = GlimmerManager.getComponentTemplate; +GlimmerManager.getComponentTemplate = (component) => { + return ( + COLOCATED_TEMPLATE_OVERRIDES.get(component) ?? + originalGetTemplate(component) + ); +}; + +export default { + name: "colocated-template-overrides", + after: "populate-template-map", + + initialize(container) { + this.eachThemePluginTemplate((templateKey, moduleNames) => { + if (!templateKey.startsWith("components/")) { + return; + } + + if (DiscourseTemplateMap.coreTemplates.has(templateKey)) { + // It's a non-colocated core component. Template will be overridden at runtime. + return; + } + + const componentName = templateKey.slice("components/".length); + const component = container.owner.resolveRegistration( + `component:${componentName}` + ); + + if (component && originalGetTemplate(component)) { + const finalOverrideModuleName = moduleNames[moduleNames.length - 1]; + const overrideTemplate = require(finalOverrideModuleName).default; + + COLOCATED_TEMPLATE_OVERRIDES.set(component, overrideTemplate); + } + }); + }, + + eachThemePluginTemplate(cb) { + for (const [key, value] of DiscourseTemplateMap.pluginTemplates) { + cb(key, value); + } + + for (const [key, value] of DiscourseTemplateMap.themeTemplates) { + cb(key, value); + } + }, + + teardown() { + COLOCATED_TEMPLATE_OVERRIDES.clear(); + }, +}; diff --git a/app/assets/javascripts/discourse/tests/acceptance/custom-html-template-test.js b/app/assets/javascripts/discourse/tests/acceptance/custom-html-template-test.js index c6825446a05..d141fd61a0d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/custom-html-template-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/custom-html-template-test.js @@ -2,11 +2,11 @@ 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 { registerTemplateModule } from "discourse/tests/helpers/template-module-helper"; +import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper"; acceptance("CustomHTML template", function (needs) { needs.hooks.beforeEach(() => { - registerTemplateModule( + registerTemporaryModule( "discourse/templates/top", hbs`TOP` ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js index 398c18eda4a..79f31319c4e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js @@ -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 { registerTemplateModule } from "../helpers/template-module-helper"; +import { registerTemporaryModule } from "../helpers/temporary-module-helper"; acceptance("Modal", function (needs) { let _translations; @@ -54,7 +54,7 @@ acceptance("Modal", function (needs) { await triggerKeyEvent("#main-outlet", "keydown", "Escape"); assert.ok(!exists(".d-modal:visible"), "ESC should close the modal"); - registerTemplateModule( + registerTemporaryModule( "discourse/templates/modal/not-dismissable", hbs`{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}` ); @@ -79,7 +79,7 @@ acceptance("Modal", function (needs) { }); test("rawTitle in modal panels", async function (assert) { - registerTemplateModule( + registerTemporaryModule( "discourse/templates/modal/test-raw-title-panels", hbs`` ); @@ -100,8 +100,8 @@ acceptance("Modal", function (needs) { }); test("modal title", async function (assert) { - registerTemplateModule("discourse/templates/modal/test-title", hbs``); - registerTemplateModule( + registerTemporaryModule("discourse/templates/modal/test-title", hbs``); + registerTemporaryModule( "discourse/templates/modal/test-title-with-body", hbs`{{#d-modal-body}}test{{/d-modal-body}}` ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-connector-class-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-connector-class-test.js index 89fb051f266..f5186065f1b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-connector-class-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-connector-class-test.js @@ -9,7 +9,7 @@ import { action } from "@ember/object"; import { extraConnectorClass } from "discourse/lib/plugin-connectors"; import { hbs } from "ember-cli-htmlbars"; import { test } from "qunit"; -import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper"; +import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper"; const PREFIX = "discourse/plugins/some-plugin/templates/connectors"; @@ -49,19 +49,19 @@ acceptance("Plugin Outlet - Connector Class", function (needs) { }, }); - registerTemplateModule( + registerTemporaryModule( `${PREFIX}/user-profile-primary/hello`, hbs`{{model.username}} {{hello}}` ); - registerTemplateModule( + registerTemporaryModule( `${PREFIX}/user-profile-primary/hi`, hbs` {{hi}}` ); - registerTemplateModule( + registerTemporaryModule( `${PREFIX}/user-profile-primary/dont-render`, hbs`I'm not rendered!` ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-decorator-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-decorator-test.js index e269882d8d4..25743748948 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-decorator-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-decorator-test.js @@ -7,7 +7,7 @@ import { hbs } from "ember-cli-htmlbars"; import { test } from "qunit"; import { visit } from "@ember/test-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; -import { registerTemplateModule } from "../helpers/template-module-helper"; +import { registerTemporaryModule } from "../helpers/temporary-module-helper"; const PREFIX = "discourse/plugins/some-plugin/templates/connectors"; @@ -15,11 +15,11 @@ acceptance("Plugin Outlet - Decorator", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - registerTemplateModule( + registerTemporaryModule( `${PREFIX}/discovery-list-container-top/foo`, hbs`FOO` ); - registerTemplateModule( + registerTemporaryModule( `${PREFIX}/discovery-list-container-top/bar`, hbs`BAR` ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-multi-template-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-multi-template-test.js index ba81c83a1f2..a7896517780 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-multi-template-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-multi-template-test.js @@ -6,7 +6,7 @@ import { import { hbs } from "ember-cli-htmlbars"; import { test } from "qunit"; import { visit } from "@ember/test-helpers"; -import { registerTemplateModule } from "../helpers/template-module-helper"; +import { registerTemporaryModule } from "../helpers/temporary-module-helper"; const HELLO = "discourse/plugins/my-plugin/templates/connectors/user-profile-primary/hello"; @@ -15,8 +15,11 @@ const GOODBYE = acceptance("Plugin Outlet - Multi Template", function (needs) { needs.hooks.beforeEach(() => { - registerTemplateModule(HELLO, hbs`Hello`); - registerTemplateModule(GOODBYE, hbs`Goodbye`); + registerTemporaryModule(HELLO, hbs`Hello`); + registerTemporaryModule( + GOODBYE, + hbs`Goodbye` + ); }); test("Renders a template into the outlet", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-single-template-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-single-template-test.js index e1a0cc62c0e..5022ac3fe2c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-single-template-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-single-template-test.js @@ -6,14 +6,14 @@ import { import { hbs } from "ember-cli-htmlbars"; import { test } from "qunit"; import { visit } from "@ember/test-helpers"; -import { registerTemplateModule } from "../helpers/template-module-helper"; +import { registerTemporaryModule } from "../helpers/temporary-module-helper"; const CONNECTOR_MODULE = "discourse/theme-12/templates/connectors/user-profile-primary/hello"; acceptance("Plugin Outlet - Single Template", function (needs) { needs.hooks.beforeEach(() => { - registerTemplateModule( + registerTemporaryModule( CONNECTOR_MODULE, hbs`{{model.username}}` ); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index da71ce3683f..06b4062601b 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -76,7 +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"; +import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper"; export function currentUser() { return User.create(sessionFixtures["/session/current.json"].current_user); @@ -208,7 +208,7 @@ export function testCleanup(container, app) { resetUserMenuTabs(); resetLinkLookup(); resetModelTransformers(); - cleanupTemporaryTemplateRegistrations(); + cleanupTemporaryModuleRegistrations(); } export function discourseModule(name, options) { diff --git a/app/assets/javascripts/discourse/tests/helpers/template-module-helper.js b/app/assets/javascripts/discourse/tests/helpers/temporary-module-helper.js similarity index 77% rename from app/assets/javascripts/discourse/tests/helpers/template-module-helper.js rename to app/assets/javascripts/discourse/tests/helpers/temporary-module-helper.js index ae1930d805f..77301ca8deb 100644 --- a/app/assets/javascripts/discourse/tests/helpers/template-module-helper.js +++ b/app/assets/javascripts/discourse/tests/helpers/temporary-module-helper.js @@ -3,28 +3,28 @@ import { expireConnectorCache } from "discourse/lib/plugin-connectors"; const modifications = []; -function generateTemplateModule(template) { +function generateTemporaryModule(defaultExport) { return function (_exports) { Object.defineProperty(_exports, "__esModule", { value: true, }); - _exports.default = template; + _exports.default = defaultExport; }; } -export function registerTemplateModule(moduleName, template) { +export function registerTemporaryModule(moduleName, defaultExport) { const modificationData = { moduleName, existingModule: requirejs.entries[moduleName], }; delete requirejs.entries[moduleName]; - define(moduleName, ["exports"], generateTemplateModule(template)); + define(moduleName, ["exports"], generateTemporaryModule(defaultExport)); modifications.push(modificationData); expireConnectorCache(); DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries)); } -export function cleanupTemporaryTemplateRegistrations() { +export function cleanupTemporaryModuleRegistrations() { for (const modificationData of modifications.reverse()) { const { moduleName, existingModule } = modificationData; delete requirejs.entries[moduleName]; diff --git a/app/assets/javascripts/discourse/tests/integration/template-override-test.js b/app/assets/javascripts/discourse/tests/integration/template-override-test.js new file mode 100644 index 00000000000..b98d37b3b70 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/template-override-test.js @@ -0,0 +1,172 @@ +import { assert, module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { registerTemporaryModule } from "../helpers/temporary-module-helper"; +import { setComponentTemplate } from "@glimmer/manager"; +import Component from "@glimmer/component"; + +class MockColocatedComponent extends Component {} +setComponentTemplate(hbs`Colocated Original`, MockColocatedComponent); + +class MockResolvedComponent extends Component {} +const MockResolvedComponentTemplate = hbs`Resolved Original`; + +const TestTemplate = hbs` +