Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selective lazy compilation #9166

Merged
merged 9 commits into from
Aug 25, 2023
2 changes: 2 additions & 0 deletions packages/core/core/src/dumpGraphToGraphViz.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default async function dumpGraphToGraphViz(
if (node.value.isOptional) parts.push('optional');
if (node.value.specifierType === SpecifierType.url) parts.push('url');
if (node.hasDeferred) parts.push('deferred');
if (node.deferred) parts.push('deferred');
if (node.excluded) parts.push('excluded');
if (parts.length) label += ' (' + parts.join(', ') + ')';
if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`;
Expand Down Expand Up @@ -171,6 +172,7 @@ export default async function dumpGraphToGraphViz(
if (node.value.needsStableName) parts.push('stable name');
parts.push(node.value.name);
parts.push('bb:' + (node.value.bundleBehavior ?? 'null'));
if (node.value.isPlaceholder) parts.push('placeholder');
if (parts.length) label += ' (' + parts.join(', ') + ')';
if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`;
} else if (node.type === 'request') {
Expand Down
54 changes: 50 additions & 4 deletions packages/core/core/src/requests/AssetGraphRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {Diagnostic} from '@parcel/diagnostic';

import invariant from 'assert';
import nullthrows from 'nullthrows';
import {PromiseQueue, setEqual} from '@parcel/utils';
import {PromiseQueue, setEqual, isGlobMatch} from '@parcel/utils';
import {hashString} from '@parcel/hash';
import ThrowableDiagnostic from '@parcel/diagnostic';
import {Priority} from '../types';
Expand All @@ -29,7 +29,7 @@ import createEntryRequest from './EntryRequest';
import createTargetRequest from './TargetRequest';
import createAssetRequest from './AssetRequest';
import createPathRequest from './PathRequest';
import {type ProjectPath} from '../projectPath';
import {type ProjectPath, fromProjectPathRelative} from '../projectPath';
import dumpGraphToGraphViz from '../dumpGraphToGraphViz';
import {propagateSymbols} from '../SymbolPropagation';

Expand All @@ -39,6 +39,8 @@ type AssetGraphRequestInput = {|
optionsRef: SharedReference,
name: string,
shouldBuildLazily?: boolean,
lazyIncludes?: string[],
lazyExcludes?: string[],
requestedAssetIds?: Set<string>,
|};

Expand Down Expand Up @@ -111,6 +113,8 @@ export class AssetGraphBuilder {
name: string;
cacheKey: string;
shouldBuildLazily: boolean;
lazyIncludes: string[];
lazyExcludes: string[];
requestedAssetIds: Set<string>;
isSingleChangeRebuild: boolean;
assetGroupsWithRemovedParents: Set<NodeId>;
Expand All @@ -127,6 +131,8 @@ export class AssetGraphBuilder {
name,
requestedAssetIds,
shouldBuildLazily,
lazyIncludes,
lazyExcludes,
} = input;
let assetGraph = prevResult?.assetGraph ?? new AssetGraph();
assetGraph.safeToIncrementallyBundle = true;
Expand All @@ -148,6 +154,8 @@ export class AssetGraphBuilder {
this.name = name;
this.requestedAssetIds = requestedAssetIds ?? new Set();
this.shouldBuildLazily = shouldBuildLazily ?? false;
this.lazyIncludes = lazyIncludes ?? [];
this.lazyExcludes = lazyExcludes ?? [];
this.cacheKey = hashString(
`${PARCEL_VERSION}${name}${JSON.stringify(entries) ?? ''}${options.mode}`,
);
Expand Down Expand Up @@ -306,14 +314,43 @@ export class AssetGraphBuilder {
let childNode = nullthrows(this.assetGraph.getNode(childNodeId));

if (node.type === 'asset' && childNode.type === 'dependency') {
if (this.requestedAssetIds.has(node.value.id)) {
// This logic will set `node.requested` to `true` if the node is in the list of requested asset ids
// (i.e. this is an entry of a (probably) placeholder bundle that wasn't previously requested)
//
// Otherwise, if this node either is explicitly not requested, or has had it's requested attribute deleted,
// it will determine whether this node is an "async child" - that is, is it a (probably)
// dynamic import(). If so, it will explicitly have it's `node.requested` set to `false`
//
// If it's not requested, but it's not an async child then it's `node.requested` is deleted (undefined)

// by default with lazy compilation all nodes are lazy
let isNodeLazy = true;

// For conditional lazy building - if this node matches the `lazyInclude` globs that means we want
// only those nodes to be treated as lazy - that means if this node does _NOT_ match that glob, then we
// also consider it not lazy (so it gets marked as requested).
if (this.lazyIncludes.length > 0) {
isNodeLazy = isGlobMatch(
fromProjectPathRelative(node.value.filePath),
this.lazyIncludes,
);
}
// Excludes override includes, so a node is _not_ lazy if it is included in the exclude list.
if (this.lazyExcludes.length > 0 && isNodeLazy) {
isNodeLazy = !isGlobMatch(
fromProjectPathRelative(node.value.filePath),
this.lazyExcludes,
);
}

if (this.requestedAssetIds.has(node.value.id) || !isNodeLazy) {
node.requested = true;
} else if (!node.requested) {
let isAsyncChild = this.assetGraph
.getIncomingDependencies(node.value)
.every(dep => dep.isEntry || dep.priority !== Priority.sync);
if (isAsyncChild) {
node.requested = false;
node.requested = !isNodeLazy;
} else {
delete node.requested;
}
Expand All @@ -322,12 +359,21 @@ export class AssetGraphBuilder {
let previouslyDeferred = childNode.deferred;
childNode.deferred = node.requested === false;

// The child dependency node we're now evaluating should not be deferred if it's parent
// is explicitly not requested (requested = false, but not requested = undefined)
//
// if we weren't previously deferred but we are now, then this dependency node's parents should also
// be marked as deferred
//
// if we were previously deferred but we not longer are, then then all parents should no longer be
// deferred either
if (!previouslyDeferred && childNode.deferred) {
this.assetGraph.markParentsWithHasDeferred(childNodeId);
} else if (previouslyDeferred && !childNode.deferred) {
this.assetGraph.unmarkParentsWithHasDeferred(childNodeId);
}

// We `shouldVisitChild` if the childNode is not deferred
return !childNode.deferred;
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/core/src/requests/BundleGraphRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export default function createBundleGraphRequest(
entries: options.entries,
optionsRef,
shouldBuildLazily: options.shouldBuildLazily,
lazyIncludes: options.lazyIncludes,
lazyExcludes: options.lazyExcludes,
requestedAssetIds,
});
let {assetGraph, changedAssets, assetRequests} = await api.runRequest(
Expand Down
15 changes: 15 additions & 0 deletions packages/core/core/src/resolveOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ export default async function resolveOptions(
: undefined;

let shouldBuildLazily = initialOptions.shouldBuildLazily ?? false;
let lazyIncludes = initialOptions.lazyIncludes ?? [];
if (lazyIncludes.length > 0 && !shouldBuildLazily) {
throw new Error(
'Lazy includes can only be provided when lazy building is enabled',
);
}
let lazyExcludes = initialOptions.lazyExcludes ?? [];
if (lazyExcludes.length > 0 && !shouldBuildLazily) {
throw new Error(
'Lazy excludes can only be provided when lazy building is enabled',
);
}

let shouldContentHash =
initialOptions.shouldContentHash ?? initialOptions.mode === 'production';
if (shouldBuildLazily && shouldContentHash) {
Expand Down Expand Up @@ -140,6 +153,8 @@ export default async function resolveOptions(
shouldAutoInstall: initialOptions.shouldAutoInstall ?? false,
hmrOptions: initialOptions.hmrOptions ?? null,
shouldBuildLazily,
lazyIncludes,
lazyExcludes,
shouldBundleIncrementally: initialOptions.shouldBundleIncrementally ?? true,
shouldContentHash,
serveOptions: initialOptions.serveOptions
Expand Down
2 changes: 2 additions & 0 deletions packages/core/core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ export type ParcelOptions = {|
shouldContentHash: boolean,
serveOptions: ServerOptions | false,
shouldBuildLazily: boolean,
lazyIncludes: string[],
lazyExcludes: string[],
shouldBundleIncrementally: boolean,
shouldAutoInstall: boolean,
logLevel: LogLevel,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/core/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const DEFAULT_OPTIONS: ParcelOptions = {
hmrOptions: undefined,
shouldContentHash: true,
shouldBuildLazily: false,
lazyIncludes: [],
lazyExcludes: [],
shouldBundleIncrementally: true,
serveOptions: false,
mode: 'development',
Expand Down
141 changes: 141 additions & 0 deletions packages/core/integration-tests/test/lazy-compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,145 @@ describe('lazy compile', function () {
]);
subscription.unsubscribe();
});

it('should support includes for lazy compile', async () => {
const b = await bundler(
path.join(__dirname, '/integration/lazy-compile/index.js'),
{
shouldBuildLazily: true,
lazyIncludes: ['**/lazy-1*'],
mode: 'development',
shouldContentHash: false,
},
);

await removeDistDirectory();

const subscription = await b.watch();
let result = await getNextBuild(b);

// Expect the bundle graph to only contain `parallel-lazy-1` but not `lazy-1`s children as we're only including lazy-1 in lazy compilation
// `parallel-lazy-1` which wasn't requested.
assertBundles(result.bundleGraph, [
{
name: /^index.*/,
assets: ['index.js', 'bundle-url.js', 'cacheLoader.js', 'js-loader.js'],
},
{
// This will be a placeholder, but that info isn't available in the BundleGraph
assets: ['lazy-1.js'],
},
{
assets: ['parallel-lazy-1.js', 'esmodule-helpers.js'],
},
{
assets: ['parallel-lazy-2.js'],
},
]);

// ensure parallel-lazy was produced, as it isn't "included" in laziness..
assert(
await distDirIncludes([
'index.js',
/^parallel-lazy-1\./,
/^parallel-lazy-2\./,
]),
);

result = await result.requestBundle(
findBundle(result.bundleGraph, /lazy-1/),
);

// Since lazy-2 was not included it should've been built when lazy-1 was..
assert(
await distDirIncludes([
'index.js',
/^parallel-lazy-1\./,
/^parallel-lazy-2\./,
/^lazy-1\./,
/^lazy-2\./,
]),
);

subscription.unsubscribe();
});

it('should support excludes for lazy compile', async () => {
const b = await bundler(
path.join(__dirname, '/integration/lazy-compile/index.js'),
{
shouldBuildLazily: true,
lazyExcludes: ['**/lazy-*'],
mode: 'development',
shouldContentHash: false,
},
);

await removeDistDirectory();

const subscription = await b.watch();
let result = await getNextBuild(b);

result = await result.requestBundle(
findBundle(result.bundleGraph, /index.js/),
);

assertBundles(result.bundleGraph, [
{
name: /^index.*/,
assets: ['index.js', 'bundle-url.js', 'cacheLoader.js', 'js-loader.js'],
},
{
assets: ['lazy-1.js', 'esmodule-helpers.js'],
},
{
assets: ['lazy-2.js'],
},
{
// This will be a placeholder, but that info isn't available in the BundleGraph
assets: ['parallel-lazy-1.js'],
},
]);

// lazy-* is _excluded_ from lazy compilation so it should have been built, but parallel-lazy should not have

assert(await distDirIncludes(['index.js', /^lazy-1\./, /^lazy-2\./]));

subscription.unsubscribe();
});

it('should lazy compile properly when same module is used sync/async', async () => {
const b = await bundler(
path.join(__dirname, '/integration/lazy-compile/index-sync-async.js'),
{
shouldBuildLazily: true,
mode: 'development',
shouldContentHash: false,
},
);

await removeDistDirectory();

const subscription = await b.watch();
let result = await getNextBuild(b);
result = await result.requestBundle(
findBundle(result.bundleGraph, /^index-sync-async\./),
);
result = await result.requestBundle(
findBundle(result.bundleGraph, /^uses-static-component\./),
);
result = await result.requestBundle(
findBundle(result.bundleGraph, /^uses-static-component-async\./),
);
result = await result.requestBundle(
findBundle(result.bundleGraph, /^static-component\./),
);

let output = await run(result.bundleGraph);
assert.deepEqual(await output.default(), [
'static component',
'static component',
]);
subscription.unsubscribe();
});
});
17 changes: 14 additions & 3 deletions packages/core/parcel/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ let serve = program
)
.option('--watch-for-stdin', 'exit when stdin closes')
.option(
'--lazy',
'Build async bundles on demand, when requested in the browser',
'--lazy [includes]',
'Build async bundles on demand, when requested in the browser. Defaults to all async bundles, unless a comma separated list of source file globs is provided. Only async bundles whose entry points match these globs will be built lazily',
)
.option(
'--lazy-exclude <excludes>',
'Can only be used in combination with --lazy. Comma separated list of source file globs, async bundles whose entry points match these globs will not be built lazily',
)
.action(runCommand);

Expand Down Expand Up @@ -470,6 +474,11 @@ async function normalizeOptions(
}

let mode = command.name() === 'build' ? 'production' : 'development';

const normalizeIncludeExcludeList = (input?: string): string[] => {
if (typeof input !== 'string') return [];
return input.split(',').map(value => value.trim());
};
return {
shouldDisableCache: command.cache === false,
cacheDir: command.cacheDir,
Expand All @@ -483,7 +492,9 @@ async function normalizeOptions(
logLevel: command.logLevel,
shouldProfile: command.profile,
shouldTrace: command.trace,
shouldBuildLazily: command.lazy,
shouldBuildLazily: typeof command.lazy !== 'undefined',
lazyIncludes: normalizeIncludeExcludeList(command.lazy),
lazyExcludes: normalizeIncludeExcludeList(command.lazyExclude),
shouldBundleIncrementally:
process.env.PARCEL_INCREMENTAL_BUNDLING === 'false' ? false : true,
detailedReport:
Expand Down
2 changes: 2 additions & 0 deletions packages/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ export type InitialParcelOptions = {|
+shouldTrace?: boolean,
+shouldPatchConsole?: boolean,
+shouldBuildLazily?: boolean,
+lazyIncludes?: string[],
+lazyExcludes?: string[],
+shouldBundleIncrementally?: boolean,

+inputFS?: FileSystem,
Expand Down