diff --git a/packages/metro-runtime/src/modules/asyncRequire.js b/packages/metro-runtime/src/modules/asyncRequire.js index 2d50bbf7d3..f59cdfa272 100644 --- a/packages/metro-runtime/src/modules/asyncRequire.js +++ b/packages/metro-runtime/src/modules/asyncRequire.js @@ -9,7 +9,6 @@ * @oncall react_native */ -type Options = {isPrefetchOnly: boolean, ...}; type MetroRequire = { (number): mixed, importAll: number => mixed, @@ -18,17 +17,14 @@ type MetroRequire = { 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 loadBundle( moduleID: number, paths: DependencyMapPaths, - options: Options, -): Promise { +): void | Promise { const loadBundle: (bundlePath: mixed) => Promise = global[`${__METRO_GLOBAL_PREFIX__}__loadBundleAsync`]; @@ -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( + moduleID: number, + paths: DependencyMapPaths, +): Promise | mixed { + const maybeLoadBundle = loadBundle(moduleID, paths); + const importAll = () => require.importAll(moduleID); + + if (maybeLoadBundle != null) { + return maybeLoadBundle.then(importAll); } - return undefined; + return importAll(); } async function asyncRequire( moduleID: number, paths: DependencyMapPaths, - moduleName?: string, + moduleName?: string, // unused ): Promise { - return asyncRequireImpl(moduleID, paths, DEFAULT_OPTIONS); + return asyncRequireImpl(moduleID, paths); } +// Synchronous version of asyncRequire, which can still return a promise +// if the module is split. +asyncRequire.importMaybeSync = function importMaybeSync( + moduleID: number, + paths: DependencyMapPaths, +): Promise | mixed { + return asyncRequireImpl(moduleID, paths); +}; + asyncRequire.prefetch = function ( moduleID: number, paths: DependencyMapPaths, - moduleName?: string, + moduleName?: string, // unused ): void { - asyncRequireImpl(moduleID, paths, {isPrefetchOnly: true}).then( + loadBundle(moduleID, paths)?.then( () => {}, () => {}, ); diff --git a/packages/metro-transform-worker/src/index.js b/packages/metro-transform-worker/src/index.js index ce268565a7..903b65e64b 100644 --- a/packages/metro-transform-worker/src/index.js +++ b/packages/metro-transform-worker/src/index.js @@ -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, }; diff --git a/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js b/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js index 28aff2e52e..ee919f7fdd 100644 --- a/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js +++ b/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js @@ -934,6 +934,65 @@ 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: 'async'}), + }, + {name: 'asyncRequire', data: objectContaining({asyncType: null})}, + ]); + expect(codeFromAst(ast)).toEqual( + comparableCode(` + require(${dependencyMapName}[1], "asyncRequire").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: 'async'}), + }, + {name: 'asyncRequire', data: objectContaining({asyncType: null})}, + ]); + expect(codeFromAst(ast)).toEqual( + comparableCode(` + require(${dependencyMapName}[1]).importMaybeSync(${dependencyMapName}[0], _dependencyMap.paths); + `), + ); + }); + + it('distinguishes between require.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: 'async'})}, + ]); + }); +}); + describe('Evaluating static arguments', () => { it('supports template literals as arguments', () => { const ast = astFromCode('require(`left-pad`)'); @@ -1568,6 +1627,14 @@ const MockDependencyTransformer: DependencyTransformer = { transformAsyncRequire(path, dependency, state, 'async'); }, + transformImportMaybeSyncCall( + path: NodePath<>, + dependency: InternalDependency, + state: State, + ): void { + transformAsyncRequire(path, dependency, state, 'importMaybeSync'); + }, + transformPrefetch( path: NodePath<>, dependency: InternalDependency, diff --git a/packages/metro/src/ModuleGraph/worker/collectDependencies.js b/packages/metro/src/ModuleGraph/worker/collectDependencies.js index 2007d687f5..33ca3526c5 100644 --- a/packages/metro/src/ModuleGraph/worker/collectDependencies.js +++ b/packages/metro/src/ModuleGraph/worker/collectDependencies.js @@ -28,6 +28,7 @@ const {isImport} = types; type ImportDependencyOptions = $ReadOnly<{ asyncType: AsyncDependencyType, + maybeSync?: boolean, }>; export type Dependency = $ReadOnly<{ @@ -112,6 +113,11 @@ export interface DependencyTransformer { dependency: InternalDependency, state: State, ): void; + transformImportMaybeSyncCall( + path: NodePath<>, + dependency: InternalDependency, + state: State, + ): void; transformPrefetch( path: NodePath<>, dependency: InternalDependency, @@ -214,6 +220,27 @@ 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: 'async', + maybeSync: true, + }); + visited.add(path.node); + return; + } + if ( name != null && state.dependencyCalls.has(name) && @@ -457,7 +484,11 @@ function processImportCall( const transformer = state.dependencyTransformer; if (options.asyncType === 'async') { - transformer.transformImportCall(path, dep, state); + if (options.maybeSync) { + transformer.transformImportMaybeSyncCall(path, dep, state); + } else { + transformer.transformImportCall(path, dep, state); + } } else { transformer.transformPrefetch(path, dep, state); } @@ -639,6 +670,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).importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths) +`); + +const makeAsyncImportMaybeSyncTemplateWithName = template.expression(` + require(ASYNC_REQUIRE_MODULE_PATH).importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME) +`); + const makeResolveWeakTemplate = template.expression(` MODULE_ID `); @@ -683,6 +722,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, diff --git a/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap b/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap index 012432b2e0..c7ef3f9cb1 100644 --- a/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap +++ b/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap @@ -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", diff --git a/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap b/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap index 4d86a7668f..6eb28f0f19 100644 --- a/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap +++ b/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap @@ -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 { @@ -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 { diff --git a/packages/metro/src/integration_tests/__tests__/server-test.js b/packages/metro/src/integration_tests/__tests__/server-test.js index f4ee32d5f2..bd7839067d 100644 --- a/packages/metro/src/integration_tests/__tests__/server-test.js +++ b/packages/metro/src/integration_tests/__tests__/server-test.js @@ -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', ]), ); }); @@ -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', diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-7.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-7.js new file mode 100644 index 0000000000..d52e0d1d46 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-7.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +module.exports = { + foo: 'export-7: FOO', +}; diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js new file mode 100644 index 0000000000..80ea23e156 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +export default 'export-8: DEFAULT'; + +export const foo = 'export-8: FOO'; diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/index.js b/packages/metro/src/integration_tests/basic_bundle/import-export/index.js index 5d6fe0563d..63a9c00e17 100644 --- a/packages/metro/src/integration_tests/basic_bundle/import-export/index.js +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/index.js @@ -10,6 +10,8 @@ 'use strict'; +import type {RequireWithUnstableImportMaybeSync} from './utils'; + import {default as myDefault, foo as myFoo, myFunction} from './export-1'; import * as importStar from './export-2'; import {foo} from './export-null'; @@ -17,6 +19,8 @@ import primitiveDefault, { foo as primitiveFoo, } from './export-primitive-default'; +declare var require: RequireWithUnstableImportMaybeSync; + export {default as namedDefaultExported} from './export-3'; export {foo as default} from './export-4'; @@ -32,3 +36,8 @@ export const extraData = { export const asyncImportCJS = import('./export-5'); export const asyncImportESM = import('./export-6'); + +export const asyncImportMaybeSyncCJS: mixed = + require.unstable_importMaybeSync('./export-7'); +export const asyncImportMaybeSyncESM: mixed = + require.unstable_importMaybeSync('./export-8'); diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/utils.js b/packages/metro/src/integration_tests/basic_bundle/import-export/utils.js new file mode 100644 index 0000000000..3dd4ffa47e --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/utils.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +export type RequireWithUnstableImportMaybeSync = { + (id: string | number): mixed, + unstable_importMaybeSync: (id: string) => mixed, +};