Skip to content

Commit

Permalink
Support require.resolveWeak
Browse files Browse the repository at this point in the history
Summary: Implements `require.resolveWeak`, a low-level [Webpack API](https://webpack.js.org/api/module-methods/#requireresolveweak) that returns a module's ID without necessarily including it in the bundle.

Reviewed By: GijsWeterings

Differential Revision: D42621299

fbshipit-source-id: ad9b53c45aeafe8ae2db0d54f7ae81d7e77f1906
  • Loading branch information
motiz88 authored and facebook-github-bot committed Feb 27, 2023
1 parent c8da588 commit 354d6e4
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 9 deletions.
10 changes: 10 additions & 0 deletions packages/metro-runtime/src/polyfills/require.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/metro/src/DeltaBundler/Graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ export class Graph<T = MixedOutput> {

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
Expand Down Expand Up @@ -414,6 +416,11 @@ export class Graph<T = MixedOutput> {

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
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';
export type AsyncDependencyType = 'async' | 'prefetch' | 'weak';

export type TransformResultDependency = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dependency<mixed>>,
code: any,
Expand Down
65 changes: 57 additions & 8 deletions packages/metro/src/ModuleGraph/worker/collectDependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,24 @@ function collectDependencies<TSplitCondition = void>(
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) &&
Expand Down Expand Up @@ -426,6 +444,33 @@ function processRequireContextCall<TSplitCondition>(
transformer.transformSyncRequire(path, dep, state);
}

function processResolveWeakCall<TSplitCondition>(
path: NodePath<CallExpression>,
state: State<TSplitCondition>,
): 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<TSplitCondition>(
path: NodePath<>,
state: State<TSplitCondition>,
Expand Down Expand Up @@ -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',
Expand All @@ -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<mixed> = {
transformSyncRequire(
path: NodePath<CallExpression>,
Expand Down Expand Up @@ -722,10 +771,10 @@ const DefaultDependencyTransformer: DependencyTransformer<mixed> = {
},
};

function createModuleIDExpression(
dependency: InternalDependency<mixed>,
state: State<mixed>,
) {
function createModuleIDExpression<TSplitCondition>(
dependency: InternalDependency<TSplitCondition>,
state: State<TSplitCondition>,
): BabelNodeExpression {
return types.memberExpression(
nullthrows(state.dependencyMapIdentifier),
types.numericLiteral(dependency.index),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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;
},
};
Loading

1 comment on commit 354d6e4

@faceyspacey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use import() in RN then? What's needed for splitting in RN?

Please sign in to comment.