diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index c7fcab88421..d996e55ac9a 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -353,10 +353,12 @@ export default class AssetGraph extends ContentGraph { nodeId !== traversedNodeId ) { if (!ctx?.hasDeferred) { + this.safeToIncrementallyBundle = false; delete traversedNode.hasDeferred; } actions.skipChildren(); } else if (traversedNode.type === 'dependency') { + this.safeToIncrementallyBundle = false; traversedNode.hasDeferred = false; } else if (nodeId !== traversedNodeId) { actions.skipChildren(); diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 20287c606af..4587966830b 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -1936,6 +1936,10 @@ export default class BundleGraph { bundle.id + bundle.target.publicUrl + this.getContentHash(bundle), ); + if (bundle.isPlaceholder) { + hash.writeString('placeholder'); + } + let inlineBundles = this.getInlineBundles(bundle); for (let inlineBundle of inlineBundles) { hash.writeString(this.getContentHash(inlineBundle)); diff --git a/packages/core/integration-tests/test/integration/lazy-compile/index-sync-async.js b/packages/core/integration-tests/test/integration/lazy-compile/index-sync-async.js new file mode 100644 index 00000000000..368a1cfe006 --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/index-sync-async.js @@ -0,0 +1,11 @@ +export default () => { + return Promise.all([ +import('./uses-static-component').then(c => { + return c.default()(); +}), +import('./uses-static-component-async').then(c => { + return c.default(); +}).then(s => { + return s(); +})]); +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/index.html b/packages/core/integration-tests/test/integration/lazy-compile/index.html new file mode 100644 index 00000000000..cc0458f1020 --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + + + + \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/index.js b/packages/core/integration-tests/test/integration/lazy-compile/index.js new file mode 100644 index 00000000000..3304ffe5f5b --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/index.js @@ -0,0 +1,7 @@ +async function main() { + const m = await import('./lazy-1'); + await import('./parallel-lazy-1'); + return m.default(); +} + +main(); \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/lazy-1.js b/packages/core/integration-tests/test/integration/lazy-compile/lazy-1.js new file mode 100644 index 00000000000..ec37bb95e48 --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/lazy-1.js @@ -0,0 +1,4 @@ +export default async () => { + const { world } = await import('./lazy-2'); + return `Hello ${world}`; +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/lazy-2.js b/packages/core/integration-tests/test/integration/lazy-compile/lazy-2.js new file mode 100644 index 00000000000..803b6568518 --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/lazy-2.js @@ -0,0 +1 @@ +export const world = 'world'; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/package.json b/packages/core/integration-tests/test/integration/lazy-compile/package.json new file mode 100644 index 00000000000..8570bd9389a --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/package.json @@ -0,0 +1,3 @@ +{ + "private": true +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-1.js b/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-1.js new file mode 100644 index 00000000000..4c401def35b --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-1.js @@ -0,0 +1,4 @@ +export default async () => { + const m = await import('./parallel-lazy-2'); + return m.default; +}; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-2.js b/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-2.js new file mode 100644 index 00000000000..811c02f445f --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/parallel-lazy-2.js @@ -0,0 +1 @@ +export default 'parallel lazy 2'; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/static-component.js b/packages/core/integration-tests/test/integration/lazy-compile/static-component.js new file mode 100644 index 00000000000..5768758f61d --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/static-component.js @@ -0,0 +1 @@ +export default () => "static component"; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component-async.js b/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component-async.js new file mode 100644 index 00000000000..6ec3b912715 --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component-async.js @@ -0,0 +1,4 @@ +export default async () => { + const m = await import('./static-component'); + return m.default; +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component.js b/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component.js new file mode 100644 index 00000000000..f8429ea120f --- /dev/null +++ b/packages/core/integration-tests/test/integration/lazy-compile/uses-static-component.js @@ -0,0 +1,4 @@ +import staticComponent from "./static-component" +export default () => { + return staticComponent; +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/lazy-compile.js b/packages/core/integration-tests/test/lazy-compile.js new file mode 100644 index 00000000000..6e29ead1c27 --- /dev/null +++ b/packages/core/integration-tests/test/lazy-compile.js @@ -0,0 +1,123 @@ +import assert from 'assert'; +import path from 'path'; +import { + bundler, + outputFS, + distDir, + getNextBuild, + assertBundles, + removeDistDirectory, + run, +} from '@parcel/test-utils'; + +const findBundle = (bundleGraph, nameRegex) => { + return bundleGraph.getBundles().find(b => nameRegex.test(b.name)); +}; + +const distDirIncludes = async matches => { + const files = await outputFS.readdir(distDir); + for (const match of matches) { + if (typeof match === 'string') { + if (!files.some(file => file === match)) { + throw new Error( + `No file matching ${match} was found in ${files.join(', ')}`, + ); + } + } else { + if (!files.some(file => match.test(file))) { + throw new Error( + `No file matching ${match} was found in ${files.join(', ')}`, + ); + } + } + } + return true; +}; + +describe('lazy compile', function () { + it('should lazy compile', async function () { + const b = await bundler( + path.join(__dirname, '/integration/lazy-compile/index.js'), + { + shouldBuildLazily: true, + mode: 'development', + shouldContentHash: false, + }, + ); + + await removeDistDirectory(); + + const subscription = await b.watch(); + let result = await getNextBuild(b); + + // This simulates what happens if index.js is loaded as well as lazy-1 which loads lazy-2. + // While parallel-lazy-1 is also async imported by index.js, we pretend it wasn't requested (i.e. like + // if it was behind a different trigger). + result = await result.requestBundle( + findBundle(result.bundleGraph, /index.js/), + ); + result = await result.requestBundle( + findBundle(result.bundleGraph, /^lazy-1/), + ); + result = await result.requestBundle( + findBundle(result.bundleGraph, /^lazy-2/), + ); + + // Expect the bundle graph to contain the whole nest of lazy from `lazy-1`, but not + // `parallel-lazy-1` which wasn't requested. + assertBundles(result.bundleGraph, [ + { + assets: ['index.js', 'bundle-url.js', 'cacheLoader.js', 'js-loader.js'], + }, + { + assets: ['lazy-1.js', 'esmodule-helpers.js'], + }, + { + assets: ['lazy-2.js'], + }, + { + assets: ['parallel-lazy-1.js'], + }, + ]); + + subscription.unsubscribe(); + + // Ensure the files match the bundle graph - lazy-2 should've been produced as it was requested + assert(await distDirIncludes(['index.js', /^lazy-1\./, /^lazy-2\./])); + }); + + 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(); + }); +}); diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index 130c737754b..94813d7e9c8 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -357,14 +357,13 @@ function getLoaderRuntime({ // Importing of the other bundles will be handled by the bundle group entry. // Do the same thing in library mode for ES modules, as we are building for another bundler // and the imports for sibling bundles will be in the target bundle. - // Also do this when building lazily or the runtime itself could get deduplicated and only - // exist in the parent. This causes errors if an old version of the parent without the runtime - // is already loaded. - if ( - bundle.env.outputFormat === 'commonjs' || - bundle.env.isLibrary || - options.shouldBuildLazily - ) { + + // Previously we also did this when building lazily, however it seemed to cause issues in some cases. + // The original comment as to why is left here, in case a future traveller is trying to fix that issue: + // > [...] the runtime itself could get deduplicated and only exist in the parent. This causes errors if an + // > old version of the parent without the runtime + // > is already loaded. + if (bundle.env.outputFormat === 'commonjs' || bundle.env.isLibrary) { externalBundles = [mainBundle]; } else { // Otherwise, load the bundle group entry after the others. @@ -443,7 +442,9 @@ function getLoaderRuntime({ loaderModules.push(code); } - if (bundle.env.context === 'browser' && !options.shouldBuildLazily) { + // Similar to the comment above, this also used to be skipped when shouldBuildLazily was true, + // however it caused issues where a bundle group contained multiple bundles. + if (bundle.env.context === 'browser') { loaderModules.push( ...externalBundles // TODO: Allow css to preload resources as well