Skip to content

Commit

Permalink
Parcel 2: BundleGraph and Default Bundler plugin (#2401)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Dec 16, 2018
1 parent af6661b commit d683eeb
Show file tree
Hide file tree
Showing 23 changed files with 757 additions and 116 deletions.
143 changes: 132 additions & 11 deletions packages/bundlers/default/src/DefaultBundler.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,139 @@
// @flow
import {Bundler} from '@parcel/plugin';

const ISOLATED_ENVS = new Set(['web-worker', 'service-worker']);
const OPTIONS = {
minBundles: 1,
minBundleSize: 30000,
maxParallelRequests: 5
};

export default new Bundler({
async bundle(graph) {
let assets = Array.from(graph.nodes.values())
.filter(node => node.type === 'asset')
.map(node => node.value);

return [
{
type: 'js',
filePath: 'bundle.js',
assets
bundle(assetGraph, bundleGraph) {
// RULES:
// 1. If dep.isAsync or dep.isEntry, start a new bundle group.
// 2. If an asset is a different type than the current bundle, make a parallel bundle in the same bundle group.
// 3. If an asset is already in a parent bundle in the same entry point, exclude from child bundles.
// 4. If an asset is only in separate isolated entry points (e.g. workers, different HTML pages), duplicate it.
// 5. If the sub-graph from an asset is >= 30kb, and the number of parallel requests in the bundle group is < 5, create a new bundle containing the sub-graph.
// 6. If two assets are always seen together, put them in the same extracted bundle.

// Step 1: create bundles for each of the explicit code split points.
assetGraph.traverse((node, context) => {
if (node.type === 'dependency') {
let dep = node.value;

// Start a new bundle if this is an async dependency, or entry point.
if (dep.isAsync || dep.isEntry) {
let isIsolated =
!context || dep.isEntry || ISOLATED_ENVS.has(dep.env.context);
let bundleGroup = {dependency: dep};
bundleGraph.addBundleGroup(
isIsolated ? null : context.bundle,
bundleGroup
);

return {bundleGroup};
}
} else if (node.type === 'asset') {
if (!context.bundle || node.value.type !== context.bundle.type) {
let bundle = assetGraph.createBundle(node.value);

// If there is a current bundle, but this asset is of a different type,
// separate it out into a parallel bundle in the same bundle group.
if (context.bundle) {
let bundles = bundleGraph.getBundles(context.bundleGroup);
let existingBundle = bundles.find(b => b.type === node.value.type);

// If there is an existing bundle of the asset's type, combine with that.
// Otherwise, a new bundle will be created.
if (existingBundle) {
existingBundle.assetGraph.merge(bundle.assetGraph);
return {bundleGroup: context.bundleGroup, bundle: existingBundle};
}
}

bundleGraph.addBundle(context.bundleGroup, bundle);
return {bundleGroup: context.bundleGroup, bundle};
}
}
});

// Step 2: remove assets that are duplicated in a parent bundle
bundleGraph.traverseBundles(bundle => {
let assetGraph = bundle.assetGraph;
assetGraph.traverseAssets(asset => {
if (bundleGraph.isAssetInAncestorBundle(bundle, asset)) {
assetGraph.removeAsset(asset);
}
});
});

// Step 3: Find duplicated assets in different bundle groups, and separate them into their own parallel bundles.
// If multiple assets are always seen together in the same bundles, combine them together.

let candidateBundles = new Map();

assetGraph.traverseAssets((asset, context, traversal) => {
// If this asset is duplicated in the minimum number of bundles, it is a candidate to be separated into its own bundle.
let bundles = bundleGraph.findBundlesWithAsset(asset);
if (bundles.length > OPTIONS.minBundles) {
let bundle = assetGraph.createBundle(asset);
let size = bundle.assetGraph.getTotalSize();

let id = bundles.map(b => b.id).join(':');
let candidate = candidateBundles.get(id);
if (!candidate) {
candidateBundles.set(id, {bundles, bundle, size});
} else {
candidate.size += size;
candidate.bundle.assetGraph.merge(bundle.assetGraph);
}

// Skip children from consideration since we added a parent already.
traversal.skipChildren();
}
});

// Sort candidates by size (consider larger bundles first), and ensure they meet the size threshold
let sortedCandidates = Array.from(candidateBundles.values())
.filter(bundle => bundle.size >= OPTIONS.minBundleSize)
.sort((a, b) => b.size - a.size);

for (let {bundle, bundles} of sortedCandidates) {
// Find all bundle groups connected to the original bundles
let bundleGroups = bundles.reduce(
(arr, bundle) => arr.concat(bundleGraph.getBundleGroups(bundle)),
[]
);

// Check that all the bundle groups are inside the parallel request limit.
if (
!bundleGroups.every(
group =>
bundleGraph.getBundles(group).length < OPTIONS.maxParallelRequests
)
) {
continue;
}
];

// Remove all of the root assets from each of the original bundles
for (let asset of bundle.assetGraph.getEntryAssets()) {
for (let bundle of bundles) {
bundle.assetGraph.removeAsset(asset);
}
}

// Create new bundle node and connect it to all of the original bundle groups
for (let bundleGroup of bundleGroups) {
bundleGraph.addBundle(bundleGroup, bundle);
}
}

bundleGraph.dumpGraphViz();

bundleGraph.traverseBundles(bundle => {
bundle.assetGraph.dumpGraphViz();
});
}
});
17 changes: 15 additions & 2 deletions packages/core/core/src/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Config,
PackageJSON
} from '@parcel/types';
import type Cache from '@parcel/cache';
import md5 from '@parcel/utils/md5';
import {loadConfig} from '@parcel/utils/config';
import createDependency from './createDependency';
Expand All @@ -28,7 +29,8 @@ type AssetOptions = {
connectedFiles?: Array<File>,
output?: AssetOutput,
env: Environment,
meta?: JSONObject
meta?: JSONObject,
cache?: Cache
};

export default class Asset implements IAsset {
Expand All @@ -41,8 +43,10 @@ export default class Asset implements IAsset {
dependencies: Array<Dependency>;
connectedFiles: Array<File>;
output: AssetOutput;
outputSize: number;
env: Environment;
meta: JSONObject;
#cache; // no type annotation because prettier dies...

constructor(options: AssetOptions) {
this.id =
Expand All @@ -60,8 +64,10 @@ export default class Asset implements IAsset {
? options.connectedFiles.slice()
: [];
this.output = options.output || {code: this.code};
this.outputSize = this.output.code.length;
this.env = options.env;
this.meta = options.meta || {};
this.#cache = options.cache;
}

toJSON(): AssetOptions {
Expand All @@ -74,6 +80,7 @@ export default class Asset implements IAsset {
dependencies: this.dependencies,
connectedFiles: this.connectedFiles,
output: this.output,
outputSize: this.outputSize,
env: this.env,
meta: this.meta
};
Expand Down Expand Up @@ -101,7 +108,7 @@ export default class Asset implements IAsset {
}

createChildAsset(result: TransformerResult) {
let code = result.code || (result.output && result.output.code) || '';
let code = (result.output && result.output.code) || result.code || '';
let opts: AssetOptions = {
hash: this.hash || md5(code),
filePath: this.filePath,
Expand All @@ -111,6 +118,7 @@ export default class Asset implements IAsset {
env: mergeEnvironment(this.env, result.env),
dependencies: this.dependencies,
connectedFiles: this.connectedFiles,
output: result.output,
meta: Object.assign({}, this.meta, result.meta)
};

Expand All @@ -132,6 +140,11 @@ export default class Asset implements IAsset {
}

async getOutput() {
if (this.#cache) {
await this.#cache.readBlobs(this);
this.#cache = null;
}

return this.output;
}

Expand Down
Loading

0 comments on commit d683eeb

Please sign in to comment.