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: '', + output: null, + errors: [{ messageId: 'noUnnecessaryComponentKebab' }], + }, + { + filename: 'test.gts', + code: '', + output: null, + errors: [{ messageId: 'noUnnecessaryComponentKebab' }], + }, + // GJS/GTS: valid JS identifier → autofix still applies + { + filename: 'test.gjs', + code: '', + output: '', + errors: [{ messageId: 'noUnnecessaryComponent' }], + }, + ], }); const hbsRuleTester = new RuleTester({