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' ,
@@ -59,8 +74,29 @@ module.exports = {
5974
6075 create ( context ) {
6176 const sourceCode = context . sourceCode ;
77+ const filename = context . filename ;
78+ const isStrictMode = filename . endsWith ( '.gjs' ) || filename . endsWith ( '.gts' ) ;
6279 let inAttribute = 0 ;
6380
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 ;
96+ }
97+ return report ;
98+ }
99+
64100 return {
65101 GlimmerAttrNode ( ) {
66102 inAttribute ++ ;
@@ -79,13 +115,9 @@ module.exports = {
79115
80116 const componentName = node . params [ 0 ] . value || node . params [ 0 ] . original ;
81117 const invocation = getComponentInvocationText ( sourceCode , node , componentName ) ;
82- context . report ( {
83- node,
84- messageId : 'noUnnecessaryComponent' ,
85- fix ( fixer ) {
86- return fixer . replaceText ( node , `{{${ invocation } }}` ) ;
87- } ,
88- } ) ;
118+ context . report (
119+ buildReport ( node , componentName , ( fixer ) => fixer . replaceText ( node , `{{${ invocation } }}` ) )
120+ ) ;
89121 } ,
90122
91123 GlimmerBlockStatement ( node ) {
@@ -99,10 +131,8 @@ module.exports = {
99131 const componentName = node . params [ 0 ] . value || node . params [ 0 ] . original ;
100132 const invocation = getComponentInvocationText ( sourceCode , node , componentName ) ;
101133
102- context . report ( {
103- node,
104- messageId : 'noUnnecessaryComponent' ,
105- fix ( fixer ) {
134+ context . report (
135+ buildReport ( node , componentName , ( fixer ) => {
106136 const openInvocationEnd = getOpenInvocationEnd ( node ) ;
107137 const closingPathEnd = node . range [ 1 ] - 2 ;
108138 const closingPathStart = closingPathEnd - node . path . original . length ;
@@ -111,8 +141,8 @@ module.exports = {
111141 fixer . replaceTextRange ( [ node . path . range [ 0 ] , openInvocationEnd ] , invocation ) ,
112142 fixer . replaceTextRange ( [ closingPathStart , closingPathEnd ] , componentName ) ,
113143 ] ;
114- } ,
115- } ) ;
144+ } )
145+ ) ;
116146 } ,
117147 } ;
118148 } ,
0 commit comments