Skip to content

Commit

Permalink
Multiple css bundles in Entry bundle groups issue (#9023)
Browse files Browse the repository at this point in the history
* initial attempt to fix

* remedy resulting failing tests

* add alternative test case

* only duplicate css in case of needstablename and non perfect overlap of bundlegroups, revert test cases

* remote bitset import
  • Loading branch information
AGawrys committed Jan 18, 2024
1 parent 874dddf commit 6bebe54
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 4 deletions.
118 changes: 115 additions & 3 deletions packages/bundlers/default/src/DefaultBundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import {ContentGraph, Graph, BitSet, ALL_EDGE_TYPES} from '@parcel/graph';

import invariant from 'assert';
import {Bundler} from '@parcel/plugin';
import {setEqual, validateSchema, DefaultMap, globToRegex} from '@parcel/utils';
import {
setEqual,
setIntersect,
validateSchema,
DefaultMap,
globToRegex,
} from '@parcel/utils';
import logger from '@parcel/logger';
import nullthrows from 'nullthrows';
import path from 'path';
Expand Down Expand Up @@ -844,6 +850,89 @@ function createIdealGraph(
}
if (!shouldMerge) continue;
mergeBundle(nodeIdA, nodeIdB);
} else if (a.needsStableName || b.needsStableName) {
let overlap = new Set(bundleBbundleGroups);
setIntersect(overlap, bundleABundleGroups);
if (overlap.size > 0) {
// Two bundles are the same type and exist in the same bundle group but cannot be (fully) merged
// We must duplicate or create a new bundle in this case

let shouldCreateNewBundle =
overlap.size == bundleBbundleGroups.size ||
overlap.size == bundleABundleGroups.size
? false
: true;
if (shouldCreateNewBundle) {
// Neither bundle can be the a host since both have unique groups
// So we may need to generate a new bundle for the intersection instead
} else {
// If the overlap of bundleGroups is a subset, then we should duplicate the bundle
// that results in a correct graph
let hostBundle =
overlap.size == bundleBbundleGroups.size
? [nodeIdB, b, bundleBbundleGroups]
: [nodeIdA, a, bundleABundleGroups];
let duplicatedBundle =
overlap.size == bundleBbundleGroups.size
? [nodeIdA, a, bundleABundleGroups]
: [nodeIdB, b, bundleBbundleGroups];
for (let asset of duplicatedBundle[1].assets) {
hostBundle[1].assets.add(asset);
hostBundle[1].size += asset.stats.size;
}
for (let group of overlap) {
let bundleGroup = nullthrows(bundleGraph.getNode(group));
invariant(
bundleGroup != null && bundleGroup !== 'root',
'Something went wrong with accessing a bundleGroup',
);
// Patch up dependency bundleGraph
for (let depId of dependencyBundleGraph.getNodeIdsConnectedTo(
dependencyBundleGraph.getNodeIdByContentKey(
String(duplicatedBundle[0]),
),
ALL_EDGE_TYPES,
)) {
let depNode = dependencyBundleGraph.getNode(depId);
invariant(bundleGroup.mainEntryAsset != null);
if (
depNode &&
depNode.type === 'dependency' &&
depNode.value.sourcePath ==
bundleGroup.mainEntryAsset.filePath
) {
dependencyBundleGraph.addEdge(
depId,
dependencyBundleGraph.getNodeIdByContentKey(
String(hostBundle[0]),
),
dependencyPriorityEdges.parallel,
);

dependencyBundleGraph.removeEdge(
depId,
dependencyBundleGraph.getNodeIdByContentKey(
String(duplicatedBundle[0]),
),
dependencyPriorityEdges.parallel,
);
for (let asset of duplicatedBundle[1].assets) {
replaceAssetReference(
asset,
duplicatedBundle[1],
hostBundle[1],
depNode.value,
);
}
}
}
// This might be a referencing bundle, not necessarily the group, so we
detachFromBundleGroup(group, duplicatedBundle[0]);
}
}
//We might've changed the bundleGroups of A, which should be recalculated;
bundleABundleGroups = getBundleGroupsForBundle(nodeIdA);
}
}
}
}
Expand Down Expand Up @@ -1580,7 +1669,24 @@ function createIdealGraph(
);
}
}

function detachFromBundleGroup(groupId: NodeId, bundleId: NodeId) {
// This removes a particular bundle from the specfied bundleGroup
if (bundleGraph.hasEdge(groupId, bundleId)) {
bundleGraph.removeEdge(groupId, bundleId);
} else {
let referencingBundleId;
bundleGraph.traverse(nodeId => {
if (bundleGraph.hasEdge(nodeId, bundleId)) {
referencingBundleId = nodeId;
}
}, groupId);
invariant(
referencingBundleId != null,
'Expected to remove bundle from group but could not find it...',
);
bundleGraph.removeEdge(referencingBundleId, bundleId);
}
}
function deleteBundle(bundleRoot: BundleRoot) {
bundleGraph.removeNode(nullthrows(bundles.get(bundleRoot.id)));
bundleRoots.delete(bundleRoot);
Expand Down Expand Up @@ -1672,11 +1778,17 @@ function createIdealGraph(
bundleRoot: BundleRoot,
toReplace: Bundle,
replaceWith: Bundle,
dependency?: Dependency,
): void {
let replaceAssetReference = assetReference.get(bundleRoot).map(entry => {
let bundle = entry[1];
let dep = entry[0];
if (bundle == toReplace) {
return [entry[0], replaceWith];
if (dependency && dependency == dep) {
return [entry[0], replaceWith];
} else if (dependency == null) {
return [entry[0], replaceWith];
}
}
return entry;
});
Expand Down
52 changes: 52 additions & 0 deletions packages/core/integration-tests/test/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,59 @@ describe('css', () => {
css.indexOf('.e {') < css.indexOf('.a {'),
);
});
it('should place one css bundle per bundlegroup for naming reasons', async () => {
let b = await bundle(
path.join(__dirname, '/integration/multi-css-bug/src/entry.js'),
);

assertBundles(b, [
{
name: 'entry.js',
type: 'js',
assets: [
'bundle-url.js',
'cacheLoader.js',
'css-loader.js',
'entry.js',
'js-loader.js',
],
},
{
type: 'js',
assets: ['esmodule-helpers.js', 'index.js'],
},
{name: 'Foo.css', type: 'css', assets: ['foo.css']},
{name: 'entry.css', type: 'css', assets: ['foo.css', 'main.css']},
]);
});
it.skip('create a new css bundle to maintain one css bundle per bundlegroup constraint', async () => {
let b = await bundle(
path.join(
__dirname,
'/integration/multi-css-multi-entry-bug/src/entry.js',
),
);

assertBundles(b, [
{
name: 'entry.js',
type: 'js',
assets: [
'bundle-url.js',
'cacheLoader.js',
'css-loader.js',
'entry.js',
'js-loader.js',
],
},
{
type: 'js',
assets: ['esmodule-helpers.js', 'index.js'],
},
{name: 'Foo.css', type: 'css', assets: ['foo.css']},
{name: 'entry.css', type: 'css', assets: ['foo.css', 'main.css']},
]);
});
it('should support loading a CSS bundle along side dynamic imports', async () => {
let b = await bundle(
path.join(__dirname, '/integration/dynamic-css/index.js'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.foo {
outline: 1px solid red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './foo.css';

export default function () { return 'foo' };
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This repro was taken from https://github.com/parcel-bundler/parcel/issues/8813

// This file needs to import a CSS asset, so that `index.css` will be created.
import './main.css';

/*
Import the CSS out of the package; causes a bundle Group entry type error.
This import causes the asset in the lazy imported bundle below to also be
named `index.css`.
*/
import './Foo/foo.css';

/*
Due to the import above, the dynamic import CSS asset is also created as
`index.css`, resulting in a name collision with the `index.css` asset created
for this file.
If the import of 'foo.css' above is removed, the dynamic import CSS asset
will be named `Foo.<hash>.css` as expected, like its JS file.
If the import of 'main.css' above is removed, the dynamic import CSS asset
will be named `index.css` as expected, but there will be no name collision
because this file did not generate an `index.css` asset itself.
Also, if parcel is run with a cache, on the first execution the
AssertionError for the bundle group will be raised. However, on a second
execution, the AssertError will not occur, but the generated `index.css` will
only contain the content of `foo.css` and be missing the content of
`main.css`.
In a React app, the dynamic import occurs via React.lazy:
import {lazy} from 'react'
const foo = lazy(() => import('./Foo'));
*/
import('./Foo');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.main {
outline: 1px solid blue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.foo {
outline: 1px solid red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './foo.css';

export default function () { return 'foo' };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This repro was taken from https://github.com/parcel-bundler/parcel/issues/8813

import './main.css';

import './Foo/foo.css';

import('./Foo');
import('./index-sib');
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

import './main.css';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.main {
outline: 1px solid blue;
}
2 changes: 1 addition & 1 deletion packages/namers/default/src/DefaultNamer.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default (new Namer({
assert(
entryBundlesOfType.length === 1,
// Otherwise, we'd end up naming two bundles the same thing.
'Bundle group cannot have more than one entry bundle of the same type',
`Bundle group cannot have more than one entry bundle of the same type. The offending bundle type is ${entryBundlesOfType[0].type}`,
);
}

Expand Down

0 comments on commit 6bebe54

Please sign in to comment.