11const fs = require ( 'fs' ) ;
22const path = require ( 'path' ) ;
3- const templateRecast = require ( 'ember-template-recast' ) ;
43const j = require ( 'jscodeshift' ) . withParser ( 'ts' ) ;
54const _debug = require ( 'debug' ) ( 'tagless-ember-components-codemod' ) ;
65
76const SilentError = require ( './silent-error' ) ;
8-
9- const b = templateRecast . builders ;
10-
11- const EVENT_HANDLER_METHODS = [
12- // Touch events
13- 'touchStart' ,
14- 'touchMove' ,
15- 'touchEnd' ,
16- 'touchCancel' ,
17-
18- // Keyboard events
19- 'keyDown' ,
20- 'keyUp' ,
21- 'keyPress' ,
22-
23- // Mouse events
24- 'mouseDown' ,
25- 'mouseUp' ,
26- 'contextMenu' ,
27- 'click' ,
28- 'doubleClick' ,
29- 'focusIn' ,
30- 'focusOut' ,
31-
32- // Form events
33- 'submit' ,
34- 'change' ,
35- 'focusIn' ,
36- 'focusOut' ,
37- 'input' ,
38-
39- // Drag and drop events
40- 'dragStart' ,
41- 'drag' ,
42- 'dragEnter' ,
43- 'dragLeave' ,
44- 'dragOver' ,
45- 'dragEnd' ,
46- 'drop' ,
47- ] ;
48-
49- const PLACEHOLDER = '@@@PLACEHOLDER@@@' ;
7+ const transformClassicComponent = require ( './transform/classic' ) ;
8+ const transformTemplate = require ( './transform/template' ) ;
509
5110function transformPath ( componentPath , options ) {
5211 let debug = ( fmt , ...args ) => _debug ( `${ componentPath } : ${ fmt } ` , ...args ) ;
@@ -70,11 +29,7 @@ function transformPath(componentPath, options) {
7029 return result . tagName ;
7130}
7231
73- function transform ( source , template , options = { } ) {
74- let debug = options . debug || _debug ;
75-
76- let root = j ( source ) ;
77-
32+ function checkComponentType ( root ) {
7833 // find `export default Component.extend({ ... });` AST node
7934 let exportDefaultDeclarations = root . find ( j . ExportDefaultDeclaration , {
8035 declaration : {
@@ -87,235 +42,34 @@ function transform(source, template, options = {}) {
8742 } ,
8843 } ) ;
8944
90- if ( exportDefaultDeclarations . length !== 1 ) {
91- throw new SilentError ( `Could not find \`export default Component.extend({ ... });\`` ) ;
92- }
93-
94- let exportDefaultDeclaration = exportDefaultDeclarations . get ( ) ;
95-
96- // find first `{ ... }` inside `Component.extend()` arguments
97- let extendObjectArgs = exportDefaultDeclaration
98- . get ( 'declaration' , 'arguments' )
99- . filter ( path => path . value . type === 'ObjectExpression' ) ;
100-
101- let objectArg = extendObjectArgs [ 0 ] ;
102- if ( ! objectArg ) {
103- throw new SilentError (
104- `Could not find object argument in \`export default Component.extend({ ... });\``
105- ) ;
106- }
107-
108- // find `tagName` property if it exists
109- let properties = objectArg . get ( 'properties' ) ;
110- let tagName = findTagName ( properties ) ;
111-
112- // skip tagless components (silent)
113- if ( tagName === '' ) {
114- debug ( 'tagName: %o -> skip' , tagName ) ;
115- return { source, template } ;
116- }
117-
118- debug ( 'tagName: %o' , tagName ) ;
119-
120- // skip components that use `this.element`
121- let thisElementPaths = j ( objectArg ) . find ( j . MemberExpression , {
122- object : { type : 'ThisExpression' } ,
123- property : { name : 'element' } ,
124- } ) ;
125- if ( thisElementPaths . length !== 0 ) {
126- throw new SilentError ( `Using \`this.element\` is not supported in tagless components` ) ;
127- }
128-
129- // skip components that use `this.elementId`
130- let thisElementIdPaths = j ( objectArg ) . find ( j . MemberExpression , {
131- object : { type : 'ThisExpression' } ,
132- property : { name : 'elementId' } ,
133- } ) ;
134- if ( thisElementIdPaths . length !== 0 ) {
135- throw new SilentError ( `Using \`this.elementId\` is not supported in tagless components` ) ;
136- }
137-
138- // skip components that use `click()` etc.
139- for ( let methodName of EVENT_HANDLER_METHODS ) {
140- let handlerMethod = properties . filter ( path => isMethod ( path , methodName ) ) [ 0 ] ;
141- if ( handlerMethod ) {
142- throw new SilentError ( `Using \`${ methodName } ()\` is not supported in tagless components` ) ;
143- }
144- }
145-
146- // analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
147- let elementId = findElementId ( properties ) ;
148- debug ( 'elementId: %o' , elementId ) ;
149-
150- let attributeBindings = findAttributeBindings ( properties ) ;
151- debug ( 'attributeBindings: %o' , attributeBindings ) ;
152-
153- let classNames = findClassNames ( properties ) ;
154- debug ( 'classNames: %o' , classNames ) ;
155-
156- let classNameBindings = findClassNameBindings ( properties ) ;
157- debug ( 'classNameBindings: %o' , classNameBindings ) ;
158-
159- let templateAST = templateRecast . parse ( template ) ;
160-
161- // set `tagName: ''`
162- let tagNamePath = j ( properties )
163- . find ( j . ObjectProperty )
164- . filter ( path => path . parentPath === properties )
165- . filter ( path => isProperty ( path , 'tagName' ) ) ;
166-
167- if ( tagNamePath . length === 1 ) {
168- j ( tagNamePath . get ( 'value' ) ) . replaceWith ( j . stringLiteral ( '' ) ) ;
169- } else {
170- properties . unshift ( j . objectProperty ( j . identifier ( 'tagName' ) , j . stringLiteral ( '' ) ) ) ;
171- }
172-
173- // remove `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
174- j ( properties )
175- . find ( j . ObjectProperty )
176- . filter ( path => path . parentPath === properties )
177- . filter (
178- path =>
179- isProperty ( path , 'elementId' ) ||
180- isProperty ( path , 'attributeBindings' ) ||
181- isProperty ( path , 'classNames' ) ||
182- isProperty ( path , 'classNameBindings' )
183- )
184- . remove ( ) ;
185-
186- let newSource = root . toSource ( ) ;
187-
188- // wrap existing template with root element
189- let classNodes = [ ] ;
190- if ( options . hasComponentCSS ) {
191- classNodes . push ( b . mustache ( 'styleNamespace' ) ) ;
192- }
193- for ( let className of classNames ) {
194- classNodes . push ( b . text ( className ) ) ;
195- }
196- classNameBindings . forEach ( ( [ truthy , falsy ] , property ) => {
197- if ( ! truthy ) {
198- classNodes . push ( b . mustache ( `unless this.${ property } "${ falsy } "` ) ) ;
199- } else {
200- classNodes . push ( b . mustache ( `if this.${ property } "${ truthy } "${ falsy ? ` "${ falsy } "` : '' } ` ) ) ;
201- }
202- } ) ;
203-
204- let attrs = [ ] ;
205- if ( elementId ) {
206- attrs . push ( b . attr ( 'id' , b . text ( elementId ) ) ) ;
207- }
208- attributeBindings . forEach ( ( value , key ) => {
209- attrs . push ( b . attr ( key , b . mustache ( `this.${ value } ` ) ) ) ;
210- } ) ;
211- if ( classNodes . length === 1 ) {
212- attrs . push ( b . attr ( 'class' , classNodes [ 0 ] ) ) ;
213- } else if ( classNodes . length !== 0 ) {
214- let parts = [ ] ;
215- classNodes . forEach ( ( node , i ) => {
216- if ( i !== 0 ) parts . push ( b . text ( ' ' ) ) ;
217- parts . push ( node ) ;
218- } ) ;
219-
220- attrs . push ( b . attr ( 'class' , b . concat ( parts ) ) ) ;
221- }
222- attrs . push ( b . attr ( '...attributes' , b . text ( '' ) ) ) ;
223-
224- templateAST . body = [
225- b . element ( tagName , {
226- attrs,
227- children : [ b . text ( `\n${ PLACEHOLDER } \n` ) ] ,
228- } ) ,
229- ] ;
230-
231- let newTemplate = templateRecast . print ( templateAST ) . replace ( PLACEHOLDER , indentLines ( template ) ) ;
232-
233- return { source : newSource , template : newTemplate , tagName } ;
234- }
235-
236- function isProperty ( path , name ) {
237- let node = path . value ;
238- return node . type === 'ObjectProperty' && node . key . type === 'Identifier' && node . key . name === name ;
239- }
240-
241- function isMethod ( path , name ) {
242- let node = path . value ;
243- return node . type === 'ObjectMethod' && node . key . type === 'Identifier' && node . key . name === name ;
244- }
245-
246- function findStringProperty ( properties , name , defaultValue = null ) {
247- let propertyPath = properties . filter ( path => isProperty ( path , name ) ) [ 0 ] ;
248- if ( ! propertyPath ) {
249- return defaultValue ;
45+ if ( exportDefaultDeclarations . length === 1 ) {
46+ return 'classic' ;
25047 }
251-
252- let valuePath = propertyPath . get ( 'value' ) ;
253- if ( valuePath . value . type !== 'StringLiteral' ) {
254- throw new SilentError ( `Unexpected \`${ name } \` value: ${ j ( valuePath ) . toSource ( ) } ` ) ;
255- }
256-
257- return valuePath . value . value ;
25848}
25949
260- function findTagName ( properties ) {
261- return findStringProperty ( properties , 'tagName' , 'div' ) ;
262- }
263-
264- function findElementId ( properties ) {
265- return findStringProperty ( properties , 'elementId' ) ;
266- }
267-
268- function findStringArrayProperties ( properties , name ) {
269- let propertyPath = properties . filter ( path => isProperty ( path , name ) ) [ 0 ] ;
270- if ( ! propertyPath ) {
271- return [ ] ;
272- }
273-
274- let arrayPath = propertyPath . get ( 'value' ) ;
275- if ( arrayPath . value . type !== 'ArrayExpression' ) {
276- throw new SilentError ( `Unexpected \`${ name } \` value: ${ j ( arrayPath ) . toSource ( ) } ` ) ;
277- }
278-
279- return arrayPath . get ( 'elements' ) . value . map ( element => {
280- if ( element . type !== 'StringLiteral' ) {
281- throw new SilentError ( `Unexpected \`${ name } \` value: ${ j ( arrayPath ) . toSource ( ) } ` ) ;
282- }
283-
284- return element . value ;
285- } ) ;
286- }
50+ function transform ( source , template , options = { } ) {
51+ let root = j ( source ) ;
52+ let type = checkComponentType ( root ) ;
53+ let result ;
28754
288- function findAttributeBindings ( properties ) {
289- let attrBindings = new Map ( ) ;
290- for ( let binding of findStringArrayProperties ( properties , 'attributeBindings' ) ) {
291- let [ value , attr ] = binding . split ( ':' ) ;
292- attrBindings . set ( attr || value , value ) ;
55+ switch ( type ) {
56+ case 'classic' :
57+ result = transformClassicComponent ( root , options ) ;
58+ break ;
59+ default :
60+ throw new Error (
61+ `Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently.`
62+ ) ;
29363 }
29464
295- return attrBindings ;
296- }
65+ if ( result ) {
66+ let { newSource, attrs, tagName } = result ;
67+ let newTemplate = transformTemplate ( template , tagName , attrs ) ;
29768
298- function findClassNames ( properties ) {
299- return findStringArrayProperties ( properties , 'classNames' ) ;
300- }
301-
302- function findClassNameBindings ( properties ) {
303- let classNameBindings = new Map ( ) ;
304- for ( let binding of findStringArrayProperties ( properties , 'classNameBindings' ) ) {
305- let parts = binding . split ( ':' ) ;
306-
307- if ( parts . length === 1 ) {
308- throw new SilentError ( `Unsupported non-boolean \`classNameBindings\` value: ${ binding } ` ) ;
309- } else if ( parts . length === 2 ) {
310- classNameBindings . set ( parts [ 0 ] , [ parts [ 1 ] , null ] ) ;
311- } else if ( parts . length === 3 ) {
312- classNameBindings . set ( parts [ 0 ] , [ parts [ 1 ] || null , parts [ 2 ] ] ) ;
313- } else {
314- throw new SilentError ( `Unexpected \`classNameBindings\` value: ${ binding } ` ) ;
315- }
69+ return { source : newSource , template : newTemplate , tagName } ;
31670 }
31771
318- return classNameBindings ;
72+ return { source , template } ;
31973}
32074
32175function guessTemplatePath ( componentPath ) {
@@ -327,20 +81,8 @@ function guessTemplatePath(componentPath) {
32781 return componentPath . replace ( '/components/' , '/templates/components/' ) . replace ( / \. j s $ / , '.hbs' ) ;
32882}
32983
330- function indentLines ( content ) {
331- return content
332- . split ( '\n' )
333- . map ( it => ` ${ it } ` )
334- . join ( '\n' ) ;
335- }
336-
33784module . exports = {
33885 transform,
33986 transformPath,
340- findTagName,
341- findElementId,
342- findAttributeBindings,
343- findClassNames,
344- findClassNameBindings,
34587 guessTemplatePath,
34688} ;
0 commit comments