1 |
|
2 |
|
3 | import type {GraphVisitor} from '@parcel/types';
|
4 | import 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 |
|
17 | import invariant from 'assert';
|
18 | import crypto from 'crypto';
|
19 | import {md5FromObject} from '@parcel/utils';
|
20 | import Graph, {type GraphOpts} from './Graph';
|
21 | import {createDependency} from './Dependency';
|
22 |
|
23 | type AssetGraphOpts = {|
|
24 | ...GraphOpts<AssetGraphNode>,
|
25 | onIncompleteNode?: (node: AssetGraphNode) => mixed,
|
26 | onNodeAdded?: (node: AssetGraphNode) => mixed,
|
27 | onNodeRemoved?: (node: AssetGraphNode) => mixed,
|
28 | |};
|
29 |
|
30 | type InitOpts = {|
|
31 | entries?: Array<string>,
|
32 | targets?: Array<Target>,
|
33 | assetGroups?: Array<AssetGroup>,
|
34 | |};
|
35 |
|
36 | type SerializedAssetGraph = {|
|
37 | ...GraphOpts<AssetGraphNode>,
|
38 | hash: ?string,
|
39 | |};
|
40 |
|
41 | export function nodeFromDep(dep: Dependency): DependencyNode {
|
42 | return {
|
43 | id: dep.id,
|
44 | type: 'dependency',
|
45 | value: dep,
|
46 | };
|
47 | }
|
48 |
|
49 | export 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 |
|
61 | export function nodeFromAsset(asset: Asset) {
|
62 | return {
|
63 | id: asset.id,
|
64 | type: 'asset',
|
65 | value: asset,
|
66 | };
|
67 | }
|
68 |
|
69 | export function nodeFromEntrySpecifier(entry: string) {
|
70 | return {
|
71 | id: 'entry_specifier:' + entry,
|
72 | type: 'entry_specifier',
|
73 | value: entry,
|
74 | };
|
75 | }
|
76 |
|
77 | export function nodeFromEntryFile(entry: Entry) {
|
78 | return {
|
79 | id: 'entry_file:' + md5FromObject(entry),
|
80 | type: 'entry_file',
|
81 | value: entry,
|
82 | };
|
83 | }
|
84 |
|
85 |
|
86 | const INCOMPLETE_TYPES = [
|
87 | 'entry_specifier',
|
88 | 'entry_file',
|
89 | 'dependency',
|
90 | 'asset_group',
|
91 | ];
|
92 |
|
93 | export 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 |
|
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 |
|
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 |
|
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 | }
|