/
broccoli-append.ts
191 lines (167 loc) · 6.51 KB
/
broccoli-append.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import Plugin, { Tree } from 'broccoli-plugin';
import { join } from 'path';
import walkSync from 'walk-sync';
import { unlinkSync, rmdirSync, mkdirSync, readFileSync, existsSync, writeFileSync, removeSync, readdirSync } from 'fs-extra';
import FSTree from 'fs-tree-diff';
import symlinkOrCopy from 'symlink-or-copy';
import uniqBy from 'lodash/uniqBy';
import { insertBefore} from './source-map-url';
/*
This is a fairly specialized broccoli transform that we use to get the output
of our webpack build added to the ember app. Mostly it's needed because we're
forced to run quite late and use the postprocessTree hook, rather than nicely
emit our content as part of treeForVendor, etc, which would be easier but
doesn't work because of whack data dependencies in new versions of ember-cli's
broccoli graph.
*/
export interface AppendOptions {
// map from a directory in the appendedTree (like `entrypoints/app`) to a file
// that may exists in the upstreamTree (like `assets/vendor.js`). Appends the
// JS files in the directory to that file, when it exists.
mappings: Map<string, string>;
// map from a directory in the appendedTree (like `lazy`) to a directory where
// we will output those files in the output (like `assets`).
passthrough: Map<string, string>;
}
export default class Append extends Plugin {
private previousUpstreamTree = new FSTree();
private previousAppendedTree = new FSTree();
private mappings: Map<string, string>;
private reverseMappings: Map<string, string>;
private passthrough: Map<string, string>;
constructor(upstreamTree: Tree, appendedTree: Tree, options: AppendOptions) {
super([upstreamTree, appendedTree], {
annotation: 'ember-auto-import-analyzer',
persistentOutput: true
});
let reverseMappings = new Map();
for (let [key, value] of options.mappings.entries( )) {
reverseMappings.set(value, key);
}
this.mappings = options.mappings;
this.reverseMappings = reverseMappings;
this.passthrough = options.passthrough;
}
private get upstreamDir() {
return this.inputPaths[0];
}
private get appendedDir() {
return this.inputPaths[1];
}
// returns the set of output files that should change based on changes to the
// appendedTree.
private diffAppendedTree() {
let changed = new Set();
let { patchset, passthroughEntries } = this.appendedPatchset();
for (let [, relativePath] of patchset) {
let [first] = relativePath.split('/');
if (this.mappings.has(first)) {
changed.add(this.mappings.get(first));
}
}
return { needsUpdate: changed, passthroughEntries };
}
build() {
// First note which output files should change due to changes in the
// appendedTree
let { needsUpdate, passthroughEntries } = this.diffAppendedTree();
// Then process all changes in the upstreamTree
for (let [operation, relativePath, entry] of this.upstreamPatchset(passthroughEntries)) {
let outputPath = join(this.outputPath, relativePath);
switch (operation) {
case 'unlink':
unlinkSync(outputPath);
break;
case 'rmdir':
rmdirSync(outputPath);
break;
case 'mkdir':
mkdirSync(outputPath);
break;
case 'change':
removeSync(outputPath);
// deliberate fallthrough
case 'create':
if (this.reverseMappings.has(relativePath)) {
// this is where we see the upstream original file being created or
// modified. We should always generate the complete appended file here.
this.handleAppend(relativePath);
// it no longer needs update once we've handled it here
needsUpdate.delete(relativePath);
} else {
if (entry.isPassthrough) {
symlinkOrCopy.sync(join(this.appendedDir, entry.originalRelativePath), outputPath);
} else {
symlinkOrCopy.sync(join(this.upstreamDir, relativePath), outputPath);
}
}
}
}
// finally, any remaining things in `needsUpdate` are cases where the
// appendedTree changed but the corresponding file in the upstreamTree
// didn't. Those needs to get handled here.
for (let relativePath of needsUpdate.values()) {
this.handleAppend(relativePath);
}
}
private upstreamPatchset(passthroughEntries) {
let input = walkSync.entries(this.upstreamDir).concat(passthroughEntries);
// FSTree requires the entries to be sorted and uniq
input.sort(compareByRelativePath);
input = uniqBy(input, e => (e as any).relativePath);
let previous = this.previousUpstreamTree;
let next = (this.previousUpstreamTree = FSTree.fromEntries(input));
return previous.calculatePatch(next);
}
private appendedPatchset() {
let input = walkSync.entries(this.appendedDir);
let passthroughEntries = input
.map(e => {
let first = e.relativePath.split('/')[0];
let remapped = this.passthrough.get(first);
if (remapped) {
let o = Object.create(e);
o.relativePath = e.relativePath.replace(new RegExp('^' + first), remapped);
o.isPassthrough = true;
o.originalRelativePath = e.relativePath;
return o;
}
}).filter(Boolean);
let previous = this.previousAppendedTree;
let next = (this.previousAppendedTree = FSTree.fromEntries(input));
return { patchset: previous.calculatePatch(next), passthroughEntries };
}
private handleAppend(relativePath) {
let upstreamPath = join(this.upstreamDir, relativePath);
let outputPath = join(this.outputPath, relativePath);
if (!existsSync(upstreamPath)) {
removeSync(outputPath);
return;
}
let sourceDir = join(this.appendedDir, this.reverseMappings.get(relativePath));
if (!existsSync(sourceDir)) {
symlinkOrCopy.sync(upstreamPath, outputPath);
return;
}
let appendedContent = readdirSync(sourceDir).map(name => {
if (/\.js$/.test(name)) {
return readFileSync(join(sourceDir, name), 'utf8');
}
}).filter(Boolean).join(";\n");
let upstreamContent = readFileSync(upstreamPath, 'utf8');
if (appendedContent.length > 0) {
upstreamContent = insertBefore(upstreamContent, ";\n" + appendedContent);
}
writeFileSync(outputPath, upstreamContent, 'utf8');
}
}
function compareByRelativePath(entryA, entryB) {
let pathA = entryA.relativePath;
let pathB = entryB.relativePath;
if (pathA < pathB) {
return -1;
} else if (pathA > pathB) {
return 1;
}
return 0;
}