1 | import Plugin, { Tree } from 'broccoli-plugin';
|
2 | import { join, extname } from 'path';
|
3 | import walkSync, { WalkSyncEntry } from 'walk-sync';
|
4 | import { unlinkSync, rmdirSync, mkdirSync, readFileSync, existsSync, writeFileSync, removeSync, readdirSync } from 'fs-extra';
|
5 | import FSTree from 'fs-tree-diff';
|
6 | import symlinkOrCopy from 'symlink-or-copy';
|
7 | import uniqBy from 'lodash/uniqBy';
|
8 | import { insertBefore} from './source-map-url';
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | export interface AppendOptions {
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | mappings: Map<string, Map<string, string>>;
|
25 |
|
26 |
|
27 |
|
28 | passthrough: Map<string, string>;
|
29 | }
|
30 |
|
31 | export default class Append extends Plugin {
|
32 | private previousUpstreamTree = new FSTree();
|
33 | private previousAppendedTree = new FSTree();
|
34 | private mappings: Map<string, Map<string, string>>;
|
35 | private reverseMappings: Map<string, string>;
|
36 | private passthrough: Map<string, string>;
|
37 |
|
38 | constructor(upstreamTree: Tree, appendedTree: Tree, options: AppendOptions) {
|
39 | super([upstreamTree, appendedTree], {
|
40 | annotation: 'ember-auto-import-analyzer',
|
41 | persistentOutput: true
|
42 | });
|
43 |
|
44 |
|
45 |
|
46 | let reverseMappings = new Map();
|
47 | for (let [key, map] of options.mappings.entries( )) {
|
48 | for (let value of map.values( )) {
|
49 | reverseMappings.set(value, key);
|
50 | }
|
51 | }
|
52 |
|
53 | this.mappings = options.mappings;
|
54 | this.reverseMappings = reverseMappings;
|
55 | this.passthrough = options.passthrough;
|
56 | }
|
57 |
|
58 | private get upstreamDir() {
|
59 | return this.inputPaths[0];
|
60 | }
|
61 |
|
62 | private get appendedDir() {
|
63 | return this.inputPaths[1];
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 | private diffAppendedTree() {
|
69 | let changed = new Set();
|
70 | let { patchset, passthroughEntries } = this.appendedPatchset();
|
71 | for (let [, relativePath] of patchset) {
|
72 | let match = findByPrefix(relativePath, this.mappings);
|
73 | if (match) {
|
74 | let ext = extname(relativePath).slice(1);
|
75 | if (match.mapsTo.has(ext)) {
|
76 | changed.add(match.mapsTo.get(ext));
|
77 | }
|
78 | }
|
79 | }
|
80 | return { needsUpdate: changed, passthroughEntries };
|
81 | }
|
82 |
|
83 | build() {
|
84 |
|
85 |
|
86 | let { needsUpdate, passthroughEntries } = this.diffAppendedTree();
|
87 |
|
88 |
|
89 | for (let [operation, relativePath, entry] of this.upstreamPatchset(passthroughEntries)) {
|
90 | let outputPath = join(this.outputPath, relativePath);
|
91 | switch (operation) {
|
92 | case 'unlink':
|
93 | unlinkSync(outputPath);
|
94 | break;
|
95 | case 'rmdir':
|
96 | rmdirSync(outputPath);
|
97 | break;
|
98 | case 'mkdir':
|
99 | mkdirSync(outputPath);
|
100 | break;
|
101 | case 'change':
|
102 | removeSync(outputPath);
|
103 |
|
104 | case 'create':
|
105 | if (this.reverseMappings.has(relativePath)) {
|
106 |
|
107 |
|
108 | this.handleAppend(relativePath);
|
109 |
|
110 | needsUpdate.delete(relativePath);
|
111 | } else {
|
112 | if (isPassthrough(entry)) {
|
113 | symlinkOrCopy.sync(join(this.appendedDir, entry.originalRelativePath), outputPath);
|
114 | } else {
|
115 | symlinkOrCopy.sync(join(this.upstreamDir, relativePath), outputPath);
|
116 | }
|
117 | }
|
118 | }
|
119 | }
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | for (let relativePath of needsUpdate.values()) {
|
125 | this.handleAppend(relativePath);
|
126 | }
|
127 | }
|
128 |
|
129 | private upstreamPatchset(passthroughEntries: AugmentedWalkSyncEntry[]) {
|
130 | let input: AugmentedWalkSyncEntry[] = walkSync.entries(this.upstreamDir).concat(passthroughEntries);
|
131 |
|
132 |
|
133 | input.sort(compareByRelativePath);
|
134 | input = uniqBy(input, e => (e as any).relativePath);
|
135 |
|
136 | let previous = this.previousUpstreamTree;
|
137 | let next = (this.previousUpstreamTree = FSTree.fromEntries(input));
|
138 | return previous.calculatePatch(next) as [ string, string, AugmentedWalkSyncEntry ][];
|
139 | }
|
140 |
|
141 | private appendedPatchset() {
|
142 | let input = walkSync.entries(this.appendedDir);
|
143 | let passthroughEntries = input
|
144 | .map(e => {
|
145 | let match = findByPrefix(e.relativePath, this.passthrough);
|
146 | if (match) {
|
147 | let o = Object.create(e);
|
148 | o.relativePath = e.relativePath.replace(new RegExp('^' + match.prefix), match.mapsTo);
|
149 | o.isPassthrough = true;
|
150 | o.originalRelativePath = e.relativePath;
|
151 | return o;
|
152 | }
|
153 | }).filter(e => e && e.relativePath !== './') as AugmentedWalkSyncEntry[];
|
154 |
|
155 | let previous = this.previousAppendedTree;
|
156 | let next = (this.previousAppendedTree = FSTree.fromEntries(input));
|
157 | return { patchset: previous.calculatePatch(next), passthroughEntries };
|
158 | }
|
159 |
|
160 | private handleAppend(relativePath: string) {
|
161 | let upstreamPath = join(this.upstreamDir, relativePath);
|
162 | let outputPath = join(this.outputPath, relativePath);
|
163 | let ext = extname(relativePath);
|
164 |
|
165 | if (!existsSync(upstreamPath)) {
|
166 | removeSync(outputPath);
|
167 | return;
|
168 | }
|
169 |
|
170 | let sourceDir = join(this.appendedDir, this.reverseMappings.get(relativePath)!);
|
171 | if (!existsSync(sourceDir)) {
|
172 | symlinkOrCopy.sync(upstreamPath, outputPath);
|
173 | return;
|
174 | }
|
175 |
|
176 | let appendedContent = readdirSync(sourceDir).map(name => {
|
177 | if (name.endsWith(ext)) {
|
178 | return readFileSync(join(sourceDir, name), 'utf8');
|
179 | }
|
180 | }).filter(Boolean).join(";\n");
|
181 | let upstreamContent = readFileSync(upstreamPath, 'utf8');
|
182 | if (appendedContent.length > 0) {
|
183 | upstreamContent = insertBefore(upstreamContent, ";\n" + appendedContent);
|
184 | }
|
185 | writeFileSync(outputPath, upstreamContent, 'utf8');
|
186 | }
|
187 | }
|
188 |
|
189 | function compareByRelativePath(entryA: WalkSyncEntry, entryB: WalkSyncEntry) {
|
190 | let pathA = entryA.relativePath;
|
191 | let pathB = entryB.relativePath;
|
192 |
|
193 | if (pathA < pathB) {
|
194 | return -1;
|
195 | } else if (pathA > pathB) {
|
196 | return 1;
|
197 | }
|
198 | return 0;
|
199 | }
|
200 |
|
201 | function isPassthrough(entry: AugmentedWalkSyncEntry): entry is PassthroughEntry {
|
202 | return (entry as any).isPassthrough;
|
203 | }
|
204 |
|
205 | interface PassthroughEntry extends WalkSyncEntry {
|
206 | isPassthrough: true;
|
207 | originalRelativePath: string;
|
208 | }
|
209 |
|
210 | type AugmentedWalkSyncEntry = WalkSyncEntry | PassthroughEntry;
|
211 |
|
212 | function findByPrefix<T>(path: string, map: Map<string, T>) {
|
213 | let parts = path.split('/');
|
214 | for (let i = 1; i < parts.length; i++) {
|
215 | let candidate = parts.slice(0, i).join('/');
|
216 | if (map.has(candidate)) {
|
217 | return {
|
218 | prefix: candidate,
|
219 | mapsTo: map.get(candidate)!
|
220 | };
|
221 | }
|
222 | }
|
223 | }
|