Skip to content

Commit 642d9ca

Browse files
AGawrysdevongovett
andauthored
Default Bundler Contributor Notes (#9488)
* init add steps and initial text * more blurbs about steps without pictures * Intro * Add images folder and init examples * typos * add examples for targets and css merging * add more examples add more snippets of code * add manual bundle example * Update docs/DefaultBundler.md Typo Co-authored-by: Devon Govett <[email protected]> * Update docs/BundlerExamples.md Syntax fix Co-authored-by: Devon Govett <[email protected]> * fix references to experimental bundler --------- Co-authored-by: Devon Govett <[email protected]>
1 parent 855bbb6 commit 642d9ca

15 files changed

+895
-0
lines changed

β€Ždocs/BundlerExamples.md

+364
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
## Targets
2+
3+
To configure multiple targets, a user can specify them in their `package.json`
4+
5+
```json5
6+
"targets": {
7+
"main1": {
8+
"distDir": "./dist/main1",
9+
"source": "./src/main1/index.html",
10+
"publicUrl": "./"
11+
},
12+
"main2": {
13+
"distDir": "./dist/main2",
14+
"source": "./src/main2/index.html",
15+
"publicUrl": "./"
16+
}
17+
},
18+
```
19+
20+
This will then be interpreted from the `AssetGraph` into a map of targets, which contains the target as the key, and the value a map of entries to the dependencies with matching targets.
21+
22+
```
23+
[Target Map] {
24+
'/dist/main1' => Map(1) {
25+
Asset(/main1/index.html) => Dependency(null -> src/main1/index.html)
26+
},
27+
'dist/main2' => Map(1) {
28+
Asset(/main2/index.html) => Dependency(null -> src/main2/index.html)
29+
}
30+
```
31+
32+
`bundle()` is then called for **each** entry in the Map, creating two distinct `IdealGraphs`, and in this case, identical graphs. `createIdealGraph` skips any subtrees of another target.
33+
34+
```js
35+
if (!entries.has(node.value)) {
36+
actions.skipChildren();
37+
}
38+
```
39+
40+
The sample file tree below represents the output created by the final `bundleGraph`
41+
42+
```
43+
> dist
44+
> main1
45+
index.html
46+
index.a05w6.js
47+
shared.123.js
48+
> main2
49+
index.html
50+
index.b80d3.js
51+
shared.123.js
52+
```
53+
54+
_The full test case can be found in the `supports multiple dist targets` case in `html.js` integration test file._
55+
56+
## Two CSS imports
57+
58+
Here we'll go through what occurs in an example where two entries to a project import different css files.
59+
60+
Consider an entry with imports to two css files, and an async file importing 1 of them
61+
62+
<table><tr>
63+
<td>
64+
65+
```js
66+
//entry.js
67+
68+
import './main.css';
69+
70+
import './Foo/foo.css';
71+
import('./Foo');
72+
```
73+
74+
</td><td>
75+
76+
```js
77+
// Foo/foo.js
78+
79+
import './foo.css';
80+
81+
export default function () {
82+
return 'foo';
83+
}
84+
```
85+
86+
</td>
87+
</tr></table>
88+
89+
Given one `js` entry, an async `js` import, and two `css` imports, we generate the following Assetgraph.
90+
![image info](./BundlerGraphs/css-merging/AssetGraph-css-merging.png)
91+
92+
Below is the local bundleGraph generated after **Step Create Bundles**. **Node 1** is generated in the first step because it is specified as an entry to the project.
93+
94+
![image info](./BundlerGraphs/css-merging/bundleGraph-premerge.png)
95+
96+
After **Step Create Bundles**, we have generated a bundle per entry, code-split points, and type-change bundles. We have four bundles, two of which are **bundleGroups**.
97+
98+
```
99+
entry.js bundle -> BundleGroups [entry.js]
100+
foo.js bundle -> BundleGroups [foo.js]
101+
foo.css bundle -> BundleGroups [entry.js, index.js]
102+
main.css bundle -> BundleGroups [entry.js]
103+
```
104+
105+
In the state above, the `entry.js` bundle loads (or is connected to) two `.css` bundles, which is not correct. In order to maintain the constraint of one bundle of a different type per bundlegroup, we need to merge bundles together. However, merging `foo.css` and `main.css` will result in `index.js` over-fetching `main.css`
106+
107+
In order to maintain correctness, we may need to duplicate assets, and end up with the final `idealGraph` below.
108+
109+
![image info](./BundlerGraphs/css-merging/bundlegraph-postmerge.png)
110+
111+
_The full test case can be found in the `multi-css-multi-entry-bug/src/` integration test._
112+
113+
## Reused Bundle
114+
115+
Reused bundles are a special type of shared bundle. Consider the following code. (taken from the 'should reuse a bundle when its main asset (aka bundleroot) is imported synchronously' test case in `javascript.js`)
116+
117+
<table><tr>
118+
<td>
119+
120+
```js
121+
//index.js
122+
123+
import('./foo');
124+
import('./bar');
125+
```
126+
127+
</td>
128+
<td>
129+
130+
```js
131+
//a.js
132+
133+
import foo from './foo';
134+
```
135+
136+
</td>
137+
<td>
138+
139+
```js
140+
//bar.js
141+
import foo from './a';
142+
import bar from './b';
143+
import styles from './styles.css';
144+
import html from './local.html';
145+
```
146+
147+
</td>
148+
<td>
149+
150+
```js
151+
// foo.js
152+
153+
import a from './a';
154+
import b from './b';
155+
156+
export default a;
157+
```
158+
159+
</td>
160+
</tr></table>
161+
162+
We know we'll have bundles created for the entry, the two async imports, and the type change, which is reflected in the graph below. (A snapshot taken after **Step Create Bundles** )
163+
![image info](./BundlerGraphs/steps/create-bundles-bundleGraph.png)
164+
165+
But where do we place `a.js`, and `b.js` ? We will consult `reachableRoots`.
166+
167+
```
168+
// ReachableRoots
169+
foo => [a,b]
170+
bar => [a, b, foo]
171+
```
172+
173+
From the availability above, it should be clear that the best way to place `a` and `b` would be to place them into our existing `foo` bundle, and simply connect `bar` to it, since `bar` requires `foo` as well. That is exactly what we do.
174+
175+
![image info](./BundlerGraphs/steps/idealBundleGraph_final_reusedFoo.png)
176+
177+
### Reused Code Deep Dive
178+
179+
Here I will explain line by line how we actually place assets in the case of reused bundles.
180+
181+
During placement, we go through each asset, one by one, and determine the set of bundles it must be placed in.
182+
183+
```
184+
for (let i = 0; i < assets.length; i++) { ... }
185+
```
186+
187+
Then we handle placement for entries and manual shared assets, see [DefaultBundler.md](DefaultBundler.md) for a more detailed look at that section.
188+
189+
`ReachableNonEntries` is the set of bundleRoots needed by our asset, a, that are _not_ entries, isolated, etc.
190+
191+
We loop through them, searching for a `candidate`. Since we don't know which asset we will process first, we need to make sure we draw that connection between the bundles regardless of if we hit `a.js`, `b.js`, or `foo.js` first. There are two cases to consider.
192+
193+
1. Asset is a bundleRoot, in this case `foo.js`.
194+
2. Asset is not a bundleRoot, in this case `a.js` or `b.js`
195+
196+
In the first case, we simply draw an edge and delete the `candidate` from this asset's reachable. We must delete it because this loop does not terminate asset placement, if reachable was still populated, we would go on to try to place our asset in the remaining reachable bundleroots.
197+
198+
```js
199+
let reuseableBundleId = bundles.get(asset.id);
200+
201+
if (reuseableBundleId != null) { // asset is a bundleRoot
202+
reachable.delete(candidateId);
203+
bundleGraph.addEdge(candidateSourceBundleId, reuseableBundleId);
204+
205+
```
206+
207+
The second case is a bit more involved. Say we are trying to place `a.js`, we know `bar.js` and `foo.js` are both bundleRoots in our `reachable`, but we do not know which is a subtree of the other. i.e. which direction the edge should go. So we need to consult `reachableAssets`, which is an inverse mapping of `reachableRoots`. This exists because bitSets are not bidirectional.
208+
209+
So, we take the assets that are reachable from our candidate bundleRoot (in this case bar), and intersect it with our reachable.
210+
211+
```js
212+
reachableIntersection.intersect(
213+
reachableAssets[
214+
nullthrows(assetToBundleRootNodeId.get(candidateSourceBundleRoot))
215+
],
216+
);
217+
```
218+
219+
The above essentially translates to
220+
221+
```
222+
223+
reachable(a) ∩ reachable(candidateBundleRoot)
224+
225+
```
226+
227+
So when the `candidate = bar` and `asset` we are placing is `a`, we are able to intersect to get the actual reusable bundle, `foo.js`. Below is an example of the values we'd be intersecting in this particular test case, in the event that `a` is processed before `foo`.
228+
229+
```
230+
231+
reachableAssets
232+
foo => {a,b}
233+
bar => {foo,a,b}
234+
235+
reachableNonEntries of a
236+
a => {bar, foo}
237+
```
238+
239+
We draw an edge from our `reusableBundle` to our `otherCandidateId`
240+
241+
```js
242+
bundleGraph.addEdge(
243+
nullthrows(bundles.get(candidateSourceBundleRoot.id)),
244+
reusableBundleId,
245+
);
246+
```
247+
248+
_The full test case can be found in the `shared-bundle-single-source/` case in `javascript.js` integration test file._
249+
250+
## Manual Bundles
251+
252+
Manual Bundles override Parcel's automatic code splitting. Consider the code below, with the following config in `package.json`.
253+
254+
```json
255+
package.json:
256+
{
257+
"@parcel/bundler-default": {
258+
"manualSharedBundles": [{
259+
"name": "vendor",
260+
"root": "math/math.js",
261+
"assets": ["math/!(divide).js"]
262+
}]
263+
}
264+
}
265+
266+
```
267+
268+
From the above, the pertinent data structures will be populated as such:
269+
270+
```json
271+
manualSharedObject =
272+
{
273+
"name": "vendor",
274+
"root": "math/math.js",
275+
"assets": ["math/!(divide).js"]
276+
}
277+
278+
parentsToConfig =
279+
{
280+
"math/math.js": {
281+
"name": "vendor"
282+
"root": etc ...
283+
}
284+
}
285+
286+
manualAssetToConfig = {
287+
"project/math/math.js": {
288+
{manualSharedObject}
289+
},
290+
"project/add.js": {
291+
{manualSharedObject}
292+
},
293+
"project/subtract.js": {
294+
{manualSharedObject}
295+
}
296+
}
297+
```
298+
299+
This allows us to look up any asset's `manualSharedObject`.
300+
301+
Below are the relevant files, our root, `math.js`, and `index.js`, which imports it. Please refer to the full test case in `bundler.js` for all source code.
302+
303+
<table><tr>
304+
<td>
305+
<td>
306+
307+
```js
308+
//math:
309+
math.js:
310+
export * from './add';
311+
export * from './subtract';
312+
export * from './divide';
313+
```
314+
315+
</td>
316+
<td>
317+
318+
```js
319+
index.js:
320+
import {add, subtract, divide} from './math/math';
321+
sideEffectNoop(divide(subtract(add(1, 2), 3), 4));
322+
323+
```
324+
325+
</td>
326+
</tr></table>
327+
328+
After **Step Create Bundles**, we are left with two bundles, one bundle group. There are no manual bundles in the graph below because the config does not math any explicit code split point.
329+
330+
![image info](./BundlerGraphs/manual-bundles/msb_step1.png)
331+
332+
The remaining assets left to place during asset placement are, `math.js`, `add.js`, `subtract.js`, and `divide.js`. From `reachable` you can infer what Parcel would/should do. Simply place all remaining assets into `index.js`, right?
333+
334+
However, we've specified via config that we want `math` and its imports which match the glob `!(divide)` in one bundle with nothing else.
335+
336+
```
337+
//reachable
338+
339+
math => [index.js]
340+
add => [index.js]
341+
subtract => [index.js]
342+
divide => [index.js]
343+
```
344+
345+
So that is exactly what happens. Looking up assets in the `manualAssetToConfig`, we place them into their own bundle, and connect it via edge and property `sourceBundles`. Source bundles property is equivalent to `reachable`.
346+
![image info](./BundlerGraphs/manual-bundles/msb_stepfinal.png)
347+
348+
_The full test case can be found in the `bundler.js` test suite._
349+
350+
## Debugging Notes
351+
352+
There are many more intricate and complex cases than what I've discussed above within Parcel's test suite. To understand the algorithm fully, debugging and visualizing the idealGraph structure is extremely beneficial. To do so, you may add the following in between steps within `DefaultBundler.js`,
353+
354+
```
355+
dumpGraphToGraphViz(
356+
// $FlowFixMe
357+
bundleGraph,
358+
'IdealGraph-Step1',
359+
);
360+
```
361+
362+
and run your example test case with the command below.
363+
364+
` PARCEL_DUMP_GRAPHVIZ=1yarn test test/<testsuite>`
Loading
Loading
Loading
Loading
Loading
Loading
22.3 KB
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
Β (0)