Skip to content

Commit 1083df6

Browse files
author
Robert Jackson
authored
Merge pull request #380 from ember-cli/caching
Implement basic patching strategy.
2 parents 2a5bb8e + d4b4801 commit 1083df6

File tree

4 files changed

+906
-163
lines changed

4 files changed

+906
-163
lines changed

lib/colocated-broccoli-plugin.js

Lines changed: 153 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,157 @@
11
'use strict';
22

33
const fs = require('fs');
4-
const mkdirp = require('mkdirp');
5-
const copyFileSync = require('fs-copy-file-sync');
64
const path = require('path');
75
const walkSync = require('walk-sync');
86
const Plugin = require('broccoli-plugin');
97
const 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

Comments
 (0)