UNPKG

8.68 kBJavaScriptView Raw
1// @flow strict-local
2
3import type {GraphVisitor} from '@parcel/types';
4import type {
5 Asset,
6 AssetGraphNode,
7 AssetGroup,
8 AssetGroupNode,
9 AssetNode,
10 Dependency,
11 DependencyNode,
12 Entry,
13 NodeId,
14 Target,
15} from './types';
16
17import invariant from 'assert';
18import crypto from 'crypto';
19import {md5FromObject} from '@parcel/utils';
20import Graph, {type GraphOpts} from './Graph';
21import {createDependency} from './Dependency';
22
23type AssetGraphOpts = {|
24 ...GraphOpts<AssetGraphNode>,
25 onIncompleteNode?: (node: AssetGraphNode) => mixed,
26 onNodeAdded?: (node: AssetGraphNode) => mixed,
27 onNodeRemoved?: (node: AssetGraphNode) => mixed,
28|};
29
30type InitOpts = {|
31 entries?: Array<string>,
32 targets?: Array<Target>,
33 assetGroups?: Array<AssetGroup>,
34|};
35
36type SerializedAssetGraph = {|
37 ...GraphOpts<AssetGraphNode>,
38 hash: ?string,
39|};
40
41export function nodeFromDep(dep: Dependency): DependencyNode {
42 return {
43 id: dep.id,
44 type: 'dependency',
45 value: dep,
46 };
47}
48
49export function nodeFromAssetGroup(
50 assetGroup: AssetGroup,
51 deferred: boolean = false,
52) {
53 return {
54 id: md5FromObject(assetGroup),
55 type: 'asset_group',
56 value: assetGroup,
57 deferred,
58 };
59}
60
61export function nodeFromAsset(asset: Asset) {
62 return {
63 id: asset.id,
64 type: 'asset',
65 value: asset,
66 };
67}
68
69export function nodeFromEntrySpecifier(entry: string) {
70 return {
71 id: 'entry_specifier:' + entry,
72 type: 'entry_specifier',
73 value: entry,
74 };
75}
76
77export function nodeFromEntryFile(entry: Entry) {
78 return {
79 id: 'entry_file:' + md5FromObject(entry),
80 type: 'entry_file',
81 value: entry,
82 };
83}
84
85// Types that are considered incomplete when they don't have a child node
86const INCOMPLETE_TYPES = [
87 'entry_specifier',
88 'entry_file',
89 'dependency',
90 'asset_group',
91];
92
93export default class AssetGraph extends Graph<AssetGraphNode> {
94 onNodeAdded: ?(node: AssetGraphNode) => mixed;
95 onNodeRemoved: ?(node: AssetGraphNode) => mixed;
96 onIncompleteNode: ?(node: AssetGraphNode) => mixed;
97 incompleteNodeIds: Set<NodeId> = new Set();
98 hash: ?string;
99
100 // $FlowFixMe
101 static deserialize(opts: SerializedAssetGraph): AssetGraph {
102 let res = new AssetGraph(opts);
103 res.incompleteNodeIds = opts.incompleteNodeIds;
104 res.hash = opts.hash;
105 return res;
106 }
107
108 // $FlowFixMe
109 serialize(): SerializedAssetGraph {
110 return {
111 ...super.serialize(),
112 incompleteNodeIds: this.incompleteNodeIds,
113 hash: this.hash,
114 };
115 }
116
117 initOptions({
118 onNodeAdded,
119 onNodeRemoved,
120 onIncompleteNode,
121 }: AssetGraphOpts = {}) {
122 this.onNodeAdded = onNodeAdded;
123 this.onNodeRemoved = onNodeRemoved;
124 this.onIncompleteNode = onIncompleteNode;
125 }
126
127 initialize({entries, assetGroups}: InitOpts) {
128 let rootNode = {id: '@@root', type: 'root', value: null};
129 this.setRootNode(rootNode);
130
131 let nodes = [];
132 if (entries) {
133 for (let entry of entries) {
134 let node = nodeFromEntrySpecifier(entry);
135 nodes.push(node);
136 }
137 } else if (assetGroups) {
138 nodes.push(
139 ...assetGroups.map(assetGroup => nodeFromAssetGroup(assetGroup)),
140 );
141 }
142 this.replaceNodesConnectedTo(rootNode, nodes);
143 }
144
145 addNode(node: AssetGraphNode) {
146 this.hash = null;
147 let existingNode = this.getNode(node.id);
148 if (
149 INCOMPLETE_TYPES.includes(node.type) &&
150 !node.complete &&
151 !node.deferred &&
152 (!existingNode || existingNode.deferred)
153 ) {
154 this.markIncomplete(node);
155 }
156 this.onNodeAdded && this.onNodeAdded(node);
157 return super.addNode(node);
158 }
159
160 removeNode(node: AssetGraphNode) {
161 this.hash = null;
162 this.incompleteNodeIds.delete(node.id);
163 this.onNodeRemoved && this.onNodeRemoved(node);
164 return super.removeNode(node);
165 }
166
167 markIncomplete(node: AssetGraphNode) {
168 this.incompleteNodeIds.add(node.id);
169 if (this.onIncompleteNode) {
170 this.onIncompleteNode(node);
171 }
172 }
173
174 hasIncompleteNodes() {
175 return this.incompleteNodeIds.size > 0;
176 }
177
178 resolveEntry(entry: string, resolved: Array<Entry>) {
179 let entrySpecifierNode = nodeFromEntrySpecifier(entry);
180 let entryFileNodes = resolved.map(file => nodeFromEntryFile(file));
181 this.replaceNodesConnectedTo(entrySpecifierNode, entryFileNodes);
182 this.incompleteNodeIds.delete(entrySpecifierNode.id);
183 }
184
185 resolveTargets(entry: Entry, targets: Array<Target>) {
186 let depNodes = targets.map(target =>
187 nodeFromDep(
188 createDependency({
189 moduleSpecifier: entry.filePath,
190 pipeline: target.name,
191 target: target,
192 env: target.env,
193 isEntry: true,
194 }),
195 ),
196 );
197
198 let entryNode = nodeFromEntryFile(entry);
199 if (this.hasNode(entryNode.id)) {
200 this.replaceNodesConnectedTo(entryNode, depNodes);
201 this.incompleteNodeIds.delete(entryNode.id);
202 }
203 }
204
205 resolveDependency(
206 dependency: Dependency,
207 assetGroupNode: AssetGroupNode | null,
208 ) {
209 let depNode = this.nodes.get(dependency.id);
210 if (!depNode) return;
211 this.incompleteNodeIds.delete(depNode.id);
212
213 if (assetGroupNode) {
214 this.replaceNodesConnectedTo(depNode, [assetGroupNode]);
215 }
216 }
217
218 resolveAssetGroup(assetGroup: AssetGroup, assets: Array<Asset>) {
219 let assetGroupNode = nodeFromAssetGroup(assetGroup);
220 this.incompleteNodeIds.delete(assetGroupNode.id);
221 if (!this.hasNode(assetGroupNode.id)) {
222 return;
223 }
224
225 let dependentAssetKeys = [];
226 let assetObjects: Array<{|
227 assetNode: AssetNode,
228 dependentAssets: Array<Asset>,
229 isDirect: boolean,
230 |}> = [];
231 for (let asset of assets) {
232 let isDirect = !dependentAssetKeys.includes(asset.uniqueKey);
233
234 let dependentAssets = [];
235 for (let dep of asset.dependencies.values()) {
236 let dependentAsset = assets.find(
237 a => a.uniqueKey === dep.moduleSpecifier,
238 );
239 if (dependentAsset) {
240 dependentAssetKeys.push(dependentAsset.uniqueKey);
241 dependentAssets.push(dependentAsset);
242 }
243 }
244 assetObjects.push({
245 assetNode: nodeFromAsset(asset),
246 dependentAssets,
247 isDirect,
248 });
249 }
250
251 this.replaceNodesConnectedTo(
252 assetGroupNode,
253 assetObjects.filter(a => a.isDirect).map(a => a.assetNode),
254 );
255 for (let {assetNode, dependentAssets} of assetObjects) {
256 this.resolveAsset(assetNode, dependentAssets);
257 }
258 }
259
260 resolveAsset(assetNode: AssetNode, dependentAssets: Array<Asset>) {
261 let depNodes = [];
262 let depNodesWithAssets = [];
263 for (let dep of assetNode.value.dependencies.values()) {
264 let depNode = nodeFromDep(dep);
265 depNodes.push(this.nodes.get(depNode.id) ?? depNode);
266 let dependentAsset = dependentAssets.find(
267 a => a.uniqueKey === dep.moduleSpecifier,
268 );
269 if (dependentAsset) {
270 depNode.complete = true;
271 depNodesWithAssets.push([depNode, nodeFromAsset(dependentAsset)]);
272 }
273 }
274 this.replaceNodesConnectedTo(assetNode, depNodes);
275
276 for (let [depNode, dependentAssetNode] of depNodesWithAssets) {
277 this.replaceNodesConnectedTo(depNode, [dependentAssetNode]);
278 }
279 }
280
281 getIncomingDependencies(asset: Asset): Array<Dependency> {
282 let node = this.getNode(asset.id);
283 if (!node) {
284 return [];
285 }
286
287 return this.findAncestors(node, node => node.type === 'dependency').map(
288 node => {
289 invariant(node.type === 'dependency');
290 return node.value;
291 },
292 );
293 }
294
295 traverseAssets<TContext>(
296 visit: GraphVisitor<Asset, TContext>,
297 startNode: ?AssetGraphNode,
298 ): ?TContext {
299 return this.filteredTraverse(
300 node => (node.type === 'asset' ? node.value : null),
301 visit,
302 startNode,
303 );
304 }
305
306 getEntryAssetGroupNodes(): Array<AssetGroupNode> {
307 let entryNodes = [];
308 this.traverse((node, _, actions) => {
309 if (node.type === 'asset_group') {
310 entryNodes.push(node);
311 actions.skipChildren();
312 }
313 });
314 return entryNodes;
315 }
316
317 getEntryAssets(): Array<Asset> {
318 let entries = [];
319 this.traverseAssets((asset, ctx, traversal) => {
320 entries.push(asset);
321 traversal.skipChildren();
322 });
323
324 return entries;
325 }
326
327 getHash() {
328 if (this.hash != null) {
329 return this.hash;
330 }
331
332 let hash = crypto.createHash('md5');
333 // TODO: sort??
334 this.traverse(node => {
335 if (node.type === 'asset') {
336 hash.update(node.value.outputHash);
337 } else if (node.type === 'dependency' && node.value.target) {
338 hash.update(JSON.stringify(node.value.target));
339 }
340 });
341
342 this.hash = hash.digest('hex');
343 return this.hash;
344 }
345}