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+
112function 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 } ,
0 commit comments