diff --git a/packages/metro-runtime/src/polyfills/__tests__/require-test.js b/packages/metro-runtime/src/polyfills/__tests__/require-test.js index e2af1e4058..7155da940f 100644 --- a/packages/metro-runtime/src/polyfills/__tests__/require-test.js +++ b/packages/metro-runtime/src/polyfills/__tests__/require-test.js @@ -505,6 +505,38 @@ describe('require', () => { ); }); + test('throws "module not found" when trying to require an unresolvable optional dependency', () => { + createModuleSystem(moduleSystem, false, ''); + + createModule( + moduleSystem, + 0, + 'foo.js', + (global, require, importDefault, importAll, module) => { + require(null); + }, + ); + + expect(() => moduleSystem.__r(0)).toThrow('Cannot find module'); + }); + + test('throws "module not found" with name when trying to require an unresolvable optional dependency', () => { + createModuleSystem(moduleSystem, true, ''); + + createModule( + moduleSystem, + 0, + 'foo.js', + (global, require, importDefault, importAll, module) => { + require(null, './not-exists'); + }, + ); + + expect(() => moduleSystem.__r(0)).toThrow( + "Cannot find module './not-exists'", + ); + }); + test('throws an error when a module throws an error', () => { createModuleSystem(moduleSystem, false, ''); diff --git a/packages/metro-runtime/src/polyfills/require.js b/packages/metro-runtime/src/polyfills/require.js index 1cb0f7b033..97f3fd54f2 100644 --- a/packages/metro-runtime/src/polyfills/require.js +++ b/packages/metro-runtime/src/polyfills/require.js @@ -169,7 +169,19 @@ function define( } } -function metroRequire(moduleId: ModuleID | VerboseModuleNameForDev): Exports { +function metroRequire( + moduleId: ModuleID | VerboseModuleNameForDev | null, + maybeNameForDev?: string, +): Exports { + // Unresolved optional dependencies are nulls in dependency maps + // eslint-disable-next-line lint/strictly-null + if (moduleId === null) { + if (__DEV__ && typeof maybeNameForDev === 'string') { + throw new Error("Cannot find module '" + maybeNameForDev + "'"); + } + throw new Error('Cannot find module'); + } + if (__DEV__ && typeof moduleId === 'string') { const verboseName = moduleId; moduleId = getModuleIdForVerboseName(verboseName); diff --git a/packages/metro/src/DeltaBundler/Graph.js b/packages/metro/src/DeltaBundler/Graph.js index 51cff208ca..07e7c9fb72 100644 --- a/packages/metro/src/DeltaBundler/Graph.js +++ b/packages/metro/src/DeltaBundler/Graph.js @@ -39,11 +39,13 @@ import type { Module, ModuleData, Options, + ResolvedDependency, TransformInputOptions, } from './types.flow'; import {fileMatchesContext} from '../lib/contextModule'; import CountingSet from '../lib/CountingSet'; +import {isResolvedDependency} from '../lib/isResolvedDependency'; import {buildSubgraph} from './buildSubgraph'; const invariant = require('invariant'); @@ -121,7 +123,7 @@ function getInternalOptions({ } function isWeakOrLazy( - dependency: Dependency, + dependency: ResolvedDependency, options: InternalOptions, ): boolean { const asyncType = dependency.data.data.asyncType; @@ -355,7 +357,7 @@ export class Graph { options.onDependencyAdded(); return result; }, - shouldTraverse: (dependency: Dependency) => { + shouldTraverse: (dependency: ResolvedDependency) => { if (options.shallow || isWeakOrLazy(dependency, options)) { return false; } @@ -510,14 +512,11 @@ export class Graph { delta: Delta, options: InternalOptions, ): void { - const path = dependency.absolutePath; - - // The module may already exist, in which case we just need to update some - // bookkeeping instead of adding a new node to the graph. - let module = this.dependencies.get(path); - if (options.shallow) { // Don't add a node for the module if the graph is shallow (single-module). + } else if (!isResolvedDependency(dependency)) { + // If the dependency is a missing optional dependency, it has no node of + // its own. We just need to add it to the parent's dependency map. } else if (dependency.data.data.asyncType === 'weak') { // Exclude weak dependencies from the bundle. } else if (options.lazy && dependency.data.data.asyncType != null) { @@ -526,6 +525,11 @@ export class Graph { // importBundleNodes. this._incrementImportBundleReference(dependency, parentModule); } else { + // The module may already exist, in which case we just need to update some + // bookkeeping instead of adding a new node to the graph. + const path = dependency.absolutePath; + let module = this.dependencies.get(path); + if (!module) { try { module = this._recursivelyCommitModule(path, delta, options); @@ -550,12 +554,15 @@ export class Graph { this._markModuleInUse(module); } - if (requireContext) { - this.#resolvedContexts.set(path, requireContext); - } else { - // This dependency may have existed previously as a require.context - - // clean it up. - this.#resolvedContexts.delete(path); + if (isResolvedDependency(dependency)) { + const path = dependency.absolutePath; + if (requireContext) { + this.#resolvedContexts.set(path, requireContext); + } else { + // This dependency may have existed previously as a require.context - + // clean it up. + this.#resolvedContexts.delete(path); + } } // Update the parent's dependency map unless we failed to add a dependency. @@ -574,13 +581,16 @@ export class Graph { ): void { parentModule.dependencies.delete(key); - const {absolutePath} = dependency; - - if (dependency.data.data.asyncType === 'weak') { - // Weak dependencies are excluded from the bundle. + if ( + !isResolvedDependency(dependency) || + dependency.data.data.asyncType === 'weak' + ) { + // Weak and unresolved dependencies are excluded from the bundle. return; } + const {absolutePath} = dependency; + const module = this.dependencies.get(absolutePath); if (options.lazy && dependency.data.data.asyncType != null) { @@ -671,8 +681,12 @@ export class Graph { orderedDependencies.set(module.path, module); } - module.dependencies.forEach((dependency: Dependency) => { + module.dependencies.forEach(dependency => { const path = dependency.absolutePath; + if (path == null) { + // If the dependency is not a missing optional dependency, it has no children to reorder. + return; + } const childModule = this.dependencies.get(path); if (!childModule) { @@ -691,7 +705,7 @@ export class Graph { // Add an entry to importBundleNodes (or record an inverse dependency of an existing one) _incrementImportBundleReference( - dependency: Dependency, + dependency: ResolvedDependency, parentModule: Module, ) { const {absolutePath} = dependency; @@ -704,7 +718,7 @@ export class Graph { // Decrease the reference count of an entry in importBundleNodes (and delete it if necessary) _decrementImportBundleReference( - dependency: Dependency, + dependency: ResolvedDependency, parentModule: Module, ) { const {absolutePath} = dependency; @@ -734,7 +748,10 @@ export class Graph { options: InternalOptions, ): Iterator> { for (const dependency of module.dependencies.values()) { - if (isWeakOrLazy(dependency, options)) { + if ( + !isResolvedDependency(dependency) || + isWeakOrLazy(dependency, options) + ) { continue; } yield nullthrows(this.dependencies.get(dependency.absolutePath)); @@ -747,6 +764,9 @@ export class Graph { const resolvedContexts: Map = new Map(); for (const [key, dependency] of dependencies) { + if (!isResolvedDependency(dependency)) { + continue; + } const resolvedContext = this.#resolvedContexts.get( dependency.absolutePath, ); @@ -783,6 +803,10 @@ export class Graph { } for (const [key, dependency] of module.dependencies) { + if (!isResolvedDependency(dependency)) { + // If the dependency is not a missing optional dependency, it has no children to remove. + continue; + } this._removeDependency(module, key, dependency, delta, options); } this.#gc.color.set(module.path, 'black'); @@ -894,6 +918,9 @@ export class Graph { if (color === 'white' && !this.#gc.possibleCycleRoots.has(module.path)) { this.#gc.color.set(module.path, 'black'); for (const dependency of module.dependencies.values()) { + if (!isResolvedDependency(dependency)) { + continue; + } const childModule = this.dependencies.get(dependency.absolutePath); // The child may already have been collected. if (childModule) { diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/getTransitiveDependencies.js b/packages/metro/src/DeltaBundler/Serializers/helpers/getTransitiveDependencies.js index dd7abd49da..a5eac43ce2 100644 --- a/packages/metro/src/DeltaBundler/Serializers/helpers/getTransitiveDependencies.js +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/getTransitiveDependencies.js @@ -13,6 +13,8 @@ import type {ReadOnlyGraph} from '../../types.flow'; +import {isResolvedDependency} from '../../../lib/isResolvedDependency'; + function getTransitiveDependencies( path: string, graph: ReadOnlyGraph, @@ -44,7 +46,9 @@ function _getDeps( deps.add(path); for (const dependency of module.dependencies.values()) { - _getDeps(dependency.absolutePath, graph, deps); + if (isResolvedDependency(dependency)) { + _getDeps(dependency.absolutePath, graph, deps); + } } return deps; diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/js.js b/packages/metro/src/DeltaBundler/Serializers/helpers/js.js index 6650eddfef..576a29f388 100644 --- a/packages/metro/src/DeltaBundler/Serializers/helpers/js.js +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/js.js @@ -14,6 +14,7 @@ import type {MixedOutput, Module} from '../../types.flow'; import type {JsOutput} from 'metro-transform-worker'; +const {isResolvedDependency} = require('../../../lib/isResolvedDependency'); const invariant = require('invariant'); const jscSafeUrl = require('jsc-safe-url'); const {addParamsToDefineCall} = require('metro-transform-plugins'); @@ -47,6 +48,11 @@ function getModuleParams(module: Module<>, options: Options): Array { let hasPaths = false; const dependencyMapArray = Array.from(module.dependencies.values()).map( dependency => { + if (!isResolvedDependency(dependency)) { + // An unresolved dependency, which should cause a runtime error + // when required. + return null; + } const id = options.createModuleId(dependency.absolutePath); if (options.includeAsyncPaths && dependency.data.data.asyncType != null) { hasPaths = true; diff --git a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js index d6a81f3789..5c77edc847 100644 --- a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js @@ -42,6 +42,7 @@ import type { Options, ReadOnlyDependencies, ReadOnlyGraph, + ResolvedDependency, TransformFn, TransformResultDependency, TransformResultWithSource, @@ -283,7 +284,10 @@ function computeInverseDependencies( } for (const module of graph.dependencies.values()) { for (const dependency of module.dependencies.values()) { - if (options.lazy && dependency.data.data.asyncType != null) { + if ( + dependency.absolutePath == null || + (options.lazy && dependency.data.data.asyncType != null) + ) { // Async deps aren't tracked in inverseDependencies continue; } @@ -323,7 +327,7 @@ class TestGraph extends Graph<> { options, ); const actualInverseDependencies = new Map>(); - for (const [path, module] of graph.dependencies) { + for (const [path, module] of this.dependencies) { actualInverseDependencies.set(path, new Set(module.inverseDependencies)); } expect(actualInverseDependencies).toEqual(expectedInverseDependencies); @@ -3495,7 +3499,7 @@ describe('require.context', () => { describe('reorderGraph', () => { test('should reorder any unordered graph in DFS order', async () => { - const dep = (path: string): Dependency => ({ + const dep = (path: string): ResolvedDependency => ({ absolutePath: path, data: { data: { @@ -3553,31 +3557,7 @@ describe('reorderGraph', () => { describe('optional dependencies', () => { let localGraph; let localOptions; - const getAllDependencies = () => { - const all = new Set(); - mockedDependencyTree.forEach(deps => { - deps.forEach(r => all.add(r.name)); - }); - return all; - }; - const assertResults = ( - dependencies: Map>, - expectedMissing: Array, - ) => { - let count = 0; - const allDependency = getAllDependencies(); - allDependency.forEach(m => { - const data = dependencies.get(`/${m}`); - if (expectedMissing.includes(m)) { - expect(data).toBeUndefined(); - } else { - expect(data).not.toBeUndefined(); - } - count += 1; - }); - expect(count).toBeGreaterThan(0); - expect(count).toBe(allDependency.size); - }; + let dependencyKeys: Map; const createMockTransform = (notOptional?: string[]) => { /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by @@ -3608,11 +3588,18 @@ describe('optional dependencies', () => { beforeEach(() => { mockedDependencies = new Set(); mockedDependencyTree = new Map(); + dependencyKeys = new Map(); entryModule = Actions.createFile('/bundle-o'); - Actions.addDependency('/bundle-o', '/regular-a'); - Actions.addDependency('/bundle-o', '/optional-b'); + dependencyKeys.set( + '/regular-a', + Actions.addDependency('/bundle-o', '/regular-a'), + ); + dependencyKeys.set( + '/optional-b', + Actions.addDependency('/bundle-o', '/optional-b'), + ); localGraph = new TestGraph({ entryPoints: new Set(['/bundle-o']), @@ -3630,8 +3617,41 @@ describe('optional dependencies', () => { const result = await localGraph.initialTraverseDependencies(localOptions); - const dependencies = result.added; - assertResults(dependencies, ['optional-b']); + expect(result.added).toEqual( + new Map([ + [ + '/bundle-o', + expect.objectContaining({ + dependencies: new Map([ + [ + dependencyKeys.get('/regular-a'), + expect.objectContaining({ + absolutePath: '/regular-a', + data: expect.objectContaining({ + isOptional: false, + }), + }), + ], + [ + dependencyKeys.get('/optional-b'), + expect.objectContaining({ + absolutePath: null, + data: expect.objectContaining({ + isOptional: true, + }), + }), + ], + ]), + }), + ], + [ + '/regular-a', + expect.objectContaining({ + dependencies: new Map(), + }), + ], + ]), + ); }); test('missing non-optional dependency will throw', async () => { localOptions = { @@ -3642,6 +3662,105 @@ describe('optional dependencies', () => { localGraph.initialTraverseDependencies(localOptions), ).rejects.toThrow(); }); + + test('deleting an optional dependency will not throw', async () => { + localOptions = { + ...options, + transform: createMockTransform(), + }; + /* + ┌───────────┐ ┌────────────┐ + │ /bundle-o │ ──▶ │ /regular-a │ + └───────────┘ └────────────┘ + │ + │ + ▼ + ┌─────────────┐ ┌────────────┐ + │ /optional-b │ ──▶ │ /regular-c │ + └─────────────┘ └────────────┘ + */ + Actions.createFile('/optional-b'); + Actions.addDependency('/optional-b', '/regular-c'); + + expect( + getPaths(await localGraph.initialTraverseDependencies(localOptions)), + ).toEqual({ + added: new Set(['/bundle-o', '/regular-a', '/optional-b', '/regular-c']), + modified: new Set(), + deleted: new Set(), + }); + + Actions.deleteFile('/optional-b', localGraph); + + /* + ┌───────────┐ ┌────────────┐ + │ /bundle-o │ ──▶ │ /regular-a │ + └───────────┘ └────────────┘ + │ + │ + ▼ + ┌────╲──╱─────┐ ┌────────────┐ + │ /optional-b │ ──▶ │ /regular-c │ + └────╱──╲─────┘ └────────────┘ + */ + + expect( + getPaths(await localGraph.traverseDependencies([...files], localOptions)), + ).toEqual({ + added: new Set(), + modified: new Set(['/bundle-o']), + deleted: new Set(['/optional-b', '/regular-c']), + }); + }); + + test('creating a file satisfying an unresolved optional dependency', async () => { + localOptions = { + ...options, + transform: createMockTransform(), + }; + /* + ┌───────────┐ ┌────────────┐ + │ /bundle-o │ ──▶ │ /regular-a │ + └───────────┘ └────────────┘ + ┊ + ┊ + ▽ + ┌┈┈┈┈┈┈┈┈┈┈┈┈┈┐ + ┊ /optional-b ┊ (not yet created) + └┈┈┈┈┈┈┈┈┈┈┈┈┈┘ + */ + + expect( + getPaths(await localGraph.initialTraverseDependencies(localOptions)), + ).toEqual({ + added: new Set(['/bundle-o', '/regular-a']), + modified: new Set(), + deleted: new Set(), + }); + + Actions.createFile('/optional-b'); + Actions.addDependency('/optional-b', '/regular-c'); + + /* + ┌───────────┐ ┌────────────┐ + │ /bundle-o │ ──▶ │ /regular-a │ + └───────────┘ └────────────┘ + │ + │ + ▼ + ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━┓ + ┃ /optional-b ┃ ──▶ ┃ /regular-c ┃ + ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━┛ + */ + + expect( + getPaths(await localGraph.traverseDependencies([...files], localOptions)), + ).toEqual({ + added: new Set(['/optional-b', '/regular-c']), + modified: new Set(['/bundle-o']), + deleted: new Set(), + }); + }); }); describe('parallel edges', () => { diff --git a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js index 37b9f51c49..cf0f2e3418 100644 --- a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js @@ -9,7 +9,10 @@ */ import type {RequireContextParams} from '../../ModuleGraph/worker/collectDependencies'; -import type {Dependency, TransformResultDependency} from '../types.flow'; +import type { + ResolvedDependency, + TransformResultDependency, +} from '../types.flow'; import {buildSubgraph} from '../buildSubgraph'; import nullthrows from 'nullthrows'; @@ -84,7 +87,8 @@ describe('GraphTraversal', () => { }; }), shouldTraverse: jest.fn( - (dependency: Dependency) => dependency.data.data.asyncType !== 'weak', + (dependency: ResolvedDependency) => + dependency.data.data.asyncType !== 'weak', ), }; }); diff --git a/packages/metro/src/DeltaBundler/buildSubgraph.js b/packages/metro/src/DeltaBundler/buildSubgraph.js index 3fd767b1f7..299973a177 100644 --- a/packages/metro/src/DeltaBundler/buildSubgraph.js +++ b/packages/metro/src/DeltaBundler/buildSubgraph.js @@ -12,18 +12,20 @@ import type {RequireContext} from '../lib/contextModule'; import type { Dependency, ModuleData, + ResolvedDependency, ResolveFn, TransformFn, TransformResultDependency, } from './types.flow'; import {deriveAbsolutePathFromContext} from '../lib/contextModule'; +import {isResolvedDependency} from '../lib/isResolvedDependency'; import path from 'path'; type Parameters = $ReadOnly<{ resolve: ResolveFn, transform: TransformFn, - shouldTraverse: Dependency => boolean, + shouldTraverse: ResolvedDependency => boolean, }>; function resolveDependencies( @@ -34,7 +36,7 @@ function resolveDependencies( dependencies: Map, resolvedContexts: Map, } { - const maybeResolvedDeps = new Map(); + const maybeResolvedDeps = new Map(); const resolvedContexts = new Map(); for (const dep of dependencies) { @@ -77,6 +79,10 @@ function resolveDependencies( if (dep.data.isOptional !== true) { throw error; } + resolvedDep = { + absolutePath: null, + data: dep, + }; } } @@ -88,18 +94,10 @@ function resolveDependencies( maybeResolvedDeps.set(key, resolvedDep); } - const resolvedDeps = new Map(); - // Return just the dependencies we successfully resolved. - // FIXME: This has a bad bug affecting all dependencies *after* an unresolved - // optional dependency. We'll need to propagate the nulls all the way to the - // serializer and the require() runtime to keep the dependency map from being - // desynced from the contents of the module. - for (const [key, resolvedDep] of maybeResolvedDeps) { - if (resolvedDep) { - resolvedDeps.set(key, resolvedDep); - } - } - return {dependencies: resolvedDeps, resolvedContexts}; + return { + dependencies: maybeResolvedDeps, + resolvedContexts, + }; } export async function buildSubgraph( @@ -137,16 +135,22 @@ export async function buildSubgraph( ...resolutionResult, }); - await Promise.all( - [...resolutionResult.dependencies] - .filter(([key, dependency]) => shouldTraverse(dependency)) - .map(([key, dependency]) => + const promises: Array> = []; + for (const dependency of resolutionResult.dependencies.values()) { + if (isResolvedDependency(dependency) && shouldTraverse(dependency)) { + const {absolutePath, data} = dependency; + promises.push( visit( - dependency.absolutePath, - resolutionResult.resolvedContexts.get(dependency.data.data.key), - ).catch(error => errors.set(dependency.absolutePath, error)), - ), - ); + absolutePath, + resolutionResult.resolvedContexts.get(data.data.key), + ).catch(error => { + errors.set(absolutePath, error); + }), + ); + } + } + + await Promise.all(promises); } await Promise.all( diff --git a/packages/metro/src/DeltaBundler/types.flow.js b/packages/metro/src/DeltaBundler/types.flow.js index 4cdaeee4b8..faf1bddb77 100644 --- a/packages/metro/src/DeltaBundler/types.flow.js +++ b/packages/metro/src/DeltaBundler/types.flow.js @@ -61,11 +61,18 @@ export type TransformResultDependency = $ReadOnly<{ }>, }>; -export type Dependency = $ReadOnly<{ +export type ResolvedDependency = $ReadOnly<{ absolutePath: string, data: TransformResultDependency, }>; +export type Dependency = + | ResolvedDependency + | $ReadOnly<{ + absolutePath: null, + data: TransformResultDependency, + }>; + export type Module = $ReadOnly<{ dependencies: Map, inverseDependencies: CountingSet, diff --git a/packages/metro/src/integration_tests/__tests__/optional-dependencies-test.js b/packages/metro/src/integration_tests/__tests__/optional-dependencies-test.js new file mode 100644 index 0000000000..2e7f61e90f --- /dev/null +++ b/packages/metro/src/integration_tests/__tests__/optional-dependencies-test.js @@ -0,0 +1,82 @@ +/** + * 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('builds a simple bundle', async () => { + const config = await Metro.loadConfig( + { + config: require.resolve('../metro.config.js'), + }, + { + transformer: { + allowOptionalDependencies: true, + }, + }, + ); + + const result = await Metro.runBuild(config, { + entry: 'optional-dependencies/index.js', + dev: true, + minify: false, + }); + + // The module we're interested in should be the first defined + const match = result.code + .replaceAll( + 'optional-dependencies\\\\', // FIXME: Normalise for Windows + 'optional-dependencies/', + ) + .match(/__d\(.*"optional-dependencies\/index\.js"\);/s); + + expect(match).not.toBeNull(); + + expect(match[0]).toMatchInlineSnapshot(` +"__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + var shouldBeB, shouldBeC; + try { + shouldBeB = _$$_REQUIRE(_dependencyMap[0], \\"./not-exists\\"); + } catch (_unused) { + shouldBeB = _$$_REQUIRE(_dependencyMap[1], \\"./optional-b\\"); + } + (function requireOptionalC() { + try { + shouldBeC = _$$_REQUIRE(_dependencyMap[2], \\"./optional-c\\"); + } catch (e) {} + })(); + var a = _$$_REQUIRE(_dependencyMap[3], \\"./required-a\\"); + var b = shouldBeB; + var c = shouldBeC; + exports.a = a; + exports.b = b; + exports.c = c; +},0,[null,1,2,3],\\"optional-dependencies/index.js\\");" +`); + + const object = execBundle(result.code); + + expect(object).toEqual({ + a: 'a', + b: 'b', + c: 'c', + }); +}); diff --git a/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/index.js b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/index.js new file mode 100644 index 0000000000..bbfa7167f4 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/index.js @@ -0,0 +1,34 @@ +/** + * 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'; + +let shouldBeB: mixed, shouldBeC: mixed; +try { + // $FlowExpectedError[cannot-resolve-module] + shouldBeB = require('./not-exists'); +} catch { + shouldBeB = require('./optional-b'); +} + +(function requireOptionalC() { + // This function is here to ensure that the `a` module is always required, + // even if it is not used in the code. + try { + shouldBeC = require('./optional-c'); + } catch (e) { + // If the optional module is not found, we can ignore the error. + // This is to simulate an optional dependency that may or may not exist. + } +})(); + +export const a = require('./required-a'); +export const b = shouldBeB; +export const c = shouldBeC; diff --git a/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-b.js b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-b.js new file mode 100644 index 0000000000..38d3109141 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-b.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 + */ + +module.exports = 'b'; diff --git a/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-c.js b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-c.js new file mode 100644 index 0000000000..aebd334cac --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/optional-c.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 + */ + +module.exports = 'c'; diff --git a/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/required-a.js b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/required-a.js new file mode 100644 index 0000000000..2f2cc498ea --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/optional-dependencies/required-a.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 + */ + +module.exports = 'a'; diff --git a/packages/metro/src/lib/isResolvedDependency.js b/packages/metro/src/lib/isResolvedDependency.js new file mode 100644 index 0000000000..9c0c887ff7 --- /dev/null +++ b/packages/metro/src/lib/isResolvedDependency.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. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {Dependency, ResolvedDependency} from '../DeltaBundler/types.flow'; + +export function isResolvedDependency( + dep: Dependency, +): dep is ResolvedDependency { + return dep.absolutePath != null; +}