diff --git a/lib/rules/template-no-at-ember-render-modifiers.js b/lib/rules/template-no-at-ember-render-modifiers.js
index 5bcd2570e1..fa93b2c776 100644
--- a/lib/rules/template-no-at-ember-render-modifiers.js
+++ b/lib/rules/template-no-at-ember-render-modifiers.js
@@ -1,3 +1,20 @@
+// Sub-path → canonical kebab-case modifier name (default import).
+const RENDER_MODIFIER_IMPORT_PATHS = {
+ '@ember/render-modifiers/modifiers/did-insert': 'did-insert',
+ '@ember/render-modifiers/modifiers/did-update': 'did-update',
+ '@ember/render-modifiers/modifiers/will-destroy': 'will-destroy',
+};
+
+// Named exports of the root package `@ember/render-modifiers` → canonical kebab name.
+const ROOT_NAMED_EXPORTS = {
+ didInsert: 'did-insert',
+ didUpdate: 'did-update',
+ willDestroy: 'will-destroy',
+};
+
+const KEBAB_NAMES = new Set(['did-insert', 'did-update', 'will-destroy']);
+const ROOT_PACKAGE = '@ember/render-modifiers';
+
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
@@ -23,24 +40,72 @@ module.exports = {
},
create(context) {
+ const filename = context.filename;
+ const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
+
+ // local import name → canonical kebab name. Only populated in GJS/GTS.
+ const importedModifiers = new Map();
+
+ function isRenderModifier(name) {
+ if (isStrictMode) {
+ return importedModifiers.has(name);
+ }
+ // HBS: resolver-resolved by canonical kebab name
+ return KEBAB_NAMES.has(name);
+ }
+
+ function canonicalName(name) {
+ return importedModifiers.get(name) || name;
+ }
+
return {
+ ImportDeclaration(node) {
+ if (!isStrictMode) {
+ return;
+ }
+ const source = node.source.value;
+
+ if (source === ROOT_PACKAGE) {
+ // `import { didInsert, didUpdate as x } from '@ember/render-modifiers'`
+ for (const specifier of node.specifiers) {
+ if (specifier.type === 'ImportSpecifier') {
+ const exportedName = specifier.imported.name;
+ const canonical = ROOT_NAMED_EXPORTS[exportedName];
+ if (canonical) {
+ importedModifiers.set(specifier.local.name, canonical);
+ }
+ }
+ }
+ return;
+ }
+
+ // Sub-path: `import didInsert from '@ember/render-modifiers/modifiers/did-insert'`
+ const canonical = RENDER_MODIFIER_IMPORT_PATHS[source];
+ if (!canonical) {
+ return;
+ }
+ for (const specifier of node.specifiers) {
+ if (specifier.type === 'ImportDefaultSpecifier') {
+ importedModifiers.set(specifier.local.name, canonical);
+ }
+ }
+ },
+
GlimmerElementNode(node) {
if (!node.modifiers) {
return;
}
for (const modifier of node.modifiers) {
- if (
- modifier.path &&
- modifier.path.type === 'GlimmerPathExpression' &&
- (modifier.path.original === 'did-insert' ||
- modifier.path.original === 'did-update' ||
- modifier.path.original === 'will-destroy')
- ) {
+ if (modifier.path?.type !== 'GlimmerPathExpression') {
+ continue;
+ }
+ const name = modifier.path.original;
+ if (isRenderModifier(name)) {
context.report({
node: modifier,
messageId: 'noRenderModifier',
- data: { modifier: modifier.path.original },
+ data: { modifier: canonicalName(name) },
});
}
}
diff --git a/tests/lib/rules/template-no-at-ember-render-modifiers.js b/tests/lib/rules/template-no-at-ember-render-modifiers.js
index 88e525ea38..4dcdb1a8f5 100644
--- a/tests/lib/rules/template-no-at-ember-render-modifiers.js
+++ b/tests/lib/rules/template-no-at-ember-render-modifiers.js
@@ -32,6 +32,25 @@ ruleTester.run('template-no-at-ember-render-modifiers', rule, {
'{{did-insert}}',
'{{did-update}}',
'{{will-destroy}}',
+
+ // In GJS/GTS, kebab identifiers cannot be imports; these are bare paths
+ // that happen to share the canonical name but are not the render modifier.
+ {
+ filename: 'test.gjs',
+ code: '',
+ },
+ // Unrelated imports with matching local names should not match
+ {
+ filename: 'test.gjs',
+ code: `import didInsert from './my-lib';
+ `,
+ },
+ // Root-package import of an unknown named export is not a render modifier
+ {
+ filename: 'test.gjs',
+ code: `import { somethingElse } from '@ember/render-modifiers';
+ `,
+ },
],
invalid: [
@@ -87,6 +106,68 @@ ruleTester.run('template-no-at-ember-render-modifiers', rule, {
output: null,
errors: [{ messageId: 'noRenderModifier' }],
},
+
+ // GJS/GTS import-based forms — local name is user-chosen
+ {
+ filename: 'test.gjs',
+ code: `import didInsert from '@ember/render-modifiers/modifiers/did-insert';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ {
+ filename: 'test.gjs',
+ code: `import didUpdate from '@ember/render-modifiers/modifiers/did-update';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ {
+ filename: 'test.gjs',
+ code: `import willDestroy from '@ember/render-modifiers/modifiers/will-destroy';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ // Renamed default import still flags
+ {
+ filename: 'test.gjs',
+ code: `import myInsert from '@ember/render-modifiers/modifiers/did-insert';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+
+ // Root-package named imports — all three modifiers
+ {
+ filename: 'test.gjs',
+ code: `import { didInsert } from '@ember/render-modifiers';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ {
+ filename: 'test.gjs',
+ code: `import { didUpdate } from '@ember/render-modifiers';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ {
+ filename: 'test.gjs',
+ code: `import { willDestroy } from '@ember/render-modifiers';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
+ // Aliased root-package import still flags
+ {
+ filename: 'test.gjs',
+ code: `import { didInsert as myModifier } from '@ember/render-modifiers';
+ `,
+ output: null,
+ errors: [{ messageId: 'noRenderModifier' }],
+ },
],
});