Skip to content

Commit 575e867

Browse files
committed
Add informative message for kebab-case components in GJS/GTS
Kebab-case names can't become valid JS bindings, so a full autofix (component helper removal + import) isn't feasible here. Report with a dedicated messageId that tells the user the PascalCase equivalent and points to ember-codemods/angle-brackets-codemod for automated end-to-end migration.
1 parent 4dbe5ac commit 575e867

2 files changed

Lines changed: 44 additions & 29 deletions

File tree

lib/rules/template-no-unnecessary-component-helper.js

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
function toPascalCase(name) {
2+
return name
3+
.split(/[/-]/)
4+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
5+
.join('');
6+
}
7+
8+
function isValidIdentifier(name) {
9+
return /^[$A-Z_a-z][\w$]*$/.test(name);
10+
}
11+
112
function isComponentWithStringLiteral(node) {
213
return (
314
node.path &&
@@ -48,6 +59,10 @@ module.exports = {
4859
schema: [],
4960
messages: {
5061
noUnnecessaryComponent: 'Invoke component directly instead of using `component` helper',
62+
noUnnecessaryComponentKebab:
63+
'In GJS/GTS, "{{name}}" must be imported as a JS binding (e.g. `import {{pascal}} from "..."`). ' +
64+
'Invoke it directly as `<{{pascal}}>` instead of via the `component` helper. ' +
65+
'The ember-codemods angle-brackets-codemod can automate this migration.',
5166
},
5267
originallyFrom: {
5368
name: 'ember-template-lint',
@@ -63,15 +78,23 @@ module.exports = {
6378
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
6479
let inAttribute = 0;
6580

66-
// In strict mode, a kebab-case / slash component name cannot become
67-
// a bare mustache invocation — the resulting identifier would not be
68-
// a valid JS binding. Detect the rule's violation, but skip autofix
69-
// when it would produce unparseable GJS/GTS output.
70-
function canAutofix(componentName) {
71-
if (!isStrictMode) {
72-
return true;
81+
// In strict mode, a kebab-case / slash component name cannot become a bare
82+
// mustache invocation — the result would not be a valid JS binding and would
83+
// require an import. Ecosystem tooling (ember-codemods/angle-brackets-codemod)
84+
// handles this migration end-to-end including adding the import.
85+
function buildReport(node, componentName, fix) {
86+
if (isStrictMode && !isValidIdentifier(componentName)) {
87+
return {
88+
node,
89+
messageId: 'noUnnecessaryComponentKebab',
90+
data: { name: componentName, pascal: toPascalCase(componentName) },
91+
};
92+
}
93+
const report = { node, messageId: 'noUnnecessaryComponent' };
94+
if (!isStrictMode || isValidIdentifier(componentName)) {
95+
report.fix = fix;
7396
}
74-
return /^[$A-Z_a-z][\w$]*$/.test(componentName);
97+
return report;
7598
}
7699

77100
return {
@@ -92,14 +115,9 @@ module.exports = {
92115

93116
const componentName = node.params[0].value || node.params[0].original;
94117
const invocation = getComponentInvocationText(sourceCode, node, componentName);
95-
const report = {
96-
node,
97-
messageId: 'noUnnecessaryComponent',
98-
};
99-
if (canAutofix(componentName)) {
100-
report.fix = (fixer) => fixer.replaceText(node, `{{${invocation}}}`);
101-
}
102-
context.report(report);
118+
context.report(
119+
buildReport(node, componentName, (fixer) => fixer.replaceText(node, `{{${invocation}}}`))
120+
);
103121
},
104122

105123
GlimmerBlockStatement(node) {
@@ -113,12 +131,8 @@ module.exports = {
113131
const componentName = node.params[0].value || node.params[0].original;
114132
const invocation = getComponentInvocationText(sourceCode, node, componentName);
115133

116-
const report = {
117-
node,
118-
messageId: 'noUnnecessaryComponent',
119-
};
120-
if (canAutofix(componentName)) {
121-
report.fix = (fixer) => {
134+
context.report(
135+
buildReport(node, componentName, (fixer) => {
122136
const openInvocationEnd = getOpenInvocationEnd(node);
123137
const closingPathEnd = node.range[1] - 2;
124138
const closingPathStart = closingPathEnd - node.path.original.length;
@@ -127,9 +141,8 @@ module.exports = {
127141
fixer.replaceTextRange([node.path.range[0], openInvocationEnd], invocation),
128142
fixer.replaceTextRange([closingPathStart, closingPathEnd], componentName),
129143
];
130-
};
131-
}
132-
context.report(report);
144+
})
145+
);
133146
},
134147
};
135148
},

tests/lib/rules/template-no-unnecessary-component-helper.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,21 @@ gjsRuleTester.run('template-no-unnecessary-component-helper', rule, {
9393
valid: validGjs,
9494
invalid: [
9595
...invalidHbs.map(wrapTemplate),
96-
// GJS/GTS: autofix is skipped when the component name isn't a valid JS
97-
// identifier. The error is still reported so the user sees the issue.
96+
// GJS/GTS: kebab-case names can't be valid JS identifiers — report with
97+
// a dedicated message suggesting the PascalCase form and import.
98+
// Full migration (including adding the import) is best handled by
99+
// ember-codemods/angle-brackets-codemod.
98100
{
99101
filename: 'test.gjs',
100102
code: '<template>{{component "my-component-name" foo=123}}</template>',
101103
output: null,
102-
errors: [{ messageId: 'noUnnecessaryComponent' }],
104+
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
103105
},
104106
{
105107
filename: 'test.gts',
106108
code: '<template>{{#component "my-component-name"}}content{{/component}}</template>',
107109
output: null,
108-
errors: [{ messageId: 'noUnnecessaryComponent' }],
110+
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
109111
},
110112
// GJS/GTS: valid JS identifier → autofix still applies
111113
{

0 commit comments

Comments
 (0)