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

Add import helper to decrease ESM loader runtime footprint #9148

Merged
merged 10 commits into from
Jul 27, 2023
13 changes: 11 additions & 2 deletions packages/core/integration-tests/test/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -1802,7 +1802,13 @@ describe('html', function () {
},
{
type: 'js',
assets: ['bundle-manifest.js', 'index.js', 'index.js', 'index.js'],
assets: [
'bundle-manifest.js',
'esm-js-loader.js',
'index.js',
'index.js',
'index.js',
],
},
{
name: 'index.html',
Expand Down Expand Up @@ -1883,6 +1889,7 @@ describe('html', function () {
type: 'js',
assets: [
'bundle-manifest.js',
'esm-js-loader.js',
'get-worker-url.js',
'index.js',
'lodash.js',
Expand Down Expand Up @@ -1944,6 +1951,7 @@ describe('html', function () {
type: 'js',
assets: [
'bundle-manifest.js',
'esm-js-loader.js',
'get-worker-url.js',
'index.js',
'lodash.js',
Expand Down Expand Up @@ -2183,7 +2191,7 @@ describe('html', function () {
assets: ['index.html'],
},
{
assets: ['a.js', 'bundle-manifest.js'],
assets: ['a.js', 'bundle-manifest.js', 'esm-js-loader.js'],
},
{
assets: [
Expand Down Expand Up @@ -2311,6 +2319,7 @@ describe('html', function () {
'index.js',
'client.js',
'bundle-manifest.js',
'esm-js-loader.js',
],
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/core/integration-tests/test/javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,7 @@ describe('javascript', function () {
{
assets: [
'bundle-manifest.js',
'esm-js-loader.js',
'get-worker-url.js',
'index.js',
'large.js',
Expand Down
15 changes: 14 additions & 1 deletion packages/core/integration-tests/test/output-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -1149,11 +1149,24 @@ describe('output formats', function () {
);
let async2Bundle = bundles.find(b => b.name.startsWith('async2'));

let esmLoaderPublicId;
b.traverse((node, _, actions) => {
if (
node.type === 'asset' &&
node.value.filePath.endsWith('esm-js-loader.js')
) {
esmLoaderPublicId = b.getAssetPublicId(node.value);
actions.stop();
}
});

assert(esmLoaderPublicId != null, 'Could not find esm loader public id');

for (let bundle of [async1Bundle, async2Bundle]) {
// async import both bundles in parallel for performance
assert(
new RegExp(
`import\\("\\./" \\+ .+\\.resolve\\("${sharedBundle.publicId}"\\)\\),\\n\\s*import\\("./" \\+ .+\\.resolve\\("${bundle.publicId}"\\)\\)`,
`\\$${esmLoaderPublicId}\\("${sharedBundle.publicId}"\\),\\n\\s*\\$${esmLoaderPublicId}\\("${bundle.publicId}"\\)`,
).test(entry),
);
}
Expand Down
131 changes: 85 additions & 46 deletions packages/runtimes/js/src/JSRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,46 +376,72 @@ function getLoaderRuntime({
let needsDynamicImportPolyfill =
!bundle.env.isLibrary && !bundle.env.supports('dynamic-import', true);

let loaderModules = externalBundles
.map(to => {
let loader = loaders[to.type];
if (!loader) {
return;
}
let needsEsmLoadPrelude = false;
let loaderModules = [];

let relativePathExpr = getRelativePathExpr(bundle, to, options);
for (let to of externalBundles) {
let loader = loaders[to.type];
if (!loader) {
continue;
}

// Use esmodule loader if possible
if (to.type === 'js' && to.env.outputFormat === 'esmodule') {
if (!needsDynamicImportPolyfill) {
return `__parcel__import__("./" + ${relativePathExpr})`;
}
if (
to.type === 'js' &&
to.env.outputFormat === 'esmodule' &&
!needsDynamicImportPolyfill &&
shouldUseRuntimeManifest(bundle, options)
) {
let params = [JSON.stringify(to.publicId)];

loader = nullthrows(
loaders.IMPORT_POLYFILL,
`No import() polyfill available for context '${bundle.env.context}'`,
);
} else if (to.type === 'js' && to.env.outputFormat === 'commonjs') {
return `Promise.resolve(__parcel__require__("./" + ${relativePathExpr}))`;
let relativeBase = getRelativeBasePath(
relativeBundlePath(bundle, to, {leadingDotSlash: false}),
);

if (relativeBase) {
params.push(relativeBase);
}

let code = `require(${JSON.stringify(loader)})(${getAbsoluteUrlExpr(
relativePathExpr,
bundle,
)})`;
loaderModules.push(`load(${params.join(',')})`);
needsEsmLoadPrelude = true;
continue;
}

// In development, clear the require cache when an error occurs so the
// user can try again (e.g. after fixing a build error).
if (
options.mode === 'development' &&
bundle.env.outputFormat === 'global'
) {
code +=
'.catch(err => {delete module.bundle.cache[module.id]; throw err;})';
let relativePathExpr = getRelativePathExpr(bundle, to, options);

// Use esmodule loader if possible
if (to.type === 'js' && to.env.outputFormat === 'esmodule') {
if (!needsDynamicImportPolyfill) {
loaderModules.push(`__parcel__import__("./" + ${relativePathExpr})`);
continue;
}
return code;
})
.filter(Boolean);

loader = nullthrows(
loaders.IMPORT_POLYFILL,
`No import() polyfill available for context '${bundle.env.context}'`,
);
} else if (to.type === 'js' && to.env.outputFormat === 'commonjs') {
loaderModules.push(
`Promise.resolve(__parcel__require__("./" + ${relativePathExpr}))`,
);
continue;
}

let code = `require(${JSON.stringify(loader)})(${getAbsoluteUrlExpr(
relativePathExpr,
bundle,
)})`;

// In development, clear the require cache when an error occurs so the
// user can try again (e.g. after fixing a build error).
if (
options.mode === 'development' &&
bundle.env.outputFormat === 'global'
) {
code +=
'.catch(err => {delete module.bundle.cache[module.id]; throw err;})';
}
loaderModules.push(code);
}

if (bundle.env.context === 'browser' && !options.shouldBuildLazily) {
loaderModules.push(
Expand Down Expand Up @@ -465,9 +491,17 @@ function getLoaderRuntime({
)}'))`;
}

let code = [];

if (needsEsmLoadPrelude) {
code.push(`let load = require('./helpers/browser/esm-js-loader');`);
}

code.push(`module.exports = ${loaderCode};`);

return {
filePath: __filename,
code: `module.exports = ${loaderCode};`,
code: code.join('\n'),
dependency,
env: {sourceType: 'module'},
};
Expand Down Expand Up @@ -623,26 +657,31 @@ function getRegisterCode(
);
}

function getRelativeBasePath(relativePath: string) {
// Get the relative part of the path. This part is not in the manifest, only the basename is.
let relativeBase = path.posix.dirname(relativePath);
if (relativeBase === '.') {
return '';
}
return JSON.stringify(relativeBase + '/');
}

function getRelativePathExpr(
from: NamedBundle,
to: NamedBundle,
options: PluginOptions,
): string {
let relativePath = relativeBundlePath(from, to, {leadingDotSlash: false});
if (shouldUseRuntimeManifest(from, options)) {
// Get the relative part of the path. This part is not in the manifest, only the basename is.
let relativeBase = path.posix.dirname(relativePath);
if (relativeBase === '.') {
relativeBase = '';
} else {
relativeBase = `${JSON.stringify(relativeBase + '/')} + `;
let basePath = getRelativeBasePath(relativePath);
let resolvedPath = `require('./helpers/bundle-manifest').resolve(${JSON.stringify(
to.publicId,
)})`;

if (basePath) {
return `${basePath} + ${resolvedPath}`;
}
return (
relativeBase +
`require('./helpers/bundle-manifest').resolve(${JSON.stringify(
to.publicId,
)})`
);
return resolvedPath;
}

let res = JSON.stringify(relativePath);
Expand Down
9 changes: 9 additions & 0 deletions packages/runtimes/js/src/helpers/browser/esm-js-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function load(id, base) {
let bundleId = require('../bundle-manifest').resolve(id);
let request = base ? './' + base + bundleId : './' + bundleId;

// eslint-disable-next-line no-undef
return __parcel__import__(request);
}

module.exports = load;
Loading