diff --git a/app/assets/javascripts/discourse/app/instance-initializers/component-templates.js b/app/assets/javascripts/discourse/app/instance-initializers/component-templates.js index 776846c925c..5e1d9505e84 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/component-templates.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/component-templates.js @@ -1,9 +1,17 @@ import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map"; import * as GlimmerManager from "@glimmer/manager"; import ClassicComponent from "@ember/component"; +import { isTesting } from "discourse-common/config/environment"; const COLOCATED_TEMPLATE_OVERRIDES = new Map(); +let THROW_GJS_ERROR = isTesting(); + +/** For use in tests/integration/component-templates-test only */ +export function overrideThrowGjsError(value) { + THROW_GJS_ERROR = value; +} + // 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; @@ -34,15 +42,29 @@ export default { const component = 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); - } else if (!component) { + if (!component) { // Plugin/theme component template with no backing class. // Treat as classic component to emulate pre-template-only-glimmer-component behaviour. owner.register(`component:${componentName}`, ClassicComponent); + return; + } + + const originalTemplate = originalGetTemplate(component); + const isStrictMode = originalTemplate?.()?.parsedLayout?.isStrictMode; + const finalOverrideModuleName = moduleNames[moduleNames.length - 1]; + + if (isStrictMode) { + const message = `[${finalOverrideModuleName}] ${componentName} was authored using gjs and its template cannot be overridden. Ignoring override.`; + if (THROW_GJS_ERROR) { + throw new Error(message); + } else { + // eslint-disable-next-line no-console + console.error(message); + } + } else if (originalTemplate) { + const overrideTemplate = require(finalOverrideModuleName).default; + + COLOCATED_TEMPLATE_OVERRIDES.set(component, overrideTemplate); } }); }, diff --git a/app/assets/javascripts/discourse/tests/integration/component-templates-test.js b/app/assets/javascripts/discourse/tests/integration/component-templates-test.gjs similarity index 87% rename from app/assets/javascripts/discourse/tests/integration/component-templates-test.js rename to app/assets/javascripts/discourse/tests/integration/component-templates-test.gjs index 2aaef532776..330980e13a9 100644 --- a/app/assets/javascripts/discourse/tests/integration/component-templates-test.js +++ b/app/assets/javascripts/discourse/tests/integration/component-templates-test.gjs @@ -6,6 +6,8 @@ import { registerTemporaryModule } from "../helpers/temporary-module-helper"; import { setComponentTemplate } from "@glimmer/manager"; import Component from "@glimmer/component"; import { forceMobile, resetMobile } from "discourse/lib/mobile"; +import sinon from "sinon"; +import { overrideThrowGjsError } from "discourse/instance-initializers/component-templates"; class MockColocatedComponent extends Component {} setComponentTemplate(hbs`Colocated Original`, MockColocatedComponent); @@ -266,4 +268,45 @@ module("Integration | Initializers | plugin-component-templates", function () { .hasText("Resolved Theme Override", "resolved component correct"); }); }); + + module("overriding gjs component", function (hooks) { + let errorStub; + + hooks.beforeEach(() => { + registerTemporaryModule( + `discourse/components/mock-gjs-component`, + class MyComponent extends Component { + <template> + <span class="greeting">Hello world</span> + </template> + } + ); + + registerTemporaryModule( + `discourse/plugins/my-plugin/discourse/templates/components/mock-gjs-component`, + hbs`doomed override` + ); + + errorStub = sinon + .stub(console, "error") + .withArgs(sinon.match(/mock-gjs-component was authored using gjs/)); + + overrideThrowGjsError(false); + }); + + hooks.afterEach(() => { + overrideThrowGjsError(true); + }); + + setupRenderingTest(hooks); + + test("theme overrides plugin component", async function () { + await render(hbs`<MockGjsComponent />`); + assert + .dom(".greeting") + .hasText("Hello world", "renders original implementation"); + + sinon.assert.calledOnce(errorStub); + }); + }); });