Skip to content

Commit

Permalink
Add library bundler (#9489)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Mar 11, 2024
1 parent aeaba41 commit 75182a0
Show file tree
Hide file tree
Showing 12 changed files with 706 additions and 87 deletions.
26 changes: 26 additions & 0 deletions packages/bundlers/library/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@parcel/bundler-library",
"version": "2.11.0",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git"
},
"main": "lib/LibraryBundler.js",
"source": "src/LibraryBundler.js",
"engines": {
"node": ">= 12.0.0",
"parcel": "^2.12.0"
},
"dependencies": {
"@parcel/plugin": "2.12.0",
"nullthrows": "^1.1.1"
}
}
80 changes: 80 additions & 0 deletions packages/bundlers/library/src/LibraryBundler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @flow strict-local
import {Bundler} from '@parcel/plugin';
import nullthrows from 'nullthrows';

// This bundler plugin is designed specifically for library builds. It outputs a bundle for
// each input asset, which ensures that the library can be effectively tree shaken and code
// split by an application bundler.
export default (new Bundler({
bundle({bundleGraph}) {
// Collect dependencies from the graph.
// We do not want to mutate the graph while traversing, so this must be done first.
let dependencies = [];
bundleGraph.traverse((node, context) => {
if (node.type === 'dependency') {
let dependency = node.value;
if (bundleGraph.isDependencySkipped(dependency)) {
return;
}
dependencies.push([
dependency,
nullthrows(dependency.target ?? context),
]);
if (dependency.target) {
return dependency.target;
}
}
});

// Create bundles for each asset.
let bundles = new Map();
for (let [dependency, target] of dependencies) {
let assets = bundleGraph.getDependencyAssets(dependency);
if (assets.length === 0) {
continue;
}

let parentAsset = bundleGraph.getAssetWithDependency(dependency);
let parentBundle;
if (parentAsset) {
let parentKey = getBundleKey(parentAsset, target);
parentBundle = bundles.get(parentKey);
}
let bundleGroup;

// Create a separate bundle group/bundle for each asset.
for (let asset of assets) {
let key = getBundleKey(asset, target);
let bundle = bundles.get(key);
if (!bundle) {
bundleGroup ??= bundleGraph.createBundleGroup(dependency, target);
bundle = bundleGraph.createBundle({
entryAsset: asset,
needsStableName: dependency.isEntry,
target,
bundleBehavior: dependency.bundleBehavior ?? asset.bundleBehavior,
});
bundleGraph.addBundleToBundleGroup(bundle, bundleGroup);
bundles.set(key, bundle);
}

if (!bundle.hasAsset(asset)) {
bundleGraph.addAssetToBundle(asset, bundle);
}

// Reference the parent bundle so we create dependencies between them.
if (parentBundle) {
bundleGraph.createBundleReference(parentBundle, bundle);
bundleGraph.createAssetReference(dependency, asset, bundle);
}
}
}
},
optimize() {},
}): Bundler);

function getBundleKey(asset, target) {
// Group by type and file path so CSS generated by macros is combined together by parent JS file.
// Also group by environment/target to ensure bundles cannot be shared between packages.
return `${asset.type}:${asset.filePath}:${asset.env.id}:${target.distDir}`;
}
41 changes: 25 additions & 16 deletions packages/core/core/src/BundleGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,24 @@ export default class BundleGraph {
getReferencedBundle(dependency: Dependency, fromBundle: Bundle): ?Bundle {
let dependencyNodeId = this._graph.getNodeIdByContentKey(dependency.id);

// Find an attached bundle via a reference edge (e.g. from createAssetReference).
let bundleNodes = this._graph
.getNodeIdsConnectedFrom(
dependencyNodeId,
bundleGraphEdgeTypes.references,
)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle');

if (bundleNodes.length) {
let bundleNode =
bundleNodes.find(
b => b.type === 'bundle' && b.value.type === fromBundle.type,
) || bundleNodes[0];
invariant(bundleNode.type === 'bundle');
return bundleNode.value;
}

// If this dependency is async, there will be a bundle group attached to it.
let node = this._graph
.getNodeIdsConnectedFrom(dependencyNodeId)
Expand All @@ -831,20 +849,6 @@ export default class BundleGraph {
return mainEntryId != null && node.value.entryAssetId === mainEntryId;
});
}

// Otherwise, find an attached bundle via a reference edge (e.g. from createAssetReference).
let bundleNode = this._graph
.getNodeIdsConnectedFrom(
dependencyNodeId,
bundleGraphEdgeTypes.references,
)
.map(id => nullthrows(this._graph.getNode(id)))
.find(node => node.type === 'bundle');

if (bundleNode) {
invariant(bundleNode.type === 'bundle');
return bundleNode.value;
}
}

removeAssetGraphFromBundle(asset: Asset, bundle: Bundle) {
Expand Down Expand Up @@ -1142,19 +1146,24 @@ export default class BundleGraph {

// If a resolution still hasn't been found, return the first referenced asset.
if (resolved == null) {
let potential = [];
this._graph.traverse(
(nodeId, _, traversal) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'asset') {
resolved = node.value;
traversal.stop();
potential.push(node.value);
} else if (node.id !== dep.id) {
traversal.skipChildren();
}
},
this._graph.getNodeIdByContentKey(dep.id),
bundleGraphEdgeTypes.references,
);

if (bundle) {
resolved = potential.find(a => a.type === bundle.type);
}
resolved ||= potential[0];
}

return resolved;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
exports.test = true;
exports['foo-bar'] = true;
Empty file.
Loading

0 comments on commit 75182a0

Please sign in to comment.