Skip to content

Commit

Permalink
Implement require.unstable_importMaybeSync and use it in JSResource (#…
Browse files Browse the repository at this point in the history
…1296)

Summary:
Pull Request resolved: #1296

This implements `require.unstable_importMaybeSync`, a new API to avoid having to wait for the next tick when importing a value using dynamic `import()` in bundles that don't use bundle splitting.

This API behaves this way:
* If the app is using bundle splitting, `require.unstable_importMaybeSync()` behaves exactly like `import()`. It always returns a promise and it marks a split point for the bundle.
* If the app isn't using bundle splitting, it behaves like a static import (e.g.: `import * as module from 'module'`) and returns the exports for that module synchronously.

This isn't meant to be used by users directly but to build other more specific APIs on top of this.

It includes a small refactor to `asyncRequire.js`.

Reviewed By: motiz88

Differential Revision: D58873729

fbshipit-source-id: 42edd0d61fe36019817a39b136ab0636ad88dd1c
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jun 25, 2024
1 parent 28e7ac2 commit 7b2ca5e
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 23 deletions.
49 changes: 32 additions & 17 deletions packages/metro-runtime/src/modules/asyncRequire.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,22 @@
* @oncall react_native
*/

type Options = {isPrefetchOnly: boolean, ...};
type MetroRequire = {
(number): mixed,
importAll: number => mixed,
importAll: <T>(number) => T,
...
};

declare var require: MetroRequire;

const DEFAULT_OPTIONS = {isPrefetchOnly: false};

type DependencyMapPaths = ?$ReadOnly<{[moduleID: number | string]: mixed}>;

declare var __METRO_GLOBAL_PREFIX__: string;

async function asyncRequireImpl(
function maybeLoadBundle(
moduleID: number,
paths: DependencyMapPaths,
options: Options,
): Promise<mixed> {
): void | Promise<void> {
const loadBundle: (bundlePath: mixed) => Promise<void> =
global[`${__METRO_GLOBAL_PREFIX__}__loadBundleAsync`];

Expand All @@ -38,32 +34,51 @@ async function asyncRequireImpl(
const bundlePath = paths[stringModuleID];
if (bundlePath != null) {
// NOTE: Errors will be swallowed by asyncRequire.prefetch
await loadBundle(bundlePath);
return loadBundle(bundlePath);
}
}
}

if (!options.isPrefetchOnly) {
return require.importAll(moduleID);
return undefined;
}

function asyncRequireImpl<T>(
moduleID: number,
paths: DependencyMapPaths,
): Promise<T> | T {
const maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
const importAll = () => require.importAll<T>(moduleID);

if (maybeLoadBundlePromise != null) {
return maybeLoadBundlePromise.then(importAll);
}

return undefined;
return importAll();
}

async function asyncRequire(
async function asyncRequire<T>(
moduleID: number,
paths: DependencyMapPaths,
moduleName?: string,
): Promise<mixed> {
return asyncRequireImpl(moduleID, paths, DEFAULT_OPTIONS);
moduleName?: string, // unused
): Promise<T> {
return asyncRequireImpl<T>(moduleID, paths);
}

// Synchronous version of asyncRequire, which can still return a promise
// if the module is split.
asyncRequire.unstable_importMaybeSync = function unstable_importMaybeSync<T>(
moduleID: number,
paths: DependencyMapPaths,
): Promise<T> | T {
return asyncRequireImpl(moduleID, paths);
};

asyncRequire.prefetch = function (
moduleID: number,
paths: DependencyMapPaths,
moduleName?: string,
moduleName?: string, // unused
): void {
asyncRequireImpl(moduleID, paths, {isPrefetchOnly: true}).then(
maybeLoadBundle(moduleID, paths)?.then(
() => {},
() => {},
);
Expand Down
1 change: 1 addition & 0 deletions packages/metro-transform-worker/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ const minifyCode = async (
const disabledDependencyTransformer: DependencyTransformer = {
transformSyncRequire: () => void 0,
transformImportCall: () => void 0,
transformImportMaybeSyncCall: () => void 0,
transformPrefetch: () => void 0,
transformIllegalDynamicRequire: () => void 0,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/metro/src/DeltaBundler/types.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type MixedOutput = {
+type: string,
};

export type AsyncDependencyType = 'async' | 'prefetch' | 'weak';
export type AsyncDependencyType = 'async' | 'maybeSync' | 'prefetch' | 'weak';

export type TransformResultDependency = $ReadOnly<{
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,68 @@ describe('import() prefetching', () => {
});
});

describe('require.unstable_importMaybeSync()', () => {
it('collects require.unstable_importMaybeSync calls', () => {
const ast = astFromCode(`
require.unstable_importMaybeSync("some/async/module");
`);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{
name: 'some/async/module',
data: objectContaining({asyncType: 'maybeSync'}),
},
{name: 'asyncRequire', data: objectContaining({asyncType: null})},
]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`
require(${dependencyMapName}[1], "asyncRequire").unstable_importMaybeSync(${dependencyMapName}[0], _dependencyMap.paths, "some/async/module");
`),
);
});

it('keepRequireNames: false', () => {
const ast = astFromCode(`
require.unstable_importMaybeSync("some/async/module");
`);
const {dependencies, dependencyMapName} = collectDependencies(ast, {
...opts,
keepRequireNames: false,
});
expect(dependencies).toEqual([
{
name: 'some/async/module',
data: objectContaining({asyncType: 'maybeSync'}),
},
{name: 'asyncRequire', data: objectContaining({asyncType: null})},
]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`
require(${dependencyMapName}[1]).unstable_importMaybeSync(${dependencyMapName}[0], _dependencyMap.paths);
`),
);
});

it('distinguishes between require.unstable_importMaybeSync and prefetch dependencies on the same module', () => {
const ast = astFromCode(`
__prefetchImport("some/async/module");
require.unstable_importMaybeSync("some/async/module").then(() => {});
`);
const {dependencies} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{
name: 'some/async/module',
data: objectContaining({asyncType: 'prefetch'}),
},
{name: 'asyncRequire', data: objectContaining({asyncType: null})},
{
name: 'some/async/module',
data: objectContaining({asyncType: 'maybeSync'}),
},
]);
});
});

describe('Evaluating static arguments', () => {
it('supports template literals as arguments', () => {
const ast = astFromCode('require(`left-pad`)');
Expand Down Expand Up @@ -1568,6 +1630,14 @@ const MockDependencyTransformer: DependencyTransformer = {
transformAsyncRequire(path, dependency, state, 'async');
},
transformImportMaybeSyncCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void {
transformAsyncRequire(path, dependency, state, 'unstable_importMaybeSync');
},
transformPrefetch(
path: NodePath<>,
dependency: InternalDependency,
Expand Down
73 changes: 69 additions & 4 deletions packages/metro/src/ModuleGraph/worker/collectDependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export interface DependencyTransformer {
dependency: InternalDependency,
state: State,
): void;
transformImportMaybeSyncCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void;
transformPrefetch(
path: NodePath<>,
dependency: InternalDependency,
Expand Down Expand Up @@ -214,6 +219,26 @@ function collectDependencies(
return;
}

// Match `require.unstable_importMaybeSync`
if (
callee.type === 'MemberExpression' &&
// `require`
callee.object.type === 'Identifier' &&
callee.object.name === 'require' &&
// `unstable_importMaybeSync`
callee.property.type === 'Identifier' &&
callee.property.name === 'unstable_importMaybeSync' &&
!callee.computed &&
// Ensure `require` refers to the global and not something else.
!path.scope.getBinding('require')
) {
processImportCall(path, state, {
asyncType: 'maybeSync',
});
visited.add(path.node);
return;
}

if (
name != null &&
state.dependencyCalls.has(name) &&
Expand Down Expand Up @@ -456,10 +481,21 @@ function processImportCall(

const transformer = state.dependencyTransformer;

if (options.asyncType === 'async') {
transformer.transformImportCall(path, dep, state);
} else {
transformer.transformPrefetch(path, dep, state);
switch (options.asyncType) {
case 'async':
transformer.transformImportCall(path, dep, state);
break;
case 'maybeSync':
transformer.transformImportMaybeSyncCall(path, dep, state);
break;
case 'prefetch':
transformer.transformPrefetch(path, dep, state);
break;
case 'weak':
throw new Error('Unreachable');
default:
options.asyncType as empty;
throw new Error('Unreachable');
}
}

Expand Down Expand Up @@ -639,6 +675,14 @@ const makeAsyncPrefetchTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);

const makeAsyncImportMaybeSyncTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths)
`);

const makeAsyncImportMaybeSyncTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);

const makeResolveWeakTemplate = template.expression(`
MODULE_ID
`);
Expand Down Expand Up @@ -683,6 +727,27 @@ const DefaultDependencyTransformer: DependencyTransformer = {
path.replaceWith(makeNode(opts));
},

transformImportMaybeSyncCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void {
const makeNode = state.keepRequireNames
? makeAsyncImportMaybeSyncTemplateWithName
: makeAsyncImportMaybeSyncTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral,
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {MODULE_NAME: createModuleNameLiteral(dependency)}
: null),
};
path.replaceWith(makeNode(opts));
},

transformPrefetch(
path: NodePath<>,
dependency: InternalDependency,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ exports[`builds a simple bundle 1`] = `
Object {
"asyncImportCJS": Promise {},
"asyncImportESM": Promise {},
"asyncImportMaybeSyncCJS": Object {
"default": Object {
"foo": "export-7: FOO",
},
"foo": "export-7: FOO",
},
"asyncImportMaybeSyncESM": Object {
"default": "export-8: DEFAULT",
"foo": "export-8: FOO",
},
"default": "export-4: FOO",
"extraData": Object {
"foo": "export-null: FOO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ Object {
}
`;

exports[`Metro development server serves bundles via HTTP should serve lazy bundles 3`] = `
Object {
"default": Object {
"foo": "export-7: FOO",
},
"foo": "export-7: FOO",
}
`;

exports[`Metro development server serves bundles via HTTP should serve lazy bundles 4`] = `
Object {
"default": "export-8: DEFAULT",
"foo": "export-8: FOO",
}
`;

exports[`Metro development server serves bundles via HTTP should serve non-lazy bundles by default 1`] = `
Object {
"default": Object {
Expand All @@ -61,6 +77,22 @@ Object {
}
`;

exports[`Metro development server serves bundles via HTTP should serve non-lazy bundles by default 3`] = `
Object {
"default": Object {
"foo": "export-7: FOO",
},
"foo": "export-7: FOO",
}
`;

exports[`Metro development server serves bundles via HTTP should serve non-lazy bundles by default 4`] = `
Object {
"default": "export-8: DEFAULT",
"foo": "export-8: FOO",
}
`;

exports[`Metro development server serves bundles via HTTP should serve production bundles 1`] = `
Object {
"Bar": Object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ describe('Metro development server serves bundles via HTTP', () => {
);
await expect(object.asyncImportCJS).resolves.toMatchSnapshot();
await expect(object.asyncImportESM).resolves.toMatchSnapshot();
await expect(object.asyncImportMaybeSyncCJS).resolves.toMatchSnapshot();
await expect(object.asyncImportMaybeSyncESM).resolves.toMatchSnapshot();
expect(bundlesDownloaded).toEqual(
new Set([
'/import-export/index.bundle?platform=ios&dev=true&minify=false&lazy=true',
'/import-export/export-6.bundle?platform=ios&dev=true&minify=false&lazy=true&modulesOnly=true&runModule=false',
'/import-export/export-5.bundle?platform=ios&dev=true&minify=false&lazy=true&modulesOnly=true&runModule=false',
'/import-export/export-6.bundle?platform=ios&dev=true&minify=false&lazy=true&modulesOnly=true&runModule=false',
'/import-export/export-7.bundle?platform=ios&dev=true&minify=false&lazy=true&modulesOnly=true&runModule=false',
'/import-export/export-8.bundle?platform=ios&dev=true&minify=false&lazy=true&modulesOnly=true&runModule=false',
]),
);
});
Expand All @@ -97,6 +101,8 @@ describe('Metro development server serves bundles via HTTP', () => {
);
await expect(object.asyncImportCJS).resolves.toMatchSnapshot();
await expect(object.asyncImportESM).resolves.toMatchSnapshot();
await expect(object.asyncImportMaybeSyncCJS).toMatchSnapshot();
await expect(object.asyncImportMaybeSyncESM).toMatchSnapshot();
expect(bundlesDownloaded).toEqual(
new Set([
'/import-export/index.bundle?platform=ios&dev=true&minify=false',
Expand Down
Loading

0 comments on commit 7b2ca5e

Please sign in to comment.