diff --git a/packages/metro/src/DeltaBundler/Graph.js b/packages/metro/src/DeltaBundler/Graph.js index e81e4e55e9..284bb38ffd 100644 --- a/packages/metro/src/DeltaBundler/Graph.js +++ b/packages/metro/src/DeltaBundler/Graph.js @@ -29,7 +29,6 @@ * nodes and entries in the importBundleNodes set. */ -import type {RequireContext} from '../lib/contextModule'; import type {RequireContextParams} from '../ModuleGraph/worker/collectDependencies'; import type { Dependencies, @@ -41,6 +40,7 @@ import type { Options, ResolvedDependency, TransformInputOptions, + VirtualSource, } from './types'; import {fileMatchesContext} from '../lib/contextModule'; @@ -149,8 +149,11 @@ export class Graph { possibleCycleRoots: new Set(), }; - /** Resolved context parameters from `require.context`. */ - #resolvedContexts: Map = new Map(); + /** + * Sources of the modules in the graph that have no file, keyed by module + * path. + */ + #virtualSources: Map = new Map(); constructor(options: GraphInputOptions) { this.entryPoints = options.entryPoints; @@ -348,7 +351,7 @@ export class Graph { options: InternalOptions, moduleFilter?: (path: string) => boolean, ): Promise> { - const subGraph = await buildSubgraph(pathsToVisit, this.#resolvedContexts, { + const subGraph = await buildSubgraph(pathsToVisit, this.#virtualSources, { resolve: options.resolve, shouldTraverse: (dependency: ResolvedDependency) => { if (options.shallow || isWeakOrLazy(dependency, options)) { @@ -356,9 +359,9 @@ export class Graph { } return moduleFilter == null || moduleFilter(dependency.absolutePath); }, - transform: async (absolutePath, requireContext) => { + transform: async (absolutePath, virtualSource) => { options.onDependencyAdd(); - const result = await options.transform(absolutePath, requireContext); + const result = await options.transform(absolutePath, virtualSource); options.onDependencyAdded(); return result; }, @@ -394,7 +397,7 @@ export class Graph { const previousDependencies = previousModule?.dependencies ?? new Map(); const { dependencies: currentDependencies, - resolvedContexts, + virtualSources, ...transformResult } = currentModule; @@ -449,7 +452,7 @@ export class Graph { nextModule, key, curDependency, - resolvedContexts.get(key), + virtualSources.get(key), delta, options, ); @@ -507,7 +510,7 @@ export class Graph { parentModule: Module, key: string, dependency: Dependency, - requireContext: ?RequireContext, + virtualSource: ?VirtualSource, delta: Delta, options: InternalOptions, ): void { @@ -555,12 +558,12 @@ export class Graph { if (isResolvedDependency(dependency)) { const path = dependency.absolutePath; - if (requireContext) { - this.#resolvedContexts.set(path, requireContext); + if (virtualSource) { + this.#virtualSources.set(path, virtualSource); } else { // This dependency may have existed previously as a require.context - // clean it up. - this.#resolvedContexts.delete(path); + this.#virtualSources.delete(path); } } @@ -623,10 +626,11 @@ export class Graph { filePath: string, modifiedPaths: Set | CountingSet, ) { - for (const [absolutePath, context] of this.#resolvedContexts) { + for (const [absolutePath, virtualSource] of this.#virtualSources) { if ( + virtualSource.type === 'requireContext' && !modifiedPaths.has(absolutePath) && - fileMatchesContext(filePath, context) + fileMatchesContext(filePath, virtualSource.requireContext) ) { modifiedPaths.add(absolutePath); } @@ -761,23 +765,21 @@ export class Graph { const {dependencies, getSource, output, unstable_transformResultKey} = module; - const resolvedContexts: Map = new Map(); + const virtualSources: Map = new Map(); for (const [key, dependency] of dependencies) { if (!isResolvedDependency(dependency)) { continue; } - const resolvedContext = this.#resolvedContexts.get( - dependency.absolutePath, - ); - if (resolvedContext != null) { - resolvedContexts.set(key, resolvedContext); + const virtualSource = this.#virtualSources.get(dependency.absolutePath); + if (virtualSource != null) { + virtualSources.set(key, virtualSource); } } return { dependencies: new Map(dependencies), getSource, output, - resolvedContexts, + virtualSources, unstable_transformResultKey, }; } @@ -828,7 +830,7 @@ export class Graph { this.dependencies.delete(module.path); this.#gc.possibleCycleRoots.delete(module.path); this.#gc.color.delete(module.path); - this.#resolvedContexts.delete(module.path); + this.#virtualSources.delete(module.path); } // Mark a module as a possible cycle root diff --git a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js index 803c892a5b..f7f139d522 100644 --- a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js @@ -32,7 +32,6 @@ * boxes/lines as needed). */ -import type {RequireContext} from '../../lib/contextModule'; import type {RequireContextParams} from '../../ModuleGraph/worker/collectDependencies'; import type {Result} from '../Graph'; import type { @@ -46,6 +45,7 @@ import type { TransformFn, TransformResultDependency, TransformResultWithSource, + VirtualSource, } from '../types'; import {deriveAbsolutePathFromContext} from '../../lib/contextModule'; @@ -357,10 +357,10 @@ beforeEach(async () => { mockTransform = jest .fn< - [string, ?RequireContext], + [string, ?VirtualSource], Promise>, >() - .mockImplementation(async (path: string, context: ?RequireContext) => { + .mockImplementation(async (path: string, context: ?VirtualSource) => { const override = transformOverrides.get(path); if (override != null) { return override(path, context); @@ -556,7 +556,7 @@ test('should retry traversing dependencies after a transform error', async () => const localOptions = { ...options, - transform(path: string, context: ?RequireContext) { + transform(path: string, context: ?VirtualSource) { if (path === '/bad') { throw new BadError(); } @@ -2351,7 +2351,7 @@ describe('edge cases', () => { let fastResolved = false; localMockTransform.mockImplementation( - async (path: string, context: ?RequireContext) => { + async (path: string, context: ?VirtualSource) => { const result = await mockTransform(path, context); if (path === slowPath && !fastResolved) { @@ -2967,10 +2967,13 @@ describe('require.context', () => { }; const ctxResolved = { - recursive: true, - mode: 'sync', - filter: /.*/, - from: p('/ctx'), + type: 'requireContext', + requireContext: { + recursive: true, + mode: 'sync', + filter: /.*/, + from: p('/ctx'), + }, }; const ctxPath = deriveAbsolutePathFromContext(p('/ctx'), ctxParams); @@ -3301,10 +3304,13 @@ describe('require.context', () => { }; const narrowCtxResolved = { - recursive: true, - mode: 'sync', - filter: /\.\/narrow\/.*/, - from: p('/ctx'), + type: 'requireContext', + requireContext: { + recursive: true, + mode: 'sync', + filter: /\.\/narrow\/.*/, + from: p('/ctx'), + }, }; const narrowCtxPath = deriveAbsolutePathFromContext( @@ -3592,7 +3598,7 @@ describe('optional dependencies', () => { const createMockTransform = (notOptional?: string[]) => { /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - return async function (path: string, context: ?RequireContext) { + return async function (path: string, context: ?VirtualSource) { const result = await mockTransform.call(this, path, context); return { ...result, diff --git a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js index a64bc90207..74c35f191b 100644 --- a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js @@ -117,7 +117,7 @@ describe('GraphTraversal', () => { ]), getSource: expect.any(Function), output: [], - resolvedContexts: new Map(), + virtualSources: new Map(), }); }); @@ -139,7 +139,7 @@ describe('GraphTraversal', () => { }; expect(params.transform).toHaveBeenCalledWith( p('/virtual?ctx=af3bf59b8564d441084c02bdf04c4d662d74d3bd'), - expectedResolvedContext, + {type: 'requireContext', requireContext: expectedResolvedContext}, ); expect(params.transform).toHaveBeenCalledWith( p('/contextMatch'), @@ -167,8 +167,14 @@ describe('GraphTraversal', () => { }, ], ]), - resolvedContexts: new Map([ - ['key-virtual', expectedResolvedContext], + virtualSources: new Map([ + [ + 'key-virtual', + { + type: 'requireContext', + requireContext: expectedResolvedContext, + }, + ], ]), output: [], getSource: expect.any(Function), @@ -178,7 +184,7 @@ describe('GraphTraversal', () => { p('/contextMatch'), { dependencies: new Map(), - resolvedContexts: new Map(), + virtualSources: new Map(), output: [], getSource: expect.any(Function), }, @@ -201,7 +207,7 @@ describe('GraphTraversal', () => { }, ], ]), - resolvedContexts: new Map(), + virtualSources: new Map(), output: [], getSource: expect.any(Function), }, diff --git a/packages/metro/src/DeltaBundler/buildSubgraph.js b/packages/metro/src/DeltaBundler/buildSubgraph.js index 0b26dbde04..dadf39a629 100644 --- a/packages/metro/src/DeltaBundler/buildSubgraph.js +++ b/packages/metro/src/DeltaBundler/buildSubgraph.js @@ -16,6 +16,7 @@ import type { ResolveFn, TransformFn, TransformResultDependency, + VirtualSource, } from './types'; import {deriveAbsolutePathFromContext} from '../lib/contextModule'; @@ -34,10 +35,10 @@ function resolveDependencies( resolve: ResolveFn, ): { dependencies: Map, - resolvedContexts: Map, + virtualSources: Map, } { const maybeResolvedDeps = new Map(); - const resolvedContexts = new Map(); + const virtualSources = new Map(); for (const dep of dependencies) { let maybeResolvedDep: Dependency; @@ -61,7 +62,10 @@ function resolveDependencies( recursive: contextParams.recursive, }; - resolvedContexts.set(key, resolvedContext); + virtualSources.set(key, { + type: 'requireContext', + requireContext: resolvedContext, + }); maybeResolvedDep = { absolutePath, @@ -95,13 +99,13 @@ function resolveDependencies( return { dependencies: maybeResolvedDeps, - resolvedContexts, + virtualSources, }; } export async function buildSubgraph( entryPaths: ReadonlySet, - resolvedContexts: ReadonlyMap, + virtualSources: ReadonlyMap, {resolve, transform, shouldTraverse}: Parameters, ): Promise<{ moduleData: Map>, @@ -113,13 +117,13 @@ export async function buildSubgraph( async function visit( absolutePath: string, - requireContext: ?RequireContext, + virtualSource: ?VirtualSource, ): Promise { if (visitedPaths.has(absolutePath)) { return; } visitedPaths.add(absolutePath); - const transformResult = await transform(absolutePath, requireContext); + const transformResult = await transform(absolutePath, virtualSource); // Get the absolute path of all sub-dependencies (some of them could have been // moved but maintain the same relative path). @@ -143,7 +147,7 @@ export async function buildSubgraph( .map(dependency => visit( dependency.absolutePath, - resolutionResult.resolvedContexts.get(dependency.data.data.key), + resolutionResult.virtualSources.get(dependency.data.data.key), ).catch(error => errors.set(dependency.absolutePath, error)), ), ); @@ -151,7 +155,7 @@ export async function buildSubgraph( await Promise.all( [...entryPaths].map(absolutePath => - visit(absolutePath, resolvedContexts.get(absolutePath)).catch(error => + visit(absolutePath, virtualSources.get(absolutePath)).catch(error => errors.set(absolutePath, error), ), ), diff --git a/packages/metro/src/DeltaBundler/types.js b/packages/metro/src/DeltaBundler/types.js index ac000fa07a..688bfd42ea 100644 --- a/packages/metro/src/DeltaBundler/types.js +++ b/packages/metro/src/DeltaBundler/types.js @@ -90,9 +90,20 @@ export type Module = Readonly<{ unstable_transformResultKey?: ?string, }>; +/** + * How the transform obtains the source of a module that has no file, as + * supplied by the resolution that produced it. A `require.context` module + * arrives as the parameters the transform expands by enumerating the file map. + */ +export type VirtualSource = Readonly<{ + type: 'requireContext', + requireContext: RequireContext, +}>; + export type ModuleData = Readonly<{ dependencies: ReadonlyMap, - resolvedContexts: ReadonlyMap, + /** Keyed by dependency key, for the dependencies that resolved to a module with no file. */ + virtualSources: ReadonlyMap, output: ReadonlyArray, getSource: () => Buffer, unstable_transformResultKey?: ?string, @@ -137,7 +148,7 @@ export type TransformResultWithSource = Readonly<{ export type TransformFn = ( string, - ?RequireContext, + ?VirtualSource, ) => Promise>; export type ResolveFn = ( diff --git a/packages/metro/src/lib/transformHelpers.js b/packages/metro/src/lib/transformHelpers.js index 1bdec316e2..82e5fd7db2 100644 --- a/packages/metro/src/lib/transformHelpers.js +++ b/packages/metro/src/lib/transformHelpers.js @@ -15,10 +15,10 @@ import type { BundlerResolution, TransformInputOptions, TransformResultDependency, + VirtualSource, } from '../DeltaBundler/types'; import type {TransformOptions} from '../DeltaBundler/Worker'; import type {ResolverInputOptions} from '../shared/types'; -import type {RequireContext} from './contextModule'; import type {ConfigT} from 'metro-config'; import type {Type} from 'metro-transform-worker'; @@ -152,10 +152,11 @@ export async function getTransformFn( ); const assetExts = new Set(config.resolver.assetExts); - return async (modulePath: string, requireContext: ?RequireContext) => { - let templateBuffer: Buffer; + return async (modulePath: string, virtualSource: ?VirtualSource) => { + let fileBuffer: ?Buffer; - if (requireContext) { + if (virtualSource?.type === 'requireContext') { + const {requireContext} = virtualSource; const graph = await bundler.getDependencyGraph(); // TODO: Check delta changes to avoid having to look over all files each time @@ -175,7 +176,7 @@ export async function getTransformFn( files, ); - templateBuffer = Buffer.from(template); + fileBuffer = Buffer.from(template); } return await bundler.transformFile( @@ -188,7 +189,7 @@ export async function getTransformFn( ), type: getType(transformOptions.type, modulePath, assetExts), }, - templateBuffer, + fileBuffer ?? undefined, ); }; }