1 |
|
2 |
|
3 | import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill';
|
4 | import type WorkerFarm, {Handle} from '@parcel/workers';
|
5 | import type {Event} from '@parcel/watcher';
|
6 | import type {
|
7 | Asset,
|
8 | AssetGraphNode,
|
9 | AssetRequestDesc,
|
10 | ParcelOptions,
|
11 | ValidationOpts,
|
12 | } from './types';
|
13 | import type ParcelConfig from './ParcelConfig';
|
14 | import type {RunRequestOpts} from './RequestTracker';
|
15 | import type {AssetGraphBuildRequest} from './requests';
|
16 |
|
17 | import EventEmitter from 'events';
|
18 | import nullthrows from 'nullthrows';
|
19 | import path from 'path';
|
20 | import {md5FromObject, md5FromString, PromiseQueue} from '@parcel/utils';
|
21 | import AssetGraph from './AssetGraph';
|
22 | import RequestTracker, {
|
23 | RequestGraph,
|
24 | generateRequestId,
|
25 | } from './RequestTracker';
|
26 | import {PARCEL_VERSION} from './constants';
|
27 | import {
|
28 | EntryRequestRunner,
|
29 | TargetRequestRunner,
|
30 | AssetRequestRunner,
|
31 | DepPathRequestRunner,
|
32 | } from './requests';
|
33 |
|
34 | import dumpToGraphViz from './dumpGraphToGraphViz';
|
35 |
|
36 | type Opts = {|
|
37 | options: ParcelOptions,
|
38 | config: ParcelConfig,
|
39 | name: string,
|
40 | entries?: Array<string>,
|
41 | assetRequests?: Array<AssetRequestDesc>,
|
42 | workerFarm: WorkerFarm,
|
43 | |};
|
44 |
|
45 | const requestPriorities: $ReadOnlyArray<$ReadOnlyArray<string>> = [
|
46 | ['entry_request'],
|
47 | ['target_request'],
|
48 | ['dep_path_request', 'asset_request'],
|
49 | ];
|
50 |
|
51 | export 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 |
|
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 |
|
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);
|
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 |
|
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 | }
|