11'use strict' ;
22
33const fs = require ( 'fs' ) ;
4- const mkdirp = require ( 'mkdirp' ) ;
5- const copyFileSync = require ( 'fs-copy-file-sync' ) ;
64const path = require ( 'path' ) ;
75const walkSync = require ( 'walk-sync' ) ;
86const Plugin = require ( 'broccoli-plugin' ) ;
97const logger = require ( 'heimdalljs-logger' ) ( 'ember-cli-htmlbars:colocated-broccoli-plugin' ) ;
8+ const FSTree = require ( 'fs-tree-diff' ) ;
109
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+ } ) ;
1415
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 ( [ ] ) ;
2017 }
2118
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 ;
2428 }
2529
26- return root ;
27- }
30+ currentEntries ( ) {
31+ return this . _lastTree . entries ;
32+ }
2833
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 ) ;
3236 }
3337
34- build ( ) {
35- let files = walkSync ( this . inputPaths [ 0 ] , { directories : false } ) ;
38+ detectRootName ( ) {
39+ let entries = this . currentEntries ( ) . filter ( e => ! e . isDirectory ( ) ) ;
3640
37- if ( files . length === 0 ) {
38- // nothing to do, bail
39- return ;
40- }
41+ let [ first ] = entries ;
42+ let parts = first . relativePath . split ( '/' ) ;
4143
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+ }
4350
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+ }
5254
53- let filePathParts = path . parse ( filePath ) ;
54- let inputPath = path . join ( this . inputPaths [ 0 ] , filePath ) ;
55+ return root ;
56+ }
5557
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 ( ) ;
6160
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+ }
6365
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 ;
6993 }
7094
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 ;
7498 }
99+ processedColocatedFiles . add ( basePath ) ;
75100
76101 let hasBackingClass = false ;
77- let backingClassPath = path . join ( filePathParts . dir , filePathParts . name ) ;
102+ let hasTemplate = this . inputHasFile ( basePath + '.hbs' ) ;
103+ let backingClassPath = basePath ;
78104
79- if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.js' ) ) ) {
105+ if ( this . inputHasFile ( basePath + '.js' ) ) {
80106 backingClassPath += '.js' ;
81107 hasBackingClass = true ;
82- } else if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.ts' ) ) ) {
108+ } else if ( this . inputHasFile ( basePath + '.ts' ) ) {
83109 backingClassPath += '.ts' ;
84110 hasBackingClass = true ;
85- } else if ( fs . existsSync ( path . join ( this . inputPaths [ 0 ] , backingClassPath + '.coffee' ) ) ) {
111+ } else if ( this . inputHasFile ( basePath + '.coffee' ) ) {
86112 backingClassPath += '.coffee' ;
87113 hasBackingClass = true ;
88114 } else {
89115 backingClassPath += '.js' ;
90116 hasBackingClass = false ;
91117 }
92118
93- let templateContents = fs . readFileSync ( inputPath , { encoding : 'utf8' } ) ;
119+ let originalJsContents = null ;
94120 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+ }
109141 }
110142
111- logger . debug (
112- `processing colocated template: ${ filePath } (template-only: ${ hasBackingClass } )`
113- ) ;
114-
115143 if ( hasBackingClass ) {
116144 // add the template, call setComponentTemplate
117145
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+ ) ;
121152
122153 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?` ;
124155 jsContents = `${ jsContents } \nthrow new Error(${ JSON . stringify ( message ) } );` ;
125156 prefix = '' ;
126157 }
@@ -132,29 +163,53 @@ module.exports = class ColocatedTemplateProcessor extends Plugin {
132163
133164 jsContents = prefix + jsContents ;
134165
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+ }
149212 }
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+ }
159214 }
160215} ;
0 commit comments