Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<script type="module" src="./a.js"></script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'a';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'b';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'c';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<script type="module" src="./index.js"></script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import a from './a';
import b from './b';
import c from './c';

result([a, b, c]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"@parcel/bundler-default": {
"minBundleSize": 0
},
"@parcel/packager-js": {
"unstable_asyncBundleRuntime": true
}
}
32 changes: 32 additions & 0 deletions packages/core/integration-tests/test/scope-hoisting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
8 changes: 8 additions & 0 deletions packages/packagers/js/src/ESMOutputFormat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
64 changes: 63 additions & 1 deletion packages/packagers/js/src/ScopeHoistingPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +74,7 @@ export class ScopeHoistingPackager {
bundleGraph: BundleGraph<NamedBundle>;
bundle: NamedBundle;
parcelRequireName: string;
useAsyncBundleRuntime: boolean;
outputFormat: OutputFormat;
isAsyncBundle: boolean;
globalNames: $ReadOnlySet<string>;
Expand Down Expand Up @@ -101,11 +102,13 @@ export class ScopeHoistingPackager {
bundleGraph: BundleGraph<NamedBundle>,
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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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}`;
}

Expand Down Expand Up @@ -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<Array<Asset>> {
let queue = new PromiseQueue({maxConcurrent: 32});
let wrapped = [];
Expand Down Expand Up @@ -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];
}

Expand Down Expand Up @@ -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(
Expand Down
63 changes: 63 additions & 0 deletions packages/packagers/js/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,69 @@ if (parcelRequire == null) {
}
`;

export const fnExpr = (
env: Environment,
params: Array<string>,
body: Array<string>,
): 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) {
Comment thread
mattcompiles marked this conversation as resolved.
$parcel$global.lb = new Set();
$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', 'r', 'd'],
Comment thread
mattcompiles marked this conversation as resolved.
['$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});
Expand Down
46 changes: 44 additions & 2 deletions packages/packagers/js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSPackagerConfig> {
// 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({
Expand Down Expand Up @@ -51,6 +92,7 @@ export default (new Packager({
bundleGraph,
bundle,
nullthrows(config).parcelRequireName,
nullthrows(config).unstable_asyncBundleRuntime,
)
: new DevPackager(
options,
Expand Down