diff --git a/packages/metro-babel-transformer/src/index.js b/packages/metro-babel-transformer/src/index.js index 4c35296faa..4f33cebfd0 100644 --- a/packages/metro-babel-transformer/src/index.js +++ b/packages/metro-babel-transformer/src/index.js @@ -54,11 +54,20 @@ export type BabelFileFunctionMapMetadata = $ReadOnly<{ export type BabelFileImportLocsMetadata = $ReadOnlySet; +export type VirtualModule = $ReadOnly<{ + absolutePath: string, + code: string, + type: 'sourceFile', +}>; + +export type VirtualModulesRawMap = Map; + export type MetroBabelFileMetadata = { ...BabelFileMetadata, metro?: ?{ functionMap?: ?BabelFileFunctionMapMetadata, unstable_importDeclarationLocs?: ?BabelFileImportLocsMetadata, + virtualModulesRawMap?: VirtualModulesRawMap, ... }, ... diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index be669c3a81..d0b95d71cd 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -58,6 +58,19 @@ export default function resolve( ); } + if ( + context.dependency?.data.isVirtualModule && + context.dependency?.data.absolutePath && + context.dependency?.data.type + ) { + // $FlowFixMe[incompatible-type] fix it + return { + type: context.dependency.data.type, + filePath: context.dependency.data.absolutePath, + isVirtualModule: true, + }; + } + if (isRelativeImport(moduleName) || path.isAbsolute(moduleName)) { const result = resolveModulePath(context, moduleName, platform); if (result.type === 'failed') { diff --git a/packages/metro-transform-worker/src/index.js b/packages/metro-transform-worker/src/index.js index 3972142fcd..250c978b82 100644 --- a/packages/metro-transform-worker/src/index.js +++ b/packages/metro-transform-worker/src/index.js @@ -48,6 +48,7 @@ import { toSegmentTuple, } from 'metro-source-map'; import metroTransformPlugins from 'metro-transform-plugins'; +import {VirtualModules} from 'metro/private/DeltaBundler/VirtualModules'; import collectDependencies from 'metro/private/ModuleGraph/worker/collectDependencies'; import generateImportNames from 'metro/private/ModuleGraph/worker/generateImportNames'; import { @@ -151,6 +152,7 @@ type JSFile = $ReadOnly<{ type: JSFileType, functionMap: FBSourceFunctionMap | null, unstable_importDeclarationLocs?: ?$ReadOnlySet, + virtualModules?: ?VirtualModules, }>; type JSONFile = { @@ -177,6 +179,7 @@ export type JsOutput = $ReadOnly<{ type TransformResponse = $ReadOnly<{ dependencies: $ReadOnlyArray, output: $ReadOnlyArray, + virtualModules?: ?VirtualModules, }>; function getDynamicDepsBehavior( @@ -405,6 +408,7 @@ async function transformJS( ? (loc: BabelSourceLocation) => importDeclarationLocs.has(locToKey(loc)) : null, + virtualModules: file.virtualModules, }; ({ast, dependencies, dependencyMapName} = collectDependencies(ast, opts)); } catch (error) { @@ -501,9 +505,12 @@ async function transformJS( }, ]; + const {virtualModules} = file; + return { dependencies, output, + virtualModules, }; } @@ -563,6 +570,11 @@ async function transformJSWithBabel( null, unstable_importDeclarationLocs: transformResult.metadata?.metro?.unstable_importDeclarationLocs, + virtualModules: new VirtualModules( + // TODO: use raw map here + // $FlowFixMe[prop-missing] we need to update the type of metadata.metro.virtualModules + transformResult.metadata?.metro?.virtualModules, + ), }; return await transformJS(jsFile, context); diff --git a/packages/metro/src/Bundler.js b/packages/metro/src/Bundler.js index b70ef7cd64..4a542c55cd 100644 --- a/packages/metro/src/Bundler.js +++ b/packages/metro/src/Bundler.js @@ -10,6 +10,7 @@ */ import type {TransformResultWithSource} from './DeltaBundler'; +import type {VirtualModules} from './DeltaBundler/VirtualModules'; import type {TransformOptions} from './DeltaBundler/Worker'; import type EventEmitter from 'events'; import type {ConfigT} from 'metro-config'; @@ -35,8 +36,10 @@ export default class Bundler { .then(() => { config.reporter.update({type: 'transformer_load_started'}); this._transformer = new Transformer(config, { - getOrComputeSha1: filePath => - this._depGraph.getOrComputeSha1(filePath), + getOrComputeSha1: ( + filePath: string, + virtualModule?: ?VirtualModules, + ) => this._depGraph.getOrComputeSha1(filePath, virtualModule), }); config.reporter.update({type: 'transformer_load_done'}); }) @@ -71,6 +74,7 @@ export default class Bundler { transformOptions: TransformOptions, /** Optionally provide the file contents, this can be used to provide virtual contents for a file. */ fileBuffer?: Buffer, + virtualModules?: ?VirtualModules, ): Promise> { // We need to be sure that the DependencyGraph has been initialized. // TODO: Remove this ugly hack! @@ -80,6 +84,7 @@ export default class Bundler { filePath, transformOptions, fileBuffer, + virtualModules, ); } diff --git a/packages/metro/src/DeltaBundler.js b/packages/metro/src/DeltaBundler.js index cec7500757..1ae61544d2 100644 --- a/packages/metro/src/DeltaBundler.js +++ b/packages/metro/src/DeltaBundler.js @@ -20,6 +20,7 @@ import type { import type EventEmitter from 'events'; import DeltaCalculator from './DeltaBundler/DeltaCalculator'; +import {VirtualModules} from './DeltaBundler/VirtualModules'; export type { DeltaResult, @@ -43,9 +44,11 @@ export type { export default class DeltaBundler { _changeEventSource: EventEmitter; _deltaCalculators: Map, DeltaCalculator> = new Map(); + _virtualModules: VirtualModules; constructor(changeEventSource: EventEmitter) { this._changeEventSource = changeEventSource; + this._virtualModules = new VirtualModules(); } end(): void { @@ -68,6 +71,13 @@ export default class DeltaBundler { await deltaCalculator.getDelta({reset: true, shallow: options.shallow}); const graph = deltaCalculator.getGraph(); + this._virtualModules.addRawMap(graph.virtualModules.toRawMap()); + + graph.dependencies.forEach((value, key) => { + // $FlowFixMe[cannot-write] We need to mark the module as virtual + value.isVirtualModule = graph.virtualModules.get(key) != null; + }); + deltaCalculator.end(); return graph.dependencies; } diff --git a/packages/metro/src/DeltaBundler/Graph.js b/packages/metro/src/DeltaBundler/Graph.js index c28a31c91a..461b533e3e 100644 --- a/packages/metro/src/DeltaBundler/Graph.js +++ b/packages/metro/src/DeltaBundler/Graph.js @@ -47,6 +47,7 @@ import {fileMatchesContext} from '../lib/contextModule'; import CountingSet from '../lib/CountingSet'; import {isResolvedDependency} from '../lib/isResolvedDependency'; import {buildSubgraph} from './buildSubgraph'; +import {VirtualModules} from './VirtualModules'; import invariant from 'invariant'; import nullthrows from 'nullthrows'; @@ -133,6 +134,7 @@ export class Graph { +entryPoints: $ReadOnlySet; +transformOptions: TransformInputOptions; +dependencies: Dependencies = new Map(); + +virtualModules: VirtualModules = new VirtualModules(); +#importBundleNodes: Map< string, $ReadOnly<{ @@ -348,21 +350,30 @@ export class Graph { options: InternalOptions, moduleFilter?: (path: string) => boolean, ): Promise> { - const subGraph = await buildSubgraph(pathsToVisit, this.#resolvedContexts, { - resolve: options.resolve, - transform: async (absolutePath, requireContext) => { - options.onDependencyAdd(); - const result = await options.transform(absolutePath, requireContext); - options.onDependencyAdded(); - return result; - }, - shouldTraverse: (dependency: ResolvedDependency) => { - if (options.shallow || isWeakOrLazy(dependency, options)) { - return false; - } - return moduleFilter == null || moduleFilter(dependency.absolutePath); + const subGraph = await buildSubgraph( + pathsToVisit, + this.#resolvedContexts, + { + resolve: options.resolve, + transform: async (absolutePath, requireContext, virtualModules) => { + options.onDependencyAdd(); + const result = await options.transform( + absolutePath, + requireContext, + virtualModules, + ); + options.onDependencyAdded(); + return result; + }, + shouldTraverse: (dependency: ResolvedDependency) => { + if (options.shallow || isWeakOrLazy(dependency, options)) { + return false; + } + return moduleFilter == null || moduleFilter(dependency.absolutePath); + }, }, - }); + this.virtualModules, + ); return { added: new Set(), diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/getSourceMapInfo.js b/packages/metro/src/DeltaBundler/Serializers/helpers/getSourceMapInfo.js index 54069121d2..5cc7200a30 100644 --- a/packages/metro/src/DeltaBundler/Serializers/helpers/getSourceMapInfo.js +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/getSourceMapInfo.js @@ -37,7 +37,11 @@ export default function getSourceMapInfo( ...getJsOutput(module).data, isIgnored: options.shouldAddToIgnoreList(module), path: options?.getSourceUrl?.(module) ?? module.path, - source: options.excludeSource ? '' : getModuleSource(module), + source: + // TODO: Figure out sourceMaps for virtual modules. + options.excludeSource || module.isVirtualModule === true + ? '' + : getModuleSource(module), }; } diff --git a/packages/metro/src/DeltaBundler/Transformer.js b/packages/metro/src/DeltaBundler/Transformer.js index e7542595dc..d404acb933 100644 --- a/packages/metro/src/DeltaBundler/Transformer.js +++ b/packages/metro/src/DeltaBundler/Transformer.js @@ -10,6 +10,7 @@ */ import type {TransformResult, TransformResultWithSource} from '../DeltaBundler'; +import type {VirtualModules} from './VirtualModules'; import type {TransformerConfig, TransformOptions} from './Worker'; import type {ConfigT} from 'metro-config'; @@ -25,9 +26,10 @@ import path from 'path'; // eslint-disable-next-line import/no-commonjs const debug = require('debug')('Metro:Transformer'); -type GetOrComputeSha1Fn = string => Promise< - $ReadOnly<{content?: Buffer, sha1: string}>, ->; +type GetOrComputeSha1Fn = ( + path: string, + virtualModules?: ?VirtualModules, +) => Promise<$ReadOnly<{content?: Buffer, sha1: string}>>; export default class Transformer { _config: ConfigT; @@ -80,6 +82,7 @@ export default class Transformer { filePath: string, transformerOptions: TransformOptions, fileBuffer?: Buffer, + virtualModules?: ?VirtualModules, ): Promise> { const cache = this._cache; @@ -139,7 +142,7 @@ export default class Transformer { sha1 = crypto.createHash('sha1').update(fileBuffer).digest('hex'); content = fileBuffer; } else { - const result = await this._getSha1(filePath); + const result = await this._getSha1(filePath, virtualModules); sha1 = result.sha1; if (result.content) { content = result.content; @@ -169,6 +172,7 @@ export default class Transformer { localPath, transformerOptions, content, + virtualModules, ); // Only re-compute the full key if the SHA-1 changed. This is because diff --git a/packages/metro/src/DeltaBundler/VirtualModules.js b/packages/metro/src/DeltaBundler/VirtualModules.js new file mode 100644 index 0000000000..baa0ce9217 --- /dev/null +++ b/packages/metro/src/DeltaBundler/VirtualModules.js @@ -0,0 +1,48 @@ +/** + * 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 {VirtualModule, VirtualModulesRawMap} from './types'; + +export class VirtualModules { + #map_: VirtualModulesRawMap; + + constructor(initialMap?: ?VirtualModulesRawMap) { + this.#map_ = new Map(initialMap ?? []); + } + + toRawMap(): VirtualModulesRawMap { + return this.#map_; + } + + addRawMap(other: ?VirtualModulesRawMap) { + other?.forEach((value, key) => this.#map_.set(key, value)); + } + + get(mixedPath: string): ?VirtualModule { + if (this.#map_.has(mixedPath)) { + return this.#map_.get(mixedPath); + } + + const key = this.#map_ + .keys() + .find(relativePath => mixedPath.endsWith(relativePath)); + + if (key == null) { + return null; + } + + return this.#map_.get(key); + } + + set(relativePath: string, vModule: VirtualModule): void { + this.#map_.set(relativePath, vModule); + } +} diff --git a/packages/metro/src/DeltaBundler/Worker.flow.js b/packages/metro/src/DeltaBundler/Worker.flow.js index 0bcb528c32..593e9f9d61 100644 --- a/packages/metro/src/DeltaBundler/Worker.flow.js +++ b/packages/metro/src/DeltaBundler/Worker.flow.js @@ -9,13 +9,14 @@ * @oncall react_native */ -import type {TransformResult} from './types'; +import type {TransformResult, VirtualModulesRawMap} from './types'; import type {LogEntry} from 'metro-core/private/Logger'; import type { JsTransformerConfig, JsTransformOptions, } from 'metro-transform-worker'; +import {VirtualModules} from './VirtualModules'; import traverse from '@babel/traverse'; import crypto from 'crypto'; import fs from 'fs'; @@ -30,6 +31,7 @@ type TransformerInterface = { string, Buffer, JsTransformOptions, + ?VirtualModules, ): Promise>, }; @@ -72,6 +74,7 @@ export const transform = ( projectRoot: string, transformerConfig: TransformerConfig, fileBuffer?: Buffer, + virtualModulesRawMap?: ?VirtualModulesRawMap, ): Promise => { let data; @@ -81,12 +84,16 @@ export const transform = ( } else { data = fs.readFileSync(path.resolve(projectRoot, filename)); } + + const virtualModules = new VirtualModules(virtualModulesRawMap); + return transformFile( filename, data, transformOptions, projectRoot, transformerConfig, + virtualModules, ); }; @@ -100,6 +107,7 @@ async function transformFile( transformOptions: JsTransformOptions, projectRoot: string, transformerConfig: TransformerConfig, + virtualModules?: ?VirtualModules, ): Promise { // eslint-disable-next-line no-useless-call const Transformer: TransformerInterface = require.call( @@ -125,6 +133,25 @@ async function transformFile( transformOptions, ); + for (const dependency of result.dependencies) { + const {name, data: dependencyData} = dependency; + const virtualModule = virtualModules?.get(name); + + if (virtualModule != null) { + // $FlowFixMe[cannot-write] we update the dependency data here because now we have a guarantee that the map of Virtual Modules is up to date + dependencyData.isVirtualModule = true; + // $FlowFixMe[cannot-write] we update the dependency data here because now we have a guarantee that the map of Virtual Modules is up to date + dependencyData.absolutePath = virtualModule.absolutePath; + // $FlowFixMe[cannot-write] we update the dependency data here because now we have a guarantee that the map of Virtual Modules is up to date + dependencyData.code = virtualModule.code; + // $FlowFixMe[cannot-write] we update the dependency data here because now we have a guarantee that the map of Virtual Modules is up to date + dependencyData.type = virtualModule.type; + // TODO: Figure out sourceURL for virtual modules. + // // $FlowFixMe[cannot-write] we update the dependency data here because now we have a guarantee that the map of Virtual Modules is up to date + // dependencyData.sourceURL = virtualModule.sourceURL; + } + } + // The babel cache caches scopes and pathes for already traversed AST nodes. // Clearing the cache here since the nodes of the transformed file are no longer referenced. // This isn't stritcly necessary since the cache uses a WeakMap. However, WeakMap only permit @@ -138,6 +165,9 @@ async function transformFile( filename, ); + // $FlowFixMe[cannot-write] This has to be mutated in order to serialize it. + result.virtualModulesRawMap = result.virtualModules?.toRawMap(); + return { result, sha1, diff --git a/packages/metro/src/DeltaBundler/WorkerFarm.js b/packages/metro/src/DeltaBundler/WorkerFarm.js index 8dabfb84a1..353dee6a6a 100644 --- a/packages/metro/src/DeltaBundler/WorkerFarm.js +++ b/packages/metro/src/DeltaBundler/WorkerFarm.js @@ -10,6 +10,7 @@ */ import type {TransformResult} from '../DeltaBundler'; +import type {VirtualModules} from './VirtualModules'; import type {TransformerConfig, TransformOptions, Worker} from './Worker'; import type {ConfigT} from 'metro-config'; import type {Readable} from 'stream'; @@ -76,6 +77,7 @@ export default class WorkerFarm { filename: string, options: TransformOptions, fileBuffer?: Buffer, + virtualModules?: ?VirtualModules, ): Promise { try { const data = await this._worker.transform( @@ -84,6 +86,7 @@ export default class WorkerFarm { this._config.projectRoot, this._transformerConfig, fileBuffer, + virtualModules?.toRawMap?.(), ); Logger.log(data.transformFileStartLogEntry); diff --git a/packages/metro/src/DeltaBundler/buildSubgraph.js b/packages/metro/src/DeltaBundler/buildSubgraph.js index 847b943dce..49b51d3bd8 100644 --- a/packages/metro/src/DeltaBundler/buildSubgraph.js +++ b/packages/metro/src/DeltaBundler/buildSubgraph.js @@ -17,6 +17,7 @@ import type { TransformFn, TransformResultDependency, } from './types'; +import type {VirtualModules} from './VirtualModules'; import {deriveAbsolutePathFromContext} from '../lib/contextModule'; import {isResolvedDependency} from '../lib/isResolvedDependency'; @@ -32,6 +33,7 @@ function resolveDependencies( parentPath: string, dependencies: $ReadOnlyArray, resolve: ResolveFn, + virtualModules?: ?VirtualModules, ): { dependencies: Map, resolvedContexts: Map, @@ -45,7 +47,14 @@ function resolveDependencies( // `require.context` const {contextParams} = dep.data; - if (contextParams) { + const {isVirtualModule} = dep.data; + if (isVirtualModule === true) { + // $FlowFixMe[incompatible-type] Can't assert that `absolutePath` is defined. + maybeResolvedDep = { + absolutePath: dep.data.absolutePath, + data: dep, + }; + } else if (contextParams) { // Ensure the filepath has uniqueness applied to ensure multiple `require.context` // statements can be used to target the same file with different properties. const from = path.join(parentPath, '..', dep.name); @@ -70,7 +79,7 @@ function resolveDependencies( } else { try { maybeResolvedDep = { - absolutePath: resolve(parentPath, dep).filePath, + absolutePath: resolve(parentPath, dep, virtualModules).filePath, data: dep, }; } catch (error) { @@ -90,6 +99,7 @@ function resolveDependencies( `resolveDependencies: Found duplicate dependency key '${key}' in ${parentPath}`, ); } + // $FlowFixMe[incompatible-type] Flow doesn't like `absolutePath` here. maybeResolvedDeps.set(key, maybeResolvedDep); } @@ -103,6 +113,7 @@ export async function buildSubgraph( entryPaths: $ReadOnlySet, resolvedContexts: $ReadOnlyMap, {resolve, transform, shouldTraverse}: Parameters, + virtualModules?: ?VirtualModules, ): Promise<{ moduleData: Map>, errors: Map, @@ -114,12 +125,19 @@ export async function buildSubgraph( async function visit( absolutePath: string, requireContext: ?RequireContext, + virtualModules?: ?VirtualModules, ): Promise { if (visitedPaths.has(absolutePath)) { return; } visitedPaths.add(absolutePath); - const transformResult = await transform(absolutePath, requireContext); + const transformResult = await transform( + absolutePath, + requireContext, + virtualModules, + ); + + virtualModules?.addRawMap(transformResult?.virtualModulesRawMap); // Get the absolute path of all sub-dependencies (some of them could have been // moved but maintain the same relative path). @@ -127,6 +145,7 @@ export async function buildSubgraph( absolutePath, transformResult.dependencies, resolve, + virtualModules, ); moduleData.set(absolutePath, { @@ -144,6 +163,7 @@ export async function buildSubgraph( visit( dependency.absolutePath, resolutionResult.resolvedContexts.get(dependency.data.data.key), + virtualModules, ).catch(error => errors.set(dependency.absolutePath, error)), ), ); @@ -151,9 +171,11 @@ export async function buildSubgraph( await Promise.all( [...entryPaths].map(absolutePath => - visit(absolutePath, resolvedContexts.get(absolutePath)).catch(error => - errors.set(absolutePath, error), - ), + visit( + absolutePath, + resolvedContexts.get(absolutePath), + virtualModules, + ).catch(error => errors.set(absolutePath, error)), ), ); diff --git a/packages/metro/src/DeltaBundler/types.js b/packages/metro/src/DeltaBundler/types.js index c3eb601ebe..548eb9b87b 100644 --- a/packages/metro/src/DeltaBundler/types.js +++ b/packages/metro/src/DeltaBundler/types.js @@ -12,6 +12,7 @@ import type {RequireContext} from '../lib/contextModule'; import type {RequireContextParams} from '../ModuleGraph/worker/collectDependencies'; import type {Graph} from './Graph'; +import type {VirtualModules} from './VirtualModules'; import type {JsTransformOptions} from 'metro-transform-worker'; import CountingSet from '../lib/CountingSet'; @@ -56,6 +57,18 @@ export type TransformResultDependency = $ReadOnly<{ /** Context for requiring a collection of modules. */ contextParams?: RequireContextParams, + + /** True if the dependency is a virtual module, i.e. it's not yet registered in the Metro file system but it will be at the moment it's accessed. */ + isVirtualModule?: boolean, + + /** Full path to the module, provided only for virtual modules. */ + absolutePath?: string, + + /** Code of the module, provided only for virtual modules. */ + code?: string, + + /** Type of the dependency, provided only for virtual modules. */ + type?: 'sourceFile', }>, }>; @@ -77,6 +90,9 @@ export type Module = $ReadOnly<{ path: string, getSource: () => Buffer, unstable_transformResultKey?: ?string, + isVirtualModule?: boolean, + virtualModules?: ?VirtualModules, + virtualModulesRawMap?: ?VirtualModulesRawMap, }>; export type ModuleData = $ReadOnly<{ @@ -85,6 +101,9 @@ export type ModuleData = $ReadOnly<{ output: $ReadOnlyArray, getSource: () => Buffer, unstable_transformResultKey?: ?string, + isVirtualModule?: boolean, + virtualModules?: ?VirtualModules, + virtualModulesRawMap?: ?VirtualModulesRawMap, }>; export type Dependencies = Map>; @@ -117,6 +136,8 @@ export type TransformResult = $ReadOnly<{ dependencies: $ReadOnlyArray, output: $ReadOnlyArray, unstable_transformResultKey?: ?string, + virtualModules?: ?VirtualModules, + virtualModulesRawMap?: ?VirtualModulesRawMap, }>; export type TransformResultWithSource = $ReadOnly<{ @@ -124,14 +145,24 @@ export type TransformResultWithSource = $ReadOnly<{ getSource: () => Buffer, }>; +export type VirtualModule = $ReadOnly<{ + absolutePath: string, + code: string, + type: 'sourceFile', +}>; + +export type VirtualModulesRawMap = Map; + export type TransformFn = ( string, ?RequireContext, + virtualModules?: ?VirtualModules, ) => Promise>; export type ResolveFn = ( from: string, dependency: TransformResultDependency, + virtualModules?: ?VirtualModules, ) => BundlerResolution; export type AllowOptionalDependenciesWithOptions = { diff --git a/packages/metro/src/IncrementalBundler.js b/packages/metro/src/IncrementalBundler.js index e913446757..01b9f48987 100644 --- a/packages/metro/src/IncrementalBundler.js +++ b/packages/metro/src/IncrementalBundler.js @@ -217,6 +217,11 @@ export default class IncrementalBundler { this._deltaBundler, ); + graph.dependencies.forEach(module => { + // $FlowFixMe[cannot-write] We need to mark the module as virtual + module.isVirtualModule = graph.virtualModules.get(module.path) != null; + }); + return { prepend, graph, diff --git a/packages/metro/src/ModuleGraph/worker/collectDependencies.js b/packages/metro/src/ModuleGraph/worker/collectDependencies.js index 4106b560d9..5e09626633 100644 --- a/packages/metro/src/ModuleGraph/worker/collectDependencies.js +++ b/packages/metro/src/ModuleGraph/worker/collectDependencies.js @@ -8,6 +8,7 @@ * @flow */ +import type {VirtualModules} from '../../DeltaBundler/VirtualModules'; import type {NodePath} from '@babel/traverse'; import type {CallExpression, Identifier, StringLiteral} from '@babel/types'; import type { @@ -63,6 +64,14 @@ type DependencyData = $ReadOnly<{ locs: $ReadOnlyArray, /** Context for requiring a collection of modules. */ contextParams?: RequireContextParams, + /** True if the dependency is a future module, i.e. it's not yet registered in the Metro file system but it will be at the moment it's accessed. */ + isVirtualModule?: boolean, + /** Code of the module, provided only for future modules. */ + code?: string, + /** Full path to the module, provided only for future modules. */ + absolutePath?: string, + /** Type of the dependency, provided only for future modules. */ + type?: 'sourceFile', }>; export type MutableInternalDependency = { @@ -99,6 +108,8 @@ export type Options = $ReadOnly<{ /** Enable `require.context` statements which can be used to import multiple files in a directory. */ unstable_allowRequireContext: boolean, unstable_isESMImportAtSource?: ?(BabelSourceLocation) => boolean, + /** Map of registered virtual modules, i.e. modules not yet registered in the Metro file system but available for bundling. */ + virtualModules?: ?VirtualModules, }>; export type CollectedDependencies = $ReadOnly<{ @@ -291,6 +302,15 @@ export default function collectDependencies( const dependencies = new Array(collectedDependencies.length); for (const {index, name, ...dependencyData} of collectedDependencies) { + const virtualModule = options.virtualModules?.get(name); + + if (virtualModule != null) { + dependencyData.isVirtualModule = true; + dependencyData.absolutePath = virtualModule.absolutePath; + dependencyData.code = virtualModule.code; + dependencyData.type = virtualModule.type; + } + dependencies[index] = { name, data: dependencyData, diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index ee08251f6f..2ff7316e80 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -1176,6 +1176,12 @@ export default class Server { // order as in a plain JS bundle. _getSortedModules(graph: ReadOnlyGraph<>): $ReadOnlyArray> { const modules = [...graph.dependencies.values()]; + graph.dependencies.forEach(module => { + // $FlowFixMe[cannot-write] + // $FlowFixMe[prop-missing] We need to mark the module as virtual + module.isVirtualModule = graph.virtualModules.get(module.path) != null; + }); + // Assign IDs to modules in a consistent order for (const module of modules) { this._createModuleId(module.path); diff --git a/packages/metro/src/lib/transformHelpers.js b/packages/metro/src/lib/transformHelpers.js index b8b5e0fcbe..c84091b587 100644 --- a/packages/metro/src/lib/transformHelpers.js +++ b/packages/metro/src/lib/transformHelpers.js @@ -16,6 +16,7 @@ import type { TransformInputOptions, TransformResultDependency, } from '../DeltaBundler/types'; +import type {VirtualModules} from '../DeltaBundler/VirtualModules'; import type {TransformOptions} from '../DeltaBundler/Worker'; import type {ResolverInputOptions} from '../shared/types'; import type {RequireContext} from './contextModule'; @@ -153,7 +154,11 @@ export async function getTransformFn( ); const assetExts = new Set(config.resolver.assetExts); - return async (modulePath: string, requireContext: ?RequireContext) => { + return async ( + modulePath: string, + requireContext: ?RequireContext, + virtualModules?: ?VirtualModules, + ) => { let templateBuffer: Buffer; if (requireContext) { @@ -190,6 +195,7 @@ export async function getTransformFn( ), }, templateBuffer, + virtualModules, ); }; } diff --git a/packages/metro/src/node-haste/DependencyGraph.js b/packages/metro/src/node-haste/DependencyGraph.js index 1dc470932a..1498709c87 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -13,6 +13,7 @@ import type { BundlerResolution, TransformResultDependency, } from '../DeltaBundler/types'; +import type {VirtualModules} from '../DeltaBundler/VirtualModules'; import type {ResolverInputOptions} from '../shared/types'; import type Package from './Package'; import type {ConfigT} from 'metro-config'; @@ -267,7 +268,22 @@ export default class DependencyGraph extends EventEmitter { */ async getOrComputeSha1( mixedPath: string, + virtualModules?: ?VirtualModules, ): Promise<{content?: Buffer, sha1: string}> { + const virtualModule = virtualModules?.get(mixedPath); + + if (virtualModule) { + // For future modules, we can't compute the sha1 based on the file contents + // since the file doesn't exist yet. Instead, we generate a sha1 based on + // the current time to ensure it will force a refresh of the transform cache. + const createHash = require('crypto').createHash; + return { + sha1: createHash('sha1') + .update(performance.now().toString()) + .digest('hex'), + content: Buffer.from(virtualModule.code, 'utf8'), + }; + } const result = await this._fileSystem.getOrComputeSha1(mixedPath); if (!result || !result.sha1) { throw new Error(`Failed to get the SHA-1 for: ${mixedPath}.