diff --git a/lib/rules/template-no-unnecessary-component-helper.js b/lib/rules/template-no-unnecessary-component-helper.js
index 9a2f67bf4f..4fd60988cf 100644
--- a/lib/rules/template-no-unnecessary-component-helper.js
+++ b/lib/rules/template-no-unnecessary-component-helper.js
@@ -1,3 +1,14 @@
+function toPascalCase(name) {
+ return name
+ .split(/[/-]/)
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join('');
+}
+
+function isValidIdentifier(name) {
+ return /^[$A-Z_a-z][\w$]*$/.test(name);
+}
+
function isComponentWithStringLiteral(node) {
return (
node.path &&
@@ -48,6 +59,10 @@ module.exports = {
schema: [],
messages: {
noUnnecessaryComponent: 'Invoke component directly instead of using `component` helper',
+ noUnnecessaryComponentKebab:
+ 'In GJS/GTS, "{{name}}" must be imported as a JS binding (e.g. `import {{pascal}} from "..."`). ' +
+ 'Invoke it directly as `<{{pascal}}>` instead of via the `component` helper. ' +
+ 'The ember-codemods angle-brackets-codemod can automate this migration.',
},
originallyFrom: {
name: 'ember-template-lint',
@@ -59,8 +74,29 @@ module.exports = {
create(context) {
const sourceCode = context.sourceCode;
+ const filename = context.filename;
+ const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
let inAttribute = 0;
+ // In strict mode, a kebab-case / slash component name cannot become a bare
+ // mustache invocation — the result would not be a valid JS binding and would
+ // require an import. Ecosystem tooling (ember-codemods/angle-brackets-codemod)
+ // handles this migration end-to-end including adding the import.
+ function buildReport(node, componentName, fix) {
+ if (isStrictMode && !isValidIdentifier(componentName)) {
+ return {
+ node,
+ messageId: 'noUnnecessaryComponentKebab',
+ data: { name: componentName, pascal: toPascalCase(componentName) },
+ };
+ }
+ const report = { node, messageId: 'noUnnecessaryComponent' };
+ if (!isStrictMode || isValidIdentifier(componentName)) {
+ report.fix = fix;
+ }
+ return report;
+ }
+
return {
GlimmerAttrNode() {
inAttribute++;
@@ -79,13 +115,9 @@ module.exports = {
const componentName = node.params[0].value || node.params[0].original;
const invocation = getComponentInvocationText(sourceCode, node, componentName);
- context.report({
- node,
- messageId: 'noUnnecessaryComponent',
- fix(fixer) {
- return fixer.replaceText(node, `{{${invocation}}}`);
- },
- });
+ context.report(
+ buildReport(node, componentName, (fixer) => fixer.replaceText(node, `{{${invocation}}}`))
+ );
},
GlimmerBlockStatement(node) {
@@ -99,10 +131,8 @@ module.exports = {
const componentName = node.params[0].value || node.params[0].original;
const invocation = getComponentInvocationText(sourceCode, node, componentName);
- context.report({
- node,
- messageId: 'noUnnecessaryComponent',
- fix(fixer) {
+ context.report(
+ buildReport(node, componentName, (fixer) => {
const openInvocationEnd = getOpenInvocationEnd(node);
const closingPathEnd = node.range[1] - 2;
const closingPathStart = closingPathEnd - node.path.original.length;
@@ -111,8 +141,8 @@ module.exports = {
fixer.replaceTextRange([node.path.range[0], openInvocationEnd], invocation),
fixer.replaceTextRange([closingPathStart, closingPathEnd], componentName),
];
- },
- });
+ })
+ );
},
};
},
diff --git a/tests/lib/rules/template-no-unnecessary-component-helper.js b/tests/lib/rules/template-no-unnecessary-component-helper.js
index 4a7344f457..217b5d2b35 100644
--- a/tests/lib/rules/template-no-unnecessary-component-helper.js
+++ b/tests/lib/rules/template-no-unnecessary-component-helper.js
@@ -91,7 +91,32 @@ const gjsRuleTester = new RuleTester({
gjsRuleTester.run('template-no-unnecessary-component-helper', rule, {
valid: validGjs,
- invalid: invalidHbs.map(wrapTemplate),
+ invalid: [
+ ...invalidHbs.map(wrapTemplate),
+ // GJS/GTS: kebab-case names can't be valid JS identifiers — report with
+ // a dedicated message suggesting the PascalCase form and import.
+ // Full migration (including adding the import) is best handled by
+ // ember-codemods/angle-brackets-codemod.
+ {
+ filename: 'test.gjs',
+ code: '{{component "my-component-name" foo=123}}',
+ output: null,
+ errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
+ },
+ {
+ filename: 'test.gts',
+ code: '{{#component "my-component-name"}}content{{/component}}',
+ output: null,
+ errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
+ },
+ // GJS/GTS: valid JS identifier → autofix still applies
+ {
+ filename: 'test.gjs',
+ code: '{{component "myComponent" foo=123}}',
+ output: '{{myComponent foo=123}}',
+ errors: [{ messageId: 'noUnnecessaryComponent' }],
+ },
+ ],
});
const hbsRuleTester = new RuleTester({