diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.html b/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.html
new file mode 100644
index 00000000000..ac8c6e251ad
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.js b/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.js
new file mode 100644
index 00000000000..0ed5c30080a
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/a.js
@@ -0,0 +1 @@
+export default 'a';
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/b.js b/packages/core/integration-tests/test/integration/bundle-queue-runtime/b.js
new file mode 100644
index 00000000000..a68ac2819dc
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/b.js
@@ -0,0 +1 @@
+export default 'b';
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/c.js b/packages/core/integration-tests/test/integration/bundle-queue-runtime/c.js
new file mode 100644
index 00000000000..37a4d86fac7
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/c.js
@@ -0,0 +1 @@
+export default 'c';
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.html b/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.html
new file mode 100644
index 00000000000..95526304133
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.js b/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.js
new file mode 100644
index 00000000000..36dc3214941
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/index.js
@@ -0,0 +1,5 @@
+import a from './a';
+import b from './b';
+import c from './c';
+
+result([a, b, c]);
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/package.json b/packages/core/integration-tests/test/integration/bundle-queue-runtime/package.json
new file mode 100644
index 00000000000..787e5fddbda
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/bundle-queue-runtime/package.json
@@ -0,0 +1,8 @@
+{
+ "@parcel/bundler-default": {
+ "minBundleSize": 0
+ },
+ "@parcel/packager-js": {
+ "unstable_asyncBundleRuntime": true
+ }
+}
\ No newline at end of file
diff --git a/packages/core/integration-tests/test/integration/bundle-queue-runtime/yarn.lock b/packages/core/integration-tests/test/integration/bundle-queue-runtime/yarn.lock
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js
index 91ac3df6921..af5be86b833 100644
--- a/packages/core/integration-tests/test/scope-hoisting.js
+++ b/packages/core/integration-tests/test/scope-hoisting.js
@@ -5861,4 +5861,36 @@ describe('scope hoisting', function () {
assert.equal(res, 'target');
});
});
+
+ it('should add experimental bundle queue runtime for out of order bundle execution', async function () {
+ let b = await bundle(
+ [
+ path.join(__dirname, 'integration/bundle-queue-runtime/index.html'),
+ path.join(__dirname, 'integration/bundle-queue-runtime/a.html'),
+ ],
+ {
+ mode: 'production',
+ defaultTargetOptions: {
+ shouldScopeHoist: true,
+ shouldOptimize: false,
+ outputFormat: 'esmodule',
+ },
+ },
+ );
+
+ let contents = await outputFS.readFile(
+ b.getBundles().find(b => /index.*\.js/.test(b.filePath)).filePath,
+ 'utf8',
+ );
+ assert(contents.includes('$parcel$global.rwr('));
+
+ let result;
+ await run(b, {
+ result: r => {
+ result = r;
+ },
+ });
+
+ assert.deepEqual(await result, ['a', 'b', 'c']);
+ });
});
diff --git a/packages/packagers/js/src/ESMOutputFormat.js b/packages/packagers/js/src/ESMOutputFormat.js
index aa53bdd02cc..ef79036c9cd 100644
--- a/packages/packagers/js/src/ESMOutputFormat.js
+++ b/packages/packagers/js/src/ESMOutputFormat.js
@@ -106,6 +106,14 @@ export class ESMOutputFormat implements OutputFormat {
lines++;
}
+ if (this.packager.shouldBundleQueue(this.packager.bundle)) {
+ // Should be last thing the bundle executes on intial eval
+ res += `\n$parcel$global.rlb(${JSON.stringify(
+ this.packager.bundle.publicId,
+ )})`;
+ lines++;
+ }
+
return [res, lines];
}
}
diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js
index 477bc179a7f..18da60fa952 100644
--- a/packages/packagers/js/src/ScopeHoistingPackager.js
+++ b/packages/packagers/js/src/ScopeHoistingPackager.js
@@ -27,7 +27,7 @@ import path from 'path';
import {ESMOutputFormat} from './ESMOutputFormat';
import {CJSOutputFormat} from './CJSOutputFormat';
import {GlobalOutputFormat} from './GlobalOutputFormat';
-import {prelude, helpers} from './helpers';
+import {prelude, helpers, bundleQueuePrelude, fnExpr} from './helpers';
import {replaceScriptDependencies, getSpecifier} from './utils';
// https://262.ecma-international.org/6.0/#sec-names-and-keywords
@@ -74,6 +74,7 @@ export class ScopeHoistingPackager {
bundleGraph: BundleGraph;
bundle: NamedBundle;
parcelRequireName: string;
+ useAsyncBundleRuntime: boolean;
outputFormat: OutputFormat;
isAsyncBundle: boolean;
globalNames: $ReadOnlySet;
@@ -101,11 +102,13 @@ export class ScopeHoistingPackager {
bundleGraph: BundleGraph,
bundle: NamedBundle,
parcelRequireName: string,
+ useAsyncBundleRuntime: boolean,
) {
this.options = options;
this.bundleGraph = bundleGraph;
this.bundle = bundle;
this.parcelRequireName = parcelRequireName;
+ this.useAsyncBundleRuntime = useAsyncBundleRuntime;
let OutputFormat = OUTPUT_FORMATS[this.bundle.env.outputFormat];
this.outputFormat = new OutputFormat(this);
@@ -202,6 +205,8 @@ export class ScopeHoistingPackager {
mainEntry = null;
}
+ let needsBundleQueue = this.shouldBundleQueue(this.bundle);
+
// If any of the entry assets are wrapped, call parcelRequire so they are executed.
for (let entry of entries) {
if (this.wrappedAssets.has(entry.id) && !this.isScriptEntry(entry)) {
@@ -210,13 +215,22 @@ export class ScopeHoistingPackager {
)});\n`;
let entryExports = entry.symbols.get('*')?.local;
+
if (
entryExports &&
entry === mainEntry &&
this.exportedSymbols.has(entryExports)
) {
+ invariant(
+ !needsBundleQueue,
+ 'Entry exports are not yet compaitble with async bundles',
+ );
res += `\nvar ${entryExports} = ${parcelRequire}`;
} else {
+ if (needsBundleQueue) {
+ parcelRequire = this.runWhenReady(this.bundle, parcelRequire);
+ }
+
res += `\n${parcelRequire}`;
}
@@ -264,6 +278,38 @@ export class ScopeHoistingPackager {
};
}
+ shouldBundleQueue(bundle: NamedBundle): boolean {
+ return (
+ this.useAsyncBundleRuntime &&
+ bundle.type === 'js' &&
+ bundle.bundleBehavior !== 'inline' &&
+ bundle.env.outputFormat === 'esmodule' &&
+ !bundle.env.isIsolated() &&
+ bundle.bundleBehavior !== 'isolated' &&
+ !this.bundleGraph.hasParentBundleOfType(bundle, 'js')
+ );
+ }
+
+ runWhenReady(bundle: NamedBundle, codeToRun: string): string {
+ let deps = this.bundleGraph
+ .getReferencedBundles(bundle)
+ .filter(b => this.shouldBundleQueue(b))
+ .map(b => b.publicId);
+
+ if (deps.length === 0) {
+ // If no deps we can safely execute immediately
+ return codeToRun;
+ }
+
+ let params = [
+ JSON.stringify(this.bundle.publicId),
+ fnExpr(this.bundle.env, [], [codeToRun]),
+ JSON.stringify(deps),
+ ];
+
+ return `$parcel$global.rwr(${params.join(', ')});`;
+ }
+
async loadAssets(): Promise> {
let queue = new PromiseQueue({maxConcurrent: 32});
let wrapped = [];
@@ -599,6 +645,14 @@ ${code}
this.needsPrelude = true;
}
+ if (
+ !shouldWrap &&
+ this.shouldBundleQueue(this.bundle) &&
+ this.bundle.getEntryAssets().some(entry => entry.id === asset.id)
+ ) {
+ code = this.runWhenReady(this.bundle, code);
+ }
+
return [code, sourceMap, lineCount];
}
@@ -1199,6 +1253,14 @@ ${code}
if (enableSourceMaps) {
lines += countLines(preludeCode) - 1;
}
+
+ if (this.shouldBundleQueue(this.bundle)) {
+ let bundleQueuePreludeCode = bundleQueuePrelude(this.bundle.env);
+ res += bundleQueuePreludeCode;
+ if (enableSourceMaps) {
+ lines += countLines(bundleQueuePreludeCode) - 1;
+ }
+ }
} else {
// Otherwise, get the current parcelRequire global.
res += `var parcelRequire = $parcel$global[${JSON.stringify(
diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js
index 581269db6eb..4d31038755b 100644
--- a/packages/packagers/js/src/helpers.js
+++ b/packages/packagers/js/src/helpers.js
@@ -32,6 +32,74 @@ if (parcelRequire == null) {
}
`;
+export const fnExpr = (
+ env: Environment,
+ params: Array,
+ body: Array,
+): string => {
+ let block = `{ ${body.join(' ')} }`;
+
+ if (env.supports('arrow-functions')) {
+ return `(${params.join(', ')}) => ${block}`;
+ }
+
+ return `function (${params.join(', ')}) ${block}`;
+};
+
+export const bundleQueuePrelude = (env: Environment): string => `
+if (!$parcel$global.lb) {
+ // Set of loaded bundles
+ $parcel$global.lb = new Set();
+ // Queue of bundles to execute once they're dep bundles are loaded
+ $parcel$global.bq = [];
+
+ // Register loaded bundle
+ $parcel$global.rlb = ${fnExpr(
+ env,
+ ['bundle'],
+ ['$parcel$global.lb.add(bundle);', '$parcel$global.pq();'],
+ )}
+
+ // Run when ready
+ $parcel$global.rwr = ${fnExpr(
+ env,
+ // b = bundle public id
+ // r = run function to execute the bundle entry
+ // d = list of dependent bundles this bundle requires before executing
+ ['b', 'r', 'd'],
+ ['$parcel$global.bq.push({b, r, d});', '$parcel$global.pq();'],
+ )}
+
+ // Process queue
+ $parcel$global.pq = ${fnExpr(
+ env,
+ [],
+ [
+ `var runnableEntry = $parcel$global.bq.find(${fnExpr(
+ env,
+ ['i'],
+ [
+ `return i.d.every(${fnExpr(
+ env,
+ ['dep'],
+ ['return $parcel$global.lb.has(dep);'],
+ )});`,
+ ],
+ )});`,
+ 'if (runnableEntry) {',
+ `$parcel$global.bq = $parcel$global.bq.filter(${fnExpr(
+ env,
+ ['i'],
+ ['return i.b !== runnableEntry.b;'],
+ )});`,
+ 'runnableEntry.r();',
+ '$parcel$global.pq();',
+ '}',
+ ],
+ )}
+}
+`;
+
const $parcel$export = `
function $parcel$export(e, n, v, s) {
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
diff --git a/packages/packagers/js/src/index.js b/packages/packagers/js/src/index.js
index 73f1ebc0aa6..f73e7d51ed7 100644
--- a/packages/packagers/js/src/index.js
+++ b/packages/packagers/js/src/index.js
@@ -2,24 +2,65 @@
import type {Async} from '@parcel/types';
import type SourceMap from '@parcel/source-map';
import {Packager} from '@parcel/plugin';
-import {replaceInlineReferences, replaceURLReferences} from '@parcel/utils';
+import {
+ replaceInlineReferences,
+ replaceURLReferences,
+ validateSchema,
+ type SchemaEntity,
+} from '@parcel/utils';
+import {encodeJSONKeyComponent} from '@parcel/diagnostic';
import {hashString} from '@parcel/hash';
import path from 'path';
import nullthrows from 'nullthrows';
import {DevPackager} from './DevPackager';
import {ScopeHoistingPackager} from './ScopeHoistingPackager';
+type JSPackagerConfig = {|
+ parcelRequireName: string,
+ unstable_asyncBundleRuntime: boolean,
+|};
+
+const CONFIG_SCHEMA: SchemaEntity = {
+ type: 'object',
+ properties: {
+ unstable_asyncBundleRuntime: {
+ type: 'boolean',
+ },
+ },
+ additionalProperties: false,
+};
+
export default (new Packager({
- async loadConfig({config, options}) {
+ async loadConfig({config, options}): Promise {
// Generate a name for the global parcelRequire function that is unique to this project.
// This allows multiple parcel builds to coexist on the same page.
let pkg = await config.getConfigFrom(
path.join(options.projectRoot, 'index'),
['package.json'],
);
+
+ let packageKey = '@parcel/packager-js';
+
+ if (pkg?.contents[packageKey]) {
+ validateSchema.diagnostic(
+ CONFIG_SCHEMA,
+ {
+ data: pkg?.contents[packageKey],
+ source: await options.inputFS.readFile(pkg.filePath, 'utf8'),
+ filePath: pkg.filePath,
+ prependKey: `/${encodeJSONKeyComponent(packageKey)}`,
+ },
+ packageKey,
+ `Invalid config for ${packageKey}`,
+ );
+ }
+
let name = pkg?.contents?.name ?? '';
return {
parcelRequireName: 'parcelRequire' + hashString(name).slice(-4),
+ unstable_asyncBundleRuntime: Boolean(
+ pkg?.contents[packageKey]?.unstable_asyncBundleRuntime,
+ ),
};
},
async package({
@@ -51,6 +92,7 @@ export default (new Packager({
bundleGraph,
bundle,
nullthrows(config).parcelRequireName,
+ nullthrows(config).unstable_asyncBundleRuntime,
)
: new DevPackager(
options,