diff --git a/packages/metro-runtime/src/polyfills/require.js b/packages/metro-runtime/src/polyfills/require.js index 43dc352cc7..cd3b1db8af 100644 --- a/packages/metro-runtime/src/polyfills/require.js +++ b/packages/metro-runtime/src/polyfills/require.js @@ -316,6 +316,16 @@ metroRequire.context = function fallbackRequireContext() { ); }; +// `require.resolveWeak()` is a compile-time primitive (see collectDependencies.js) +metroRequire.resolveWeak = function fallbackRequireResolveWeak() { + if (__DEV__) { + throw new Error( + 'require.resolveWeak cannot be called dynamically. Ensure you are using the same version of `metro` and `metro-runtime`.', + ); + } + throw new Error('require.resolveWeak cannot be called dynamically.'); +}; + let inGuard = false; function guardedLoadModule( moduleId: ModuleID, diff --git a/packages/metro/src/DeltaBundler/Graph.js b/packages/metro/src/DeltaBundler/Graph.js index 36851b0c98..531d205b53 100644 --- a/packages/metro/src/DeltaBundler/Graph.js +++ b/packages/metro/src/DeltaBundler/Graph.js @@ -355,6 +355,8 @@ export class Graph { if (options.shallow) { // Don't add a node for the module if the graph is shallow (single-module). + } else if (dependency.data.data.asyncType === 'weak') { + // Exclude weak dependencies from the bundle. } else if ( options.experimentalImportBundleSupport && dependency.data.data.asyncType != null @@ -414,6 +416,11 @@ export class Graph { const {absolutePath} = dependency; + if (dependency.data.data.asyncType === 'weak') { + // Weak dependencies are excluded from the bundle. + return; + } + if ( options.experimentalImportBundleSupport && dependency.data.data.asyncType != null diff --git a/packages/metro/src/DeltaBundler/types.flow.js b/packages/metro/src/DeltaBundler/types.flow.js index b5026518b0..01ef785bcb 100644 --- a/packages/metro/src/DeltaBundler/types.flow.js +++ b/packages/metro/src/DeltaBundler/types.flow.js @@ -23,7 +23,7 @@ export type MixedOutput = { +type: string, }; -export type AsyncDependencyType = 'async' | 'prefetch'; +export type AsyncDependencyType = 'async' | 'prefetch' | 'weak'; export type TransformResultDependency = { /** diff --git a/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js b/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js index eb5da60086..e8750721b5 100644 --- a/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js +++ b/packages/metro/src/ModuleGraph/worker/__tests__/collectDependencies-test.js @@ -1427,6 +1427,21 @@ it('uses the dependency registry specified in the options to register dependenci ]); }); +it('collects require.resolveWeak calls', () => { + const ast = astFromCode(` + require.resolveWeak("some/async/module"); + `); + const {dependencies, dependencyMapName} = collectDependencies(ast, opts); + expect(dependencies).toEqual([ + {name: 'some/async/module', data: objectContaining({asyncType: 'weak'})}, + ]); + expect(codeFromAst(ast)).toEqual( + comparableCode(` + ${dependencyMapName}[0]; + `), + ); +}); + function formatDependencyLocs( dependencies: $ReadOnlyArray>, code: any, diff --git a/packages/metro/src/ModuleGraph/worker/collectDependencies.js b/packages/metro/src/ModuleGraph/worker/collectDependencies.js index a13740a63d..914dcf41e3 100644 --- a/packages/metro/src/ModuleGraph/worker/collectDependencies.js +++ b/packages/metro/src/ModuleGraph/worker/collectDependencies.js @@ -246,6 +246,24 @@ function collectDependencies( return; } + // Match `require.resolveWeak` + if ( + callee.type === 'MemberExpression' && + // `require` + callee.object.type === 'Identifier' && + callee.object.name === 'require' && + // `resolveWeak` + callee.property.type === 'Identifier' && + callee.property.name === 'resolveWeak' && + !callee.computed && + // Ensure `require` refers to the global and not something else. + !path.scope.getBinding('require') + ) { + processResolveWeakCall(path, state); + visited.add(path.node); + return; + } + if ( name != null && state.dependencyCalls.has(name) && @@ -426,6 +444,33 @@ function processRequireContextCall( transformer.transformSyncRequire(path, dep, state); } +function processResolveWeakCall( + path: NodePath, + state: State, +): void { + const name = getModuleNameFromCallArgs(path); + + if (name == null) { + throw new InvalidRequireCallError(path); + } + + const dependency = registerDependency( + state, + { + name, + asyncType: 'weak', + optional: isOptionalDependency(name, path, state), + }, + path, + ); + + path.replaceWith( + makeResolveWeakTemplate({ + MODULE_ID: createModuleIDExpression(dependency, state), + }), + ); +} + function collectImports( path: NodePath<>, state: State, @@ -619,7 +664,7 @@ collectDependencies.InvalidRequireCallError = InvalidRequireCallError; * is reached. This makes dynamic require errors catchable by libraries that * want to use them. */ -const dynamicRequireErrorTemplate = template.statement(` +const dynamicRequireErrorTemplate = template.expression(` (function(line) { throw new Error( 'Dynamic require defined at line ' + line + '; not supported by Metro', @@ -631,18 +676,22 @@ const dynamicRequireErrorTemplate = template.statement(` * Produces a Babel template that transforms an "import(...)" call into a * "require(...)" call to the asyncRequire specified. */ -const makeAsyncRequireTemplate = template.statement(` +const makeAsyncRequireTemplate = template.expression(` require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, MODULE_NAME, DEPENDENCY_MAP.paths) `); -const makeAsyncPrefetchTemplate = template.statement(` +const makeAsyncPrefetchTemplate = template.expression(` require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, MODULE_NAME, DEPENDENCY_MAP.paths) `); -const makeJSResourceTemplate = template.statement(` +const makeJSResourceTemplate = template.expression(` require(ASYNC_REQUIRE_MODULE_PATH).resource(MODULE_ID, MODULE_NAME, DEPENDENCY_MAP.paths) `); +const makeResolveWeakTemplate = template.expression(` + MODULE_ID +`); + const DefaultDependencyTransformer: DependencyTransformer = { transformSyncRequire( path: NodePath, @@ -722,10 +771,10 @@ const DefaultDependencyTransformer: DependencyTransformer = { }, }; -function createModuleIDExpression( - dependency: InternalDependency, - state: State, -) { +function createModuleIDExpression( + dependency: InternalDependency, + state: State, +): BabelNodeExpression { return types.memberExpression( nullthrows(state.dependencyMapIdentifier), types.numericLiteral(dependency.index), diff --git a/packages/metro/src/integration_tests/__tests__/require-resolveWeak-test.js b/packages/metro/src/integration_tests/__tests__/require-resolveWeak-test.js new file mode 100644 index 0000000000..435351d1e5 --- /dev/null +++ b/packages/metro/src/integration_tests/__tests__/require-resolveWeak-test.js @@ -0,0 +1,78 @@ +/** + * 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 + * @oncall react_native + */ + +'use strict'; + +const Metro = require('../../..'); +const execBundle = require('../execBundle'); + +jest.unmock('cosmiconfig'); + +jest.setTimeout(30 * 1000); + +test('resolveWeak() returns a different ID for each resolved module', async () => { + await expect(execTest('require-resolveWeak/multiple.js')).resolves.toEqual({ + counterModuleId1: 1, + counterModuleId2: 1, + throwingModuleId: 2, + }); +}); + +describe('resolveWeak() without calling require()', () => { + test('runtime semantics', async () => { + await expect( + execTest('require-resolveWeak/never-required.js'), + ).resolves.toEqual({ + moduleId: 1, + }); + }); + + test('the weak dependency is omitted', async () => { + const {code} = await buildTest('require-resolveWeak/never-required.js'); + expect(code).not.toContain('This module cannot be evaluated.'); + }); +}); + +test('calling both require() and resolveWeak() with the same module', async () => { + await expect( + execTest('require-resolveWeak/require-and-resolveWeak.js'), + ).resolves.toEqual({ + moduleId: 1, + timesIncremented: 2, + }); +}); + +test('calling both import() and resolveWeak() with the same module', async () => { + await expect( + execTest('require-resolveWeak/import-and-resolveWeak.js'), + ).resolves.toEqual({ + moduleId: 1, + timesIncremented: 2, + }); +}); + +async function buildTest(entry, {dev = true}: $ReadOnly<{dev: boolean}> = {}) { + const config = await Metro.loadConfig({ + config: require.resolve('../metro.config.js'), + }); + + const result = await Metro.runBuild(config, { + entry, + dev, + minify: !dev, + }); + + return result; +} + +async function execTest(entry, {dev = true}: $ReadOnly<{dev: boolean}> = {}) { + const result = await buildTest(entry, {dev}); + return execBundle(result.code); +} diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/import-and-resolveWeak.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/import-and-resolveWeak.js new file mode 100644 index 0000000000..365bfea93c --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/import-and-resolveWeak.js @@ -0,0 +1,33 @@ +/** + * 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-local + */ + +import type {RequireWithResolveWeak} from './utils'; + +declare var require: RequireWithResolveWeak; + +async function main() { + const moduleId = require.resolveWeak('./subdir/counter-module'); + + // Require the module statically via its path, spelled slightly differently + (await import('./subdir/counter-module.js')).increment(); + + const dynamicRequire = require; + + // Require the module dynamically via its ID + const timesIncremented = dynamicRequire(moduleId).increment(); + + return { + moduleId, + // Should be 2, proving there's just one module instance + timesIncremented, + }; +} + +module.exports = (main(): mixed); diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/multiple.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/multiple.js new file mode 100644 index 0000000000..a9738cd747 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/multiple.js @@ -0,0 +1,23 @@ +/** + * 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-local + */ + +import type {RequireWithResolveWeak} from './utils'; + +declare var require: RequireWithResolveWeak; + +async function main() { + return { + counterModuleId1: require.resolveWeak('./subdir/counter-module'), + counterModuleId2: require.resolveWeak('./subdir/counter-module.js'), + throwingModuleId: require.resolveWeak('./subdir/throwing-module.js'), + }; +} + +module.exports = (main(): mixed); diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/never-required.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/never-required.js new file mode 100644 index 0000000000..dcb2b6b9d3 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/never-required.js @@ -0,0 +1,21 @@ +/** + * 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-local + */ + +import type {RequireWithResolveWeak} from './utils'; + +declare var require: RequireWithResolveWeak; + +function main() { + return { + moduleId: require.resolveWeak('./subdir/throwing-module'), + }; +} + +module.exports = (main(): mixed); diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/require-and-resolveWeak.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/require-and-resolveWeak.js new file mode 100644 index 0000000000..d29a50b586 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/require-and-resolveWeak.js @@ -0,0 +1,33 @@ +/** + * 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-local + */ + +import type {RequireWithResolveWeak} from './utils'; + +declare var require: RequireWithResolveWeak; + +function main() { + const moduleId = require.resolveWeak('./subdir/counter-module'); + + const dynamicRequire = require; + + // Require the module dynamically via its ID + dynamicRequire(moduleId).increment(); + + // Require the module statically via its path, spelled slightly differently + const timesIncremented = require('./subdir/counter-module.js').increment(); + + return { + moduleId, + // Should be 2, proving there's just one module instance + timesIncremented, + }; +} + +module.exports = (main(): mixed); diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/counter-module.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/counter-module.js new file mode 100644 index 0000000000..423b476d07 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/counter-module.js @@ -0,0 +1,18 @@ +/** + * 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-local + */ + +let count: number = 0; + +module.exports = { + increment(): number { + ++count; + return count; + }, +}; diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/throwing-module.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/throwing-module.js new file mode 100644 index 0000000000..e10150c4a5 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/subdir/throwing-module.js @@ -0,0 +1,11 @@ +/** + * 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-local + */ + +throw new Error('This module cannot be evaluated.'); diff --git a/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/utils.js b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/utils.js new file mode 100644 index 0000000000..7a1368dd80 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/require-resolveWeak/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 + */ + +export type RequireWithResolveWeak = { + (id: string | number): any, + resolveWeak: (id: string) => string | number, +};