Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ Type: `string`
The name of a module that provides the `asyncRequire` function, which is used to implement [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) at runtime. Defaults to [`metro-runtime/src/modules/asyncRequire`](https://github.com/react/metro/blob/main/packages/metro-runtime/src/modules/asyncRequire.js).

:::note
The module named by `asyncRequireModulePath` is [resolved](./Resolution.md) relative to the module containing the original `import()` call. In particular, assuming the default value of `asyncRequireModulePath` is in use, the project must have a compatible version of `metro-runtime` installed in `node_modules`.
The module named by `asyncRequireModulePath` is [resolved](./Resolution.md) relative to each module containing an `import()` call, including modules in `node_modules`, so it must be resolvable from all of them. In particular, assuming the default value of `asyncRequireModulePath` is in use, the project must have a compatible version of `metro-runtime` installed in `node_modules`. Use `metro:runtime/modules/asyncRequire` to resolve the same module from any importer, through Metro's built-in [`metro:` scheme resolver](#schemeresolvers), in the `metro-runtime` that Metro itself depends on.
:::

:::info
Expand Down
8 changes: 6 additions & 2 deletions packages/metro/src/DeltaBundler/__tests__/resolver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,12 @@ function dep(name: string): TransformResultDependency {

jest.requireMock('node:os').tmpdir = () => p('/tmp');

// Node resolves Metro's own `@babel/runtime` to a path on the host,
// which is not valid for the platform under test.
// Node resolves Metro's own metro-runtime and `@babel/runtime` to paths
// on the host, which are not valid for the platform under test.
jest.doMock('../../lib/metroRuntime', () => ({
getMetroRuntimePackageJsonPath: () =>
p('/root/node_modules/metro-runtime/package.json'),
}));
jest.doMock('../../lib/metroBabelRuntime', () => ({
getMetroBabelRuntimePackageJsonPath: () =>
p('/root/node_modules/@babel/runtime/package.json'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {getDynamicRootId} = require('../../lib/dynamicRoots');
const {
getMetroBabelRuntimePackageJsonPath,
} = require('../../lib/metroBabelRuntime');
const {getMetroRuntimePackageJsonPath} = require('../../lib/metroRuntime');
const execBundle = require('../execBundle');
const fs = require('node:fs');
const path = require('node:path');
Expand Down Expand Up @@ -122,3 +123,42 @@ describe("Metro's own @babel/runtime outside every configured root", () => {
expect(sourceResponse.status).toBe(200);
});
});

describe("Metro's own metro-runtime outside every configured root", () => {
const metroRuntimeDir = path.dirname(getMetroRuntimePackageJsonPath());
let config;

beforeEach(async () => {
const baseConfig = await Metro.loadConfig({
config: require.resolve('../metro.config.js'),
});
config = {
...baseConfig,
resolver: {
...baseConfig.resolver,
// metro-runtime is not resolvable from the importer as a bare
// specifier, as for library code under an isolated layout.
disableHierarchicalLookup: true,
nodeModulesPaths: [],
},
transformer: {
...baseConfig.transformer,
asyncRequireModulePath: 'metro:runtime/modules/asyncRequire',
},
watchFolders: [baseConfig.projectRoot],
};
});

test('the fixture has metro-runtime outside projectRoot and watchFolders', () => {
for (const root of config.watchFolders) {
expect(metroRuntimeDir.startsWith(root + path.sep)).toBe(false);
}
});

test('builds and runs a bundle that uses import() with asyncRequireModulePath from metro:runtime', async () => {
const result = await Metro.runBuild(config, {
entry: 'metro-runtime/index.js',
});
await expect(execBundle(result.code).lazyValue).resolves.toBe('lazy');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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.
*
* @flow strict
* @format
*/

export const lazyValue: Promise<string> = import('./lazy').then(
module => module.default,
);
Original file line number Diff line number Diff line change
@@ -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.
*
* @flow strict
* @format
*/

export default 'lazy';
3 changes: 1 addition & 2 deletions packages/metro/src/integration_tests/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ module.exports = {
},
transformer: {
assetRegistryPath: path.join(ROOT_PATH, 'AssetRegistry'),
asyncRequireModulePath:
require.resolve('metro-runtime/src/modules/asyncRequire'),
asyncRequireModulePath: 'metro:runtime/modules/asyncRequire',
babelTransformerPath:
require.resolve('@react-native/metro-babel-transformer'),
enableBabelRCLookup: false,
Expand Down
41 changes: 41 additions & 0 deletions packages/metro/src/lib/__tests__/metroSchemeResolver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,44 @@ test('throws for unsupported metro: specifiers', () => {
).toThrow("Unsupported 'metro:' pathname: something-else");
expect(calls).toHaveLength(0);
});

test.each([
['metro:runtime', 'metro-runtime'],
['metro:runtime/modules/asyncRequire', 'metro-runtime/modules/asyncRequire'],
])(
'resolves %s to metro-runtime via package self resolution',
(specifier, expected) => {
const {resolveRequest, calls} = makeCapturingResolveRequest();
const context = {
...makeContext(resolveRequest),
unstable_enablePackageExports: false,
};

expect(metroSchemeResolver(context, specifier, 'ios')).toEqual({
type: 'sourceFile',
filePath: p('/resolved.js'),
});
expect(calls).toEqual([
{
originModulePath: require.resolve('metro-runtime/package.json'),
specifier: expected,
platform: 'ios',
},
]);
},
);

test('applies package exports for metro:runtime regardless of the project setting', () => {
const contexts: Array<CustomResolutionContext> = [];
const context = {
...makeContext(ctx => {
contexts.push(ctx);
return {type: 'sourceFile', filePath: p('/resolved.js')};
}),
unstable_enablePackageExports: false,
};

metroSchemeResolver(context, 'metro:runtime/modules/asyncRequire', null);
expect(contexts).toHaveLength(1);
expect(contexts[0].unstable_enablePackageExports).toBe(true);
});
5 changes: 2 additions & 3 deletions packages/metro/src/lib/metroBabelRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @oncall react_native
*/

import {getMetroRuntimePackageJsonPath} from './metroRuntime';
import * as path from 'node:path';

/**
Expand All @@ -29,9 +30,7 @@ let version: ?string = null;
*/
export function getMetroBabelRuntimePackageJsonPath(): string {
if (packageJsonPath == null) {
const metroRuntimeDir = path.dirname(
require.resolve('metro-runtime/package.json'),
);
const metroRuntimeDir = path.dirname(getMetroRuntimePackageJsonPath());
packageJsonPath = require.resolve('@babel/runtime/package.json', {
paths: [metroRuntimeDir],
});
Expand Down
25 changes: 25 additions & 0 deletions packages/metro/src/lib/metroRuntime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* 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.
*
* @flow strict-local
* @format
* @oncall react_native
*/

// Resolved on first use rather than at module load, for consistency with
// `metroBabelRuntime`.
let packageJsonPath: ?string = null;

/**
* The `package.json` path of the metro-runtime that Metro depends on, which
* `metro:runtime` resolves into.
*/
export function getMetroRuntimePackageJsonPath(): string {
if (packageJsonPath == null) {
packageJsonPath = require.resolve('metro-runtime/package.json');
}
return packageJsonPath;
}
31 changes: 29 additions & 2 deletions packages/metro/src/lib/metroSchemeResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
import type {CustomResolver} from 'metro-resolver';

import {getMetroBabelRuntimePackageJsonPath} from './metroBabelRuntime';
import {getMetroRuntimePackageJsonPath} from './metroRuntime';

const BABEL_RUNTIME_SPECIFIER = 'babel-runtime';
const BABEL_RUNTIME_PACKAGE = '@babel/runtime';
const RUNTIME_SPECIFIER = 'runtime';
const RUNTIME_PACKAGE = 'metro-runtime';

/**
* Resolver used for Metro's own `metro:` URI scheme, currently handling only
* metro:babel-runtime and subpaths.
* Resolver used for Metro's own `metro:` URI scheme, handling
* metro:babel-runtime, metro:runtime and their subpaths.
*/
export default ((context, specifier, platform) => {
const {protocol, pathname} = new URL(specifier);
Expand All @@ -42,5 +45,29 @@ export default ((context, specifier, platform) => {
);
}

// Maps `metro:runtime/<subpath>` (e.g. `metro:runtime/modules/asyncRequire`)
// to the metro-runtime that Metro depends on, so that modules Metro injects
// into transformed code resolve from any importer, including library code
// that cannot see metro-runtime under an isolated `node_modules` layout.
// The subpath is resolved as a package self-reference, like
// `metro:babel-runtime`. Subpaths such as `modules/asyncRequire` exist only
// in metro-runtime's `exports`, so package exports are enabled here
// regardless of the project's `unstable_enablePackageExports` setting.
if (
pathname === RUNTIME_SPECIFIER ||
pathname.startsWith(RUNTIME_SPECIFIER + '/')
) {
const subpath = pathname.slice(RUNTIME_SPECIFIER.length);
return context.resolveRequest(
{
...context,
originModulePath: getMetroRuntimePackageJsonPath(),
unstable_enablePackageExports: true,
},
RUNTIME_PACKAGE + subpath,
platform,
);
}

throw new Error(`Unsupported '${protocol}' pathname: ${pathname}`);
}) as CustomResolver;
32 changes: 20 additions & 12 deletions packages/metro/src/node-haste/DependencyGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {

import {getDynamicRoots} from '../lib/dynamicRoots';
import {getMetroBabelRuntimePackageJsonPath} from '../lib/metroBabelRuntime';
import {getMetroRuntimePackageJsonPath} from '../lib/metroRuntime';
import createFileMap from './DependencyGraph/createFileMap';
import createModuleResolver from './DependencyGraph/createModuleResolver';
import {PackageCache} from './PackageCache';
Expand Down Expand Up @@ -123,7 +124,7 @@ export default class DependencyGraph extends EventEmitter {
this._haste.on('status', status => this._onWatcherStatus(status));

this._initializedPromise = fileMap.build().then(async ({fileSystem}) => {
await this._addMetroBabelRuntimeRoot();
await this._addMetroRuntimeRoots();
log(createActionEndEntry(initializingMetroLogEntry));
config.reporter.update({type: 'dep_graph_loaded'});

Expand All @@ -145,19 +146,26 @@ export default class DependencyGraph extends EventEmitter {
}

/**
* `metro:babel-runtime` resolves into the `@babel/runtime` that
* metro-runtime depends on, wherever it is installed, so its files must be
* in the file map even when no configured root contains them.
* `metro:runtime` and `metro:babel-runtime` resolve into the metro-runtime
* that Metro depends on and its `@babel/runtime`, wherever they are
* installed, so their files must be in the file map even when no configured
* root contains them. metro-runtime is added first, so that an
* `@babel/runtime` nested within it needs no root of its own.
*/
async _addMetroBabelRuntimeRoot(): Promise<void> {
let packageJsonPath;
try {
packageJsonPath = getMetroBabelRuntimePackageJsonPath();
} catch {
// Reported by the `metro:` scheme resolver, if it is ever used.
return;
async _addMetroRuntimeRoots(): Promise<void> {
for (const getPackageJsonPath of [
getMetroRuntimePackageJsonPath,
getMetroBabelRuntimePackageJsonPath,
]) {
let packageJsonPath;
try {
packageJsonPath = getPackageJsonPath();
} catch {
// Reported by the `metro:` scheme resolver, if it is ever used.
continue;
}
await this._haste.addRoot(path.dirname(packageJsonPath));
}
await this._haste.addRoot(path.dirname(packageJsonPath));
}

/**
Expand Down
Loading