UNPKG

9.72 kBJavaScriptView Raw
1// @flow strict-local
2
3import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill';
4import type WorkerFarm, {Handle} from '@parcel/workers';
5import type {Event} from '@parcel/watcher';
6import type {
7 Asset,
8 AssetGraphNode,
9 AssetRequestDesc,
10 ParcelOptions,
11 ValidationOpts,
12} from './types';
13import type ParcelConfig from './ParcelConfig';
14import type {RunRequestOpts} from './RequestTracker';
15import type {AssetGraphBuildRequest} from './requests';
16
17import EventEmitter from 'events';
18import nullthrows from 'nullthrows';
19import path from 'path';
20import {md5FromObject, md5FromString, PromiseQueue} from '@parcel/utils';
21import AssetGraph from './AssetGraph';
22import RequestTracker, {
23 RequestGraph,
24 generateRequestId,
25} from './RequestTracker';
26import {PARCEL_VERSION} from './constants';
27import {
28 EntryRequestRunner,
29 TargetRequestRunner,
30 AssetRequestRunner,
31 DepPathRequestRunner,
32} from './requests';
33
34import dumpToGraphViz from './dumpGraphToGraphViz';
35
36type Opts = {|
37 options: ParcelOptions,
38 config: ParcelConfig,
39 name: string,
40 entries?: Array<string>,
41 assetRequests?: Array<AssetRequestDesc>,
42 workerFarm: WorkerFarm,
43|};
44
45const requestPriorities: $ReadOnlyArray<$ReadOnlyArray<string>> = [
46 ['entry_request'],
47 ['target_request'],
48 ['dep_path_request', 'asset_request'],
49];
50
51export default class AssetGraphBuilder extends EventEmitter {
52 assetGraph: AssetGraph;
53 requestGraph: RequestGraph;
54 requestTracker: RequestTracker;
55 entryRequestRunner: EntryRequestRunner;
56 targetRequestRunner: TargetRequestRunner;
57 depPathRequestRunner: DepPathRequestRunner;
58 assetRequestRunner: AssetRequestRunner;
59 assetRequests: Array<AssetRequestDesc>;
60 runValidate: ValidationOpts => Promise<void>;
61 queue: PromiseQueue<mixed>;
62
63 changedAssets: Map<string, Asset> = new Map();
64 options: ParcelOptions;
65 config: ParcelConfig;
66 workerFarm: WorkerFarm;
67 cacheKey: string;
68
69 handle: Handle;
70
71 async init({
72 config,
73 options,
74 entries,
75 name,
76 assetRequests,
77 workerFarm,
78 }: Opts) {
79 this.options = options;
80 this.assetRequests = [];
81
82 let {minify, hot, scopeHoist} = options;
83 this.cacheKey = md5FromObject({
84 parcelVersion: PARCEL_VERSION,
85 name,
86 options: {minify, hot, scopeHoist},
87 entries,
88 });
89
90 this.queue = new PromiseQueue();
91
92 this.runValidate = workerFarm.createHandle('runValidate');
93 this.handle = workerFarm.createReverseHandle(() => {
94 // Do nothing, this is here because there is a bug in `@parcel/workers`
95 });
96
97 let changes = await this.readFromCache();
98 if (!changes) {
99 this.assetGraph = new AssetGraph();
100 this.requestGraph = new RequestGraph();
101 }
102
103 this.assetGraph.initOptions({
104 onNodeRemoved: node => this.handleNodeRemovedFromAssetGraph(node),
105 onIncompleteNode: node => this.handleIncompleteNode(node),
106 });
107
108 let assetGraph = this.assetGraph;
109 this.requestTracker = new RequestTracker({
110 graph: this.requestGraph,
111 });
112 let tracker = this.requestTracker;
113 this.entryRequestRunner = new EntryRequestRunner({
114 tracker,
115 options,
116 assetGraph,
117 });
118 this.targetRequestRunner = new TargetRequestRunner({
119 tracker,
120 options,
121 assetGraph,
122 });
123 this.assetRequestRunner = new AssetRequestRunner({
124 tracker,
125 options,
126 workerFarm,
127 assetGraph,
128 });
129 this.depPathRequestRunner = new DepPathRequestRunner({
130 tracker,
131 options,
132 config,
133 assetGraph,
134 });
135
136 if (changes) {
137 this.requestGraph.invalidateUnpredictableNodes();
138 this.requestTracker.respondToFSEvents(changes);
139 } else {
140 this.assetGraph.initialize({
141 entries,
142 assetGroups: assetRequests,
143 });
144 }
145 }
146
147 async build(
148 signal?: AbortSignal,
149 ): Promise<{|
150 assetGraph: AssetGraph,
151 changedAssets: Map<string, Asset>,
152 |}> {
153 let lastQueueError;
154 for (let currPriorities of requestPriorities) {
155 if (!this.requestTracker.hasInvalidRequests()) {
156 break;
157 }
158
159 let promises = [];
160 for (let request of this.requestTracker.getInvalidRequests()) {
161 // $FlowFixMe
162 let assetGraphBuildRequest: AssetGraphBuildRequest = (request: any);
163 if (currPriorities.includes(request.type)) {
164 promises.push(this.runRequest(assetGraphBuildRequest, {signal}));
165 }
166 }
167 await Promise.all(promises);
168 if (lastQueueError) {
169 throw lastQueueError;
170 }
171 this.queue.run().catch(e => {
172 lastQueueError = e;
173 });
174 }
175
176 if (this.assetGraph.hasIncompleteNodes()) {
177 for (let id of this.assetGraph.incompleteNodeIds) {
178 this.processIncompleteAssetGraphNode(
179 nullthrows(this.assetGraph.getNode(id)),
180 signal,
181 );
182 }
183
184 await this.queue.run();
185 }
186
187 dumpToGraphViz(this.assetGraph, 'AssetGraph');
188 dumpToGraphViz(this.requestGraph, 'RequestGraph');
189
190 let changedAssets = this.changedAssets;
191 this.changedAssets = new Map();
192
193 return {assetGraph: this.assetGraph, changedAssets: changedAssets};
194 }
195
196 async validate(): Promise<void> {
197 let promises = this.assetRequests.map(request =>
198 this.runValidate({request, options: this.options}),
199 );
200 this.assetRequests = [];
201 await Promise.all(promises);
202 }
203
204 async runRequest(request: AssetGraphBuildRequest, runOpts: RunRequestOpts) {
205 switch (request.type) {
206 case 'entry_request':
207 return this.entryRequestRunner.runRequest(request.request, runOpts);
208 case 'target_request':
209 return this.targetRequestRunner.runRequest(request.request, runOpts);
210 case 'dep_path_request':
211 return this.depPathRequestRunner.runRequest(request.request, runOpts);
212 case 'asset_request': {
213 this.assetRequests.push(request.request);
214 let result = await this.assetRequestRunner.runRequest(
215 request.request,
216 runOpts,
217 );
218 if (result != null) {
219 for (let asset of result.assets) {
220 this.changedAssets.set(asset.id, asset); // ? Is this right?
221 }
222 }
223 return result;
224 }
225 }
226 }
227
228 getCorrespondingRequest(node: AssetGraphNode) {
229 switch (node.type) {
230 case 'entry_specifier': {
231 let type = 'entry_request';
232 return {
233 type,
234 request: node.value,
235 id: generateRequestId(type, node.value),
236 };
237 }
238 case 'entry_file': {
239 let type = 'target_request';
240 return {
241 type,
242 request: node.value,
243 id: generateRequestId(type, node.value),
244 };
245 }
246 case 'dependency': {
247 let type = 'dep_path_request';
248 return {
249 type,
250 request: node.value,
251 id: generateRequestId(type, node.value),
252 };
253 }
254 case 'asset_group': {
255 let type = 'asset_request';
256 return {
257 type,
258 request: node.value,
259 id: generateRequestId(type, node.value),
260 };
261 }
262 }
263 }
264
265 processIncompleteAssetGraphNode(node: AssetGraphNode, signal: ?AbortSignal) {
266 let request = nullthrows(this.getCorrespondingRequest(node));
267 if (!this.requestTracker.isTracked(request.id)) {
268 this.queue
269 .add(() =>
270 this.runRequest(request, {
271 signal,
272 }),
273 )
274 .catch(() => {
275 // Do nothing, the individual promise is not being awaited, but the queue is and will throw
276 });
277 }
278 }
279
280 handleIncompleteNode(node: AssetGraphNode) {
281 this.processIncompleteAssetGraphNode(node);
282 }
283
284 handleNodeRemovedFromAssetGraph(node: AssetGraphNode) {
285 let request = this.getCorrespondingRequest(node);
286 if (request != null) {
287 this.requestTracker.untrackRequest(request.id);
288 }
289 }
290
291 respondToFSEvents(events: Array<Event>) {
292 return this.requestGraph.respondToFSEvents(events);
293 }
294
295 getWatcherOptions() {
296 let vcsDirs = ['.git', '.hg'].map(dir =>
297 path.join(this.options.projectRoot, dir),
298 );
299 let ignore = [this.options.cacheDir, ...vcsDirs];
300 return {ignore};
301 }
302
303 getCacheKeys() {
304 let assetGraphKey = md5FromString(`${this.cacheKey}:assetGraph`);
305 let requestGraphKey = md5FromString(`${this.cacheKey}:requestGraph`);
306 let snapshotKey = md5FromString(`${this.cacheKey}:snapshot`);
307 return {assetGraphKey, requestGraphKey, snapshotKey};
308 }
309
310 async readFromCache(): Promise<?Array<Event>> {
311 if (this.options.disableCache) {
312 return null;
313 }
314
315 let {assetGraphKey, requestGraphKey, snapshotKey} = this.getCacheKeys();
316 let assetGraph = await this.options.cache.get(assetGraphKey);
317 let requestGraph = await this.options.cache.get(requestGraphKey);
318
319 if (assetGraph && requestGraph) {
320 this.assetGraph = assetGraph;
321 this.requestGraph = requestGraph;
322
323 let opts = this.getWatcherOptions();
324 let snapshotPath = this.options.cache._getCachePath(snapshotKey, '.txt');
325 return this.options.inputFS.getEventsSince(
326 this.options.projectRoot,
327 snapshotPath,
328 opts,
329 );
330 }
331
332 return null;
333 }
334
335 async writeToCache() {
336 if (this.options.disableCache) {
337 return;
338 }
339
340 let {assetGraphKey, requestGraphKey, snapshotKey} = this.getCacheKeys();
341 await this.options.cache.set(assetGraphKey, this.assetGraph);
342 await this.options.cache.set(requestGraphKey, this.requestGraph);
343
344 let opts = this.getWatcherOptions();
345 let snapshotPath = this.options.cache._getCachePath(snapshotKey, '.txt');
346 await this.options.inputFS.writeSnapshot(
347 this.options.projectRoot,
348 snapshotPath,
349 opts,
350 );
351 }
352}