diff --git a/packages/metro-resolver/src/__tests__/utils.js b/packages/metro-resolver/src/__tests__/utils.js index d8393fadc4..cc82c88229 100644 --- a/packages/metro-resolver/src/__tests__/utils.js +++ b/packages/metro-resolver/src/__tests__/utils.js @@ -76,6 +76,7 @@ export function createResolutionContext( realPath: candidate.realPath, }; }, + isESMImport: false, mainFields: ['browser', 'main'], nodeModulesPaths: [], preferNativePlatform: false, diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index e092e255f2..efd05da12b 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -157,6 +157,17 @@ export type ResolutionContext = $ReadOnly<{ */ dependency?: TransformResultDependency, + /** + * Whether the dependency to be resolved was declared with an ESM import, + * ("import x from 'y'" or "await import('z')"), or a CommonJS "require". + * Corresponds to the criteria Node.js uses to assert an "import" + * resolution condition, vs "require". + * + * Always equal to dependency.data.isESMImport where dependency is provided, + * but may be used for resolution. + */ + isESMImport: boolean, + /** * Synchonously returns information about a given absolute path, including * whether it exists, whether it is a file or directory, and its absolute diff --git a/packages/metro-resolver/types/types.d.ts b/packages/metro-resolver/types/types.d.ts index 6d22a538b3..a497b4ffd2 100644 --- a/packages/metro-resolver/types/types.d.ts +++ b/packages/metro-resolver/types/types.d.ts @@ -133,6 +133,17 @@ export interface ResolutionContext { */ readonly dependency?: TransformResultDependency; + /** + * Whether the dependency to be resolved was declared with an ESM import, + * ("import x from 'y'" or "await import('z')"), or a CommonJS "require". + * Corresponds to the criteria Node.js uses to assert an "import" + * resolution condition, vs "require". + * + * Always equal to dependency.data.isESMImport where dependency is provided, + * but may be used for resolution. + */ + readonly isESMImport: boolean; + /** * Synchonously returns information about a given absolute path, including * whether it exists, whether it is a file or directory, and its absolute diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js index 5a7be728b6..f3faed1421 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js @@ -41,7 +41,10 @@ const fooModule: Module<> = { './bar', { absolutePath: '/root/bar', - data: {data: {asyncType: null, locs: [], key: './bar'}, name: './bar'}, + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: './bar'}, + name: './bar', + }, }, ], ]), diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index 23c678074e..c784561b96 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -32,7 +32,10 @@ function createModule( dep, { absolutePath: `/root/${dep}.js`, - data: {data: {asyncType: null, locs: [], key: dep}, name: dep}, + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: dep}, + name: dep, + }, }, ]), ), diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/sourceMapString-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/sourceMapString-test.js index 592e7928dd..fbe8231c23 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/sourceMapString-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/sourceMapString-test.js @@ -44,7 +44,10 @@ const fooModule: Module<> = { './bar', { absolutePath: '/root/bar.js', - data: {data: {asyncType: null, locs: [], key: './bar'}, name: './bar'}, + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: './bar'}, + name: './bar', + }, }, ], ]), diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js b/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js index d95b4575c6..0ca2390162 100644 --- a/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js @@ -30,14 +30,20 @@ beforeEach(() => { 'bar', { absolutePath: '/bar.js', - data: {data: {asyncType: null, locs: [], key: 'bar'}, name: 'bar'}, + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: 'bar'}, + name: 'bar', + }, }, ], [ 'baz', { absolutePath: '/baz.js', - data: {data: {asyncType: null, locs: [], key: 'baz'}, name: 'baz'}, + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: 'baz'}, + name: 'baz', + }, }, ], ]), diff --git a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-context-test.js b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-context-test.js index f5657f85ad..2cf8f29a48 100644 --- a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-context-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-context-test.js @@ -91,7 +91,12 @@ describe('DeltaCalculator + require.context', () => { absolutePath: '/ctx?ctx=xxx', data: { name: 'ctx', - data: {key: 'ctx?ctx=xxx', asyncType: null, locs: []}, + data: { + key: 'ctx?ctx=xxx', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], @@ -109,7 +114,12 @@ describe('DeltaCalculator + require.context', () => { absolutePath: '/ctx/foo', data: { name: 'foo', - data: {key: 'foo', asyncType: null, locs: []}, + data: { + key: 'foo', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], diff --git a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js index 25c56763c9..33570b3218 100644 --- a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js @@ -94,7 +94,12 @@ describe.each(['linux', 'win32'])('DeltaCalculator (%s)', osPlatform => { absolutePath: p('/foo'), data: { name: 'foo', - data: {key: 'foo', asyncType: null, locs: []}, + data: { + key: 'foo', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], @@ -104,7 +109,12 @@ describe.each(['linux', 'win32'])('DeltaCalculator (%s)', osPlatform => { absolutePath: p('/bar'), data: { name: 'bar', - data: {key: 'bar', asyncType: null, locs: []}, + data: { + key: 'bar', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], @@ -114,7 +124,12 @@ describe.each(['linux', 'win32'])('DeltaCalculator (%s)', osPlatform => { absolutePath: p('/baz'), data: { name: 'baz', - data: {key: 'baz', asyncType: null, locs: []}, + data: { + key: 'baz', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], @@ -132,7 +147,12 @@ describe.each(['linux', 'win32'])('DeltaCalculator (%s)', osPlatform => { absolutePath: p('/qux'), data: { name: 'qux', - data: {key: 'qux', asyncType: null, locs: []}, + data: { + key: 'qux', + asyncType: null, + isESMImport: false, + locs: [], + }, }, }, ], diff --git a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js index ee75d3230c..de1df03c87 100644 --- a/packages/metro/src/DeltaBundler/__tests__/Graph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/Graph-test.js @@ -369,6 +369,7 @@ beforeEach(async () => { name: dep.name, data: { asyncType: null, + isESMImport: false, // $FlowFixMe[missing-empty-array-annot] locs: [], // $FlowFixMe[incompatible-call] @@ -3498,6 +3499,7 @@ describe('reorderGraph', () => { data: { data: { asyncType: null, + isESMImport: false, locs: [], key: path.substr(1), }, diff --git a/packages/metro/src/DeltaBundler/__tests__/__snapshots__/Graph-test.js.snap b/packages/metro/src/DeltaBundler/__tests__/__snapshots__/Graph-test.js.snap index eec94bf247..50aa3a8239 100644 --- a/packages/metro/src/DeltaBundler/__tests__/__snapshots__/Graph-test.js.snap +++ b/packages/metro/src/DeltaBundler/__tests__/__snapshots__/Graph-test.js.snap @@ -10,6 +10,7 @@ TestGraph { "data": Object { "data": Object { "asyncType": null, + "isESMImport": false, "key": "LB7P4TKrvfdUdViBXGaVopqz7Os=", "locs": Array [], }, @@ -39,6 +40,7 @@ TestGraph { "data": Object { "data": Object { "asyncType": null, + "isESMImport": false, "key": "W+de6an7x9bzpev84O0W/hS4K8U=", "locs": Array [], }, @@ -50,6 +52,7 @@ TestGraph { "data": Object { "data": Object { "asyncType": null, + "isESMImport": false, "key": "x6e9Oz1JO0QPfIBBjUad2qqGFjI=", "locs": Array [], }, @@ -137,6 +140,7 @@ TestGraph { "data": Object { "data": Object { "asyncType": null, + "isESMImport": false, "key": "LB7P4TKrvfdUdViBXGaVopqz7Os=", "locs": Array [], }, diff --git a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js index ae5f497896..c7fd5b45d7 100644 --- a/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/buildSubgraph-test.js @@ -17,10 +17,17 @@ import nullthrows from 'nullthrows'; const makeTransformDep = ( name: string, asyncType: null | 'weak' | 'async' = null, + isESMImport: boolean = false, contextParams?: RequireContextParams, ): TransformResultDependency => ({ name, - data: {key: 'key-' + name, asyncType, locs: [], contextParams}, + data: { + key: 'key-' + name + (isESMImport ? '-import' : ''), + asyncType, + isESMImport, + locs: [], + contextParams, + }, }); class BadTransformError extends Error {} @@ -40,7 +47,7 @@ describe('GraphTraversal', () => { [ '/entryWithContext', [ - makeTransformDep('virtual', null, { + makeTransformDep('virtual', null, false, { filter: { pattern: 'contextMatch.*', flags: 'i', diff --git a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js index a980636d88..dd35c49ea3 100644 --- a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js @@ -47,6 +47,7 @@ function dep(name: string): TransformResultDependency { name, data: { asyncType: null, + isESMImport: false, key: name, locs: [], }, diff --git a/packages/metro/src/DeltaBundler/types.flow.js b/packages/metro/src/DeltaBundler/types.flow.js index aff6393627..d32ebdb66d 100644 --- a/packages/metro/src/DeltaBundler/types.flow.js +++ b/packages/metro/src/DeltaBundler/types.flow.js @@ -44,6 +44,11 @@ export type TransformResultDependency = $ReadOnly<{ * If not null, this dependency is due to a dynamic `import()` or `__prefetchImport()` call. */ asyncType: AsyncDependencyType | null, + /** + * True if the dependency is declared with a static "import x from 'y'" or + * an import() call. + */ + isESMImport: boolean, /** * The dependency is enclosed in a try/catch block. */ diff --git a/packages/metro/src/HmrServer.js b/packages/metro/src/HmrServer.js index 6c7d7c1b48..aee9361173 100644 --- a/packages/metro/src/HmrServer.js +++ b/packages/metro/src/HmrServer.js @@ -125,6 +125,7 @@ class HmrServer { data: { key: entryFile, asyncType: null, + isESMImport: false, locs: [], }, }, diff --git a/packages/metro/src/ModuleGraph/worker/collectDependencies.js b/packages/metro/src/ModuleGraph/worker/collectDependencies.js index 717c3b4541..d3e6b088e2 100644 --- a/packages/metro/src/ModuleGraph/worker/collectDependencies.js +++ b/packages/metro/src/ModuleGraph/worker/collectDependencies.js @@ -29,6 +29,7 @@ const {isImport} = types; type ImportDependencyOptions = $ReadOnly<{ asyncType: AsyncDependencyType, + isESMImport: boolean, }>; export type Dependency = $ReadOnly<{ @@ -56,6 +57,11 @@ type DependencyData = $ReadOnly<{ // If null, then the dependency is synchronous. // (ex. `require('foo')`) asyncType: AsyncDependencyType | null, + // If true, the dependency is declared using an ESM import, e.g. + // "import x from 'y'" or "await import('z')". A resolver should typically + // use this to assert either "import" or "require" for conditional exports + // and subpath imports. + isESMImport: boolean, isOptional?: boolean, locs: $ReadOnlyArray, /** Context for requiring a collection of modules. */ @@ -171,6 +177,7 @@ function collectDependencies( if (isImport(callee)) { processImportCall(path, state, { asyncType: 'async', + isESMImport: true, }); return; } @@ -178,6 +185,7 @@ function collectDependencies( if (name === '__prefetchImport' && !path.scope.getBinding(name)) { processImportCall(path, state, { asyncType: 'prefetch', + isESMImport: true, }); return; } @@ -235,6 +243,7 @@ function collectDependencies( ) { processImportCall(path, state, { asyncType: 'maybeSync', + isESMImport: false, // @nocommit - verify whether require.unstable_importMaybeSync is ESM or CJS }); visited.add(path.node); return; @@ -408,6 +417,7 @@ function processRequireContextCall( // Capture the matching context contextParams, asyncType: null, + isESMImport: false, optional: isOptionalDependency(directory, path, state), }, path, @@ -433,6 +443,7 @@ function processResolveWeakCall( { name, asyncType: 'weak', + isESMImport: false, optional: isOptionalDependency(name, path, state), }, path, @@ -458,6 +469,7 @@ See: https://github.com/facebook/metro/pull/1343`, { name: path.node.source.value, asyncType: null, + isESMImport: true, optional: false, }, path, @@ -481,6 +493,7 @@ function processImportCall( { name, asyncType: options.asyncType, + isESMImport: options.isESMImport, optional: isOptionalDependency(name, path, state), }, path, @@ -528,6 +541,7 @@ function processRequireCall( { name, asyncType: null, + isESMImport: false, optional: isOptionalDependency(name, path, state), }, path, @@ -555,6 +569,7 @@ function getNearestLocFromPath(path: NodePath<>): ?BabelSourceLocation { export type ImportQualifier = $ReadOnly<{ name: string, asyncType: AsyncDependencyType | null, + isESMImport: boolean, optional: boolean, contextParams?: RequireContextParams, }>; @@ -801,13 +816,16 @@ function createModuleNameLiteral(dependency: InternalDependency) { /** * Given an import qualifier, return a key used to register the dependency. - * Generally this return the `ImportQualifier.name` property, but more - * attributes can be appended to distinguish various combinations that would - * otherwise conflict. + * Attributes can be appended to distinguish various combinations that would + * otherwise be considered the same dependency edge. + * + * For example, the following dependencies would collapse into a single edge + * if they simply utilized the `name` property: * - * For example, the following case would have collision issues if they all utilized the `name` property: * ``` * require('./foo'); + * import foo from './foo' + * await import('./foo') * require.context('./foo'); * require.context('./foo', true, /something/); * require.context('./foo', false, /something/); @@ -817,14 +835,13 @@ function createModuleNameLiteral(dependency: InternalDependency) { * This method should be utilized by `registerDependency`. */ function getKeyForDependency(qualifier: ImportQualifier): string { - let key = qualifier.name; + const {asyncType, contextParams, isESMImport, name} = qualifier; - const {asyncType} = qualifier; - if (asyncType) { - key += ['', asyncType].join('\0'); + let key = [name, isESMImport ? 'import' : 'require'].join('\0'); + if (asyncType != null) { + key += '\0' + asyncType; } - const {contextParams} = qualifier; // Add extra qualifiers when using `require.context` to prevent collisions. if (contextParams) { // NOTE(EvanBacon): Keep this synchronized with `RequireContextParams`, if any other properties are added @@ -854,6 +871,7 @@ class DependencyRegistry { const newDependency: MutableInternalDependency = { name: qualifier.name, asyncType: qualifier.asyncType, + isESMImport: qualifier.isESMImport, locs: [], index: this._dependencies.size, key: crypto.createHash('sha1').update(key).digest('base64'), diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index 04a1c1a8b8..afd448c935 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -1420,7 +1420,7 @@ class Server { : this._config.projectRoot; return resolutionFn(`${rootDir}/.`, { name: filePath, - data: {key: filePath, locs: [], asyncType: null}, + data: {key: filePath, locs: [], asyncType: null, isESMImport: false}, }).filePath; } diff --git a/packages/metro/src/Server/__tests__/Server-test.js b/packages/metro/src/Server/__tests__/Server-test.js index 2975de2dd7..93c9321197 100644 --- a/packages/metro/src/Server/__tests__/Server-test.js +++ b/packages/metro/src/Server/__tests__/Server-test.js @@ -211,7 +211,12 @@ describe('processRequest', () => { { absolutePath: '/root/foo.js', data: { - data: {asyncType: null, key: 'foo', locs: []}, + data: { + asyncType: null, + isESMImport: false, + key: 'foo', + locs: [], + }, name: 'foo', }, }, diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index 433cd2bc97..66f90bf389 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -118,6 +118,7 @@ class ModuleResolver { data: { key: this._options.emptyModulePath, asyncType: null, + isESMImport: false, locs: [], }, }, @@ -165,6 +166,7 @@ class ModuleResolver { doesFileExist, extraNodeModules, fileSystemLookup, + isESMImport: dependency.data.isESMImport, mainFields, nodeModulesPaths, preferNativePlatform,