1
1
'use strict' ;
2
2
3
3
const fs = require ( 'fs' ) ;
4
- const mkdirp = require ( 'mkdirp' ) ;
5
- const copyFileSync = require ( 'fs-copy-file-sync' ) ;
6
4
const path = require ( 'path' ) ;
7
5
const walkSync = require ( 'walk-sync' ) ;
8
6
const Plugin = require ( 'broccoli-plugin' ) ;
9
7
const logger = require ( 'heimdalljs-logger' ) ( 'ember-cli-htmlbars:colocated-broccoli-plugin' ) ;
8
+ const FSTree = require ( 'fs-tree-diff' ) ;
10
9
11
- function detectRootName ( files ) {
12
- let [ first ] = files ;
13
- let parts = first . split ( '/' ) ;
10
+ module . exports = class ColocatedTemplateProcessor extends Plugin {
11
+ constructor ( tree ) {
12
+ super ( [ tree ] , {
13
+ persistentOutput : true ,
14
+ } ) ;
14
15
15
- let root ;
16
- if ( parts [ 0 ] . startsWith ( '@' ) ) {
17
- root = parts . slice ( 0 , 2 ) . join ( '/' ) ;
18
- } else {
19
- root = parts [ 0 ] ;
16
+ this . _lastTree = FSTree . fromEntries ( [ ] ) ;
20
17
}
21
18
22
- if ( ! files . every ( f => f . startsWith ( root ) ) ) {
23
- root = null ;
19
+ calculatePatch ( ) {
20
+ let updatedEntries = walkSync . entries ( this . inputPaths [ 0 ] ) ;
21
+ let currentTree = FSTree . fromEntries ( updatedEntries ) ;
22
+
23
+ let patch = this . _lastTree . calculatePatch ( currentTree ) ;
24
+
25
+ this . _lastTree = currentTree ;
26
+
27
+ return patch ;
24
28
}
25
29
26
- return root ;
27
- }
30
+ currentEntries ( ) {
31
+ return this . _lastTree . entries ;
32
+ }
28
33
29
- module . exports = class ColocatedTemplateProcessor extends Plugin {
30
- constructor ( tree , options ) {
31
- super ( [ tree ] , options ) ;
34
+ inputHasFile ( relativePath ) {
35
+ return ! ! this . currentEntries ( ) . find ( e => e . relativePath === relativePath ) ;
32
36
}
33
37
34
- build ( ) {
35
- let files = walkSync ( this . inputPaths [ 0 ] , { directories : false } ) ;
38
+ detectRootName ( ) {
39
+ let entries = this . currentEntries ( ) . filter ( e => ! e . isDirectory ( ) ) ;
36
40
37
- if ( files . length === 0 ) {
38
- // nothing to do, bail
39
- return ;
40
- }
41
+ let [ first ] = entries ;
42
+ let parts = first . relativePath . split ( '/' ) ;
41
43
42
- let root = detectRootName ( files ) ;
44
+ let root ;
45
+ if ( parts [ 0 ] . startsWith ( '@' ) ) {
46
+ root = parts . slice ( 0 , 2 ) . join ( '/' ) ;
47
+ } else {
48
+ root = parts [ 0 ] ;
49
+ }
43
50
44
- let filesToCopy = [ ] ;
45
- files . forEach ( filePath => {
46
- if ( root === null ) {
47
- // do nothing, we cannot detect the proper root path for the app/addon
48
- // being processed
49
- filesToCopy . push ( filePath ) ;
50
- return ;
51
- }
51
+ if ( ! entries . every ( e => e . relativePath . startsWith ( root ) ) ) {
52
+ root = null ;
53
+ }
52
54
53
- let filePathParts = path . parse ( filePath ) ;
54
- let inputPath = path . join ( this . inputPaths [ 0 ] , filePath ) ;
55
+ return root ;
56
+ }
55
57
56
- // TODO: why are these different?
57
- // Apps: my-app/components/foo.hbs, my-app/templates/components/foo.hbs
58
- // Addons: components/foo.js, templates/components/foo.hbs
59
- //
60
- // will be fixed by https://github.com/ember-cli/ember-cli/pull/8834
58
+ build ( ) {
59
+ let patch = this . calculatePatch ( ) ;
61
60
62
- let isInsideComponentsFolder = filePath . startsWith ( `${ root } /components/` ) ;
61
+ // We skip building if this is a rebuild with a zero-length patch
62
+ if ( patch . length === 0 ) {
63
+ return ;
64
+ }
63
65
64
- // copy forward non-hbs files
65
- // TODO: don't copy .js files that will ultimately be overridden
66
- if ( ! isInsideComponentsFolder || filePathParts . ext !== '.hbs' ) {
67
- filesToCopy . push ( filePath ) ;
68
- return ;
66
+ let root = this . detectRootName ( ) ;
67
+
68
+ let processedColocatedFiles = new Set ( ) ;
69
+
70
+ for ( let operation of patch ) {
71
+ let [ method , relativePath ] = operation ;
72
+
73
+ let filePathParts = path . parse ( relativePath ) ;
74
+
75
+ let isOutsideComponentsFolder = ! relativePath . startsWith ( `${ root } /components/` ) ;
76
+ let isPodsTemplate = filePathParts . name === 'template' && filePathParts . ext === '.hbs' ;
77
+ let isNotColocationExtension = ! [ '.hbs' , '.js' , '.ts' , '.coffee' ] . includes ( filePathParts . ext ) ;
78
+ let isDirectoryOperation = [ 'rmdir' , 'mkdir' ] . includes ( method ) ;
79
+ let basePath = path . posix . join ( filePathParts . dir , filePathParts . name ) ;
80
+ let relativeTemplatePath = basePath + '.hbs' ;
81
+
82
+ // if the change in question has nothing to do with colocated templates
83
+ // just apply the patch to the outputPath
84
+ if (
85
+ isOutsideComponentsFolder ||
86
+ isPodsTemplate ||
87
+ isNotColocationExtension ||
88
+ isDirectoryOperation
89
+ ) {
90
+ logger . debug ( `default operation for non-colocation modification: ${ relativePath } ` ) ;
91
+ FSTree . applyPatch ( this . inputPaths [ 0 ] , this . outputPath , [ operation ] ) ;
92
+ continue ;
69
93
}
70
94
71
- if ( filePathParts . name === 'template' ) {
72
- filesToCopy . push ( filePath ) ;
73
- return ;
95
+ // we have already processed this colocated file, carry on
96
+ if ( processedColocatedFiles . has ( basePath ) ) {
97
+ continue ;
74
98
}
99
+ processedColocatedFiles . add ( basePath ) ;
75
100
76
101
let hasBackingClass = false ;
77
- let backingClassPath = path . join ( filePathParts . dir , filePathParts . name ) ;
102
+ let hasTemplate = this . inputHasFile ( basePath + '.hbs' ) ;
103
+ let backingClassPath = basePath ;
78
104
79
- if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.js' ) ) ) {
105
+ if ( this . inputHasFile ( basePath + '.js' ) ) {
80
106
backingClassPath += '.js' ;
81
107
hasBackingClass = true ;
82
- } else if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.ts' ) ) ) {
108
+ } else if ( this . inputHasFile ( basePath + '.ts' ) ) {
83
109
backingClassPath += '.ts' ;
84
110
hasBackingClass = true ;
85
- } else if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.coffee' ) ) ) {
111
+ } else if ( this . inputHasFile ( basePath + '.coffee' ) ) {
86
112
backingClassPath += '.coffee' ;
87
113
hasBackingClass = true ;
88
114
} else {
89
115
backingClassPath += '.js' ;
90
116
hasBackingClass = false ;
91
117
}
92
118
93
- let templateContents = fs . readFileSync ( inputPath , { encoding : 'utf8' } ) ;
119
+ let originalJsContents = null ;
94
120
let jsContents = null ;
95
-
96
- let hbsInvocationOptions = {
97
- contents : templateContents ,
98
- moduleName : filePath ,
99
- parseOptions : {
100
- srcName : filePath ,
101
- } ,
102
- } ;
103
- let hbsInvocation = `hbs(${ JSON . stringify ( templateContents ) } , ${ JSON . stringify (
104
- hbsInvocationOptions
105
- ) } )`;
106
- let prefix = `import { hbs } from 'ember-cli-htmlbars';\nconst __COLOCATED_TEMPLATE__ = ${ hbsInvocation } ;\n` ;
107
- if ( backingClassPath . endsWith ( '.coffee' ) ) {
108
- prefix = `import { hbs } from 'ember-cli-htmlbars'\n__COLOCATED_TEMPLATE__ = ${ hbsInvocation } \n` ;
121
+ let prefix = '' ;
122
+
123
+ if ( hasTemplate ) {
124
+ let templatePath = path . join ( this . inputPaths [ 0 ] , basePath + '.hbs' ) ;
125
+ let templateContents = fs . readFileSync ( templatePath , { encoding : 'utf8' } ) ;
126
+ let hbsInvocationOptions = {
127
+ contents : templateContents ,
128
+ moduleName : relativeTemplatePath ,
129
+ parseOptions : {
130
+ srcName : relativeTemplatePath ,
131
+ } ,
132
+ } ;
133
+ let hbsInvocation = `hbs(${ JSON . stringify ( templateContents ) } , ${ JSON . stringify (
134
+ hbsInvocationOptions
135
+ ) } )`;
136
+
137
+ prefix = `import { hbs } from 'ember-cli-htmlbars';\nconst __COLOCATED_TEMPLATE__ = ${ hbsInvocation } ;\n` ;
138
+ if ( backingClassPath . endsWith ( '.coffee' ) ) {
139
+ prefix = `import { hbs } from 'ember-cli-htmlbars'\n__COLOCATED_TEMPLATE__ = ${ hbsInvocation } \n` ;
140
+ }
109
141
}
110
142
111
- logger . debug (
112
- `processing colocated template: ${ filePath } (template-only: ${ hasBackingClass } )`
113
- ) ;
114
-
115
143
if ( hasBackingClass ) {
116
144
// add the template, call setComponentTemplate
117
145
118
- jsContents = fs . readFileSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath ) , {
119
- encoding : 'utf8' ,
120
- } ) ;
146
+ jsContents = originalJsContents = fs . readFileSync (
147
+ path . join ( this . inputPaths [ 0 ] , backingClassPath ) ,
148
+ {
149
+ encoding : 'utf8' ,
150
+ }
151
+ ) ;
121
152
122
153
if ( ! jsContents . includes ( 'export default' ) ) {
123
- let message = `\`${ filePath } \` does not contain a \`default export\`. Did you forget to export the component class?` ;
154
+ let message = `\`${ relativePath } \` does not contain a \`default export\`. Did you forget to export the component class?` ;
124
155
jsContents = `${ jsContents } \nthrow new Error(${ JSON . stringify ( message ) } );` ;
125
156
prefix = '' ;
126
157
}
@@ -132,29 +163,53 @@ module.exports = class ColocatedTemplateProcessor extends Plugin {
132
163
133
164
jsContents = prefix + jsContents ;
134
165
135
- let outputPath = path . join ( this . outputPath , backingClassPath ) ;
136
-
137
- // TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT)
138
- mkdirp . sync ( path . dirname ( outputPath ) ) ;
139
- fs . writeFileSync ( outputPath , jsContents , { encoding : 'utf8' } ) ;
140
- } ) ;
141
-
142
- filesToCopy . forEach ( filePath => {
143
- let inputPath = path . join ( this . inputPaths [ 0 ] , filePath ) ;
144
- let outputPath = path . join ( this . outputPath , filePath ) ;
145
-
146
- // avoid copying file over top of a previously written one
147
- if ( fs . existsSync ( outputPath ) ) {
148
- return ;
166
+ let jsOutputPath = path . join ( this . outputPath , backingClassPath ) ;
167
+
168
+ switch ( method ) {
169
+ case 'unlink' : {
170
+ if ( filePathParts . ext === '.hbs' && hasBackingClass ) {
171
+ fs . writeFileSync ( jsOutputPath , originalJsContents , { encoding : 'utf8' } ) ;
172
+
173
+ logger . debug ( `removing colocated template for: ${ basePath } ` ) ;
174
+ } else if ( filePathParts . ext !== '.hbs' && hasTemplate ) {
175
+ fs . writeFileSync ( jsOutputPath , jsContents , { encoding : 'utf8' } ) ;
176
+ logger . debug (
177
+ `converting colocated template with backing class to template only: ${ basePath } `
178
+ ) ;
179
+ } else {
180
+ // Copied from https://github.com/stefanpenner/fs-tree-diff/blob/v2.0.1/lib/index.ts#L38-L68
181
+ try {
182
+ fs . unlinkSync ( jsOutputPath ) ;
183
+ } catch ( e ) {
184
+ if ( typeof e === 'object' && e !== null && e . code === 'ENOENT' ) {
185
+ return ;
186
+ }
187
+ throw e ;
188
+ }
189
+ }
190
+ break ;
191
+ }
192
+ case 'change' :
193
+ case 'create' : {
194
+ fs . writeFileSync ( jsOutputPath , jsContents , { encoding : 'utf8' } ) ;
195
+
196
+ logger . debug (
197
+ `writing colocated template: ${ basePath } (template-only: ${ ! hasBackingClass } )`
198
+ ) ;
199
+ break ;
200
+ }
201
+ default : {
202
+ throw new Error (
203
+ `ember-cli-htmlbars: Unexpected operation when patching files for colocation.\n\tOperation:\n${ JSON . stringify (
204
+ [ method , relativePath ]
205
+ ) } \n\tKnown files:\n${ JSON . stringify (
206
+ this . currentEntries ( ) . map ( e => e . relativePath ) ,
207
+ null ,
208
+ 2
209
+ ) } `
210
+ ) ;
211
+ }
149
212
}
150
-
151
- logger . debug ( `copying unchanged file: ${ filePath } ` ) ;
152
-
153
- // TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT)
154
- mkdirp . sync ( path . dirname ( outputPath ) ) ;
155
- copyFileSync ( inputPath , outputPath ) ;
156
- } ) ;
157
-
158
- logger . info ( `copied over (unchanged): ${ filesToCopy . length } files` ) ;
213
+ }
159
214
}
160
215
} ;
0 commit comments