Skip to content

Commit b393bcb

Browse files
authored
fix(node-resolve): Fix bug where JS import was converted to a TS import, resulting in an error when using export maps (#921)
1 parent f930e31 commit b393bcb

File tree

17 files changed

+242
-99
lines changed

17 files changed

+242
-99
lines changed

packages/node-resolve/src/fs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ export async function fileExists(filePath) {
1616
return false;
1717
}
1818
}
19+
20+
export async function resolveSymlink(path) {
21+
return (await fileExists(path)) ? realpath(path) : path;
22+
}

packages/node-resolve/src/index.js

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,30 +121,17 @@ export function nodeResolve(opts = {}) {
121121
return false;
122122
}
123123

124-
const importSpecifierList = [];
124+
const importSpecifierList = [importee];
125125

126126
if (importer === undefined && !importee[0].match(/^\.?\.?\//)) {
127127
// For module graph roots (i.e. when importer is undefined), we
128128
// need to handle 'path fragments` like `foo/bar` that are commonly
129129
// found in rollup config files. If importee doesn't look like a
130130
// relative or absolute path, we make it relative and attempt to
131-
// resolve it. If we don't find anything, we try resolving it as we
132-
// got it.
131+
// resolve it.
133132
importSpecifierList.push(`./${importee}`);
134133
}
135134

136-
const importeeIsBuiltin = builtins.has(importee);
137-
138-
if (importeeIsBuiltin) {
139-
// The `resolve` library will not resolve packages with the same
140-
// name as a node built-in module. If we're resolving something
141-
// that's a builtin, and we don't prefer to find built-ins, we
142-
// first try to look up a local module with that name. If we don't
143-
// find anything, we resolve the builtin which just returns back
144-
// the built-in's name.
145-
importSpecifierList.push(`${importee}/`);
146-
}
147-
148135
// TypeScript files may import '.js' to refer to either '.ts' or '.tsx'
149136
if (importer && importee.endsWith('.js')) {
150137
for (const ext of ['.ts', '.tsx']) {
@@ -154,8 +141,6 @@ export function nodeResolve(opts = {}) {
154141
}
155142
}
156143

157-
importSpecifierList.push(importee);
158-
159144
const warn = (...args) => context.warn(...args);
160145
const isRequire =
161146
opts && opts.custom && opts.custom['node-resolve'] && opts.custom['node-resolve'].isRequire;
@@ -180,6 +165,7 @@ export function nodeResolve(opts = {}) {
180165
ignoreSideEffectsForRoot
181166
});
182167

168+
const importeeIsBuiltin = builtins.has(importee);
183169
const resolved =
184170
importeeIsBuiltin && preferBuiltins
185171
? {

packages/node-resolve/src/package/resolvePackageImports.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async function resolvePackageImports({
3333
}
3434

3535
if (importSpecifier === '#' || importSpecifier.startsWith('#/')) {
36-
throw new InvalidModuleSpecifierError(context, 'Invalid import specifier.');
36+
throw new InvalidModuleSpecifierError(context, true, 'Invalid import specifier.');
3737
}
3838

3939
return resolvePackageImportsExports(context, {

packages/node-resolve/src/package/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export class InvalidConfigurationError extends ResolveError {
6464
}
6565

6666
export class InvalidModuleSpecifierError extends ResolveError {
67-
constructor(context, internal) {
68-
super(createErrorMsg(context, internal));
67+
constructor(context, internal, reason) {
68+
super(createErrorMsg(context, reason, internal));
6969
}
7070
}
7171

packages/node-resolve/src/resolveImportSpecifiers.js

Lines changed: 172 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { dirname } from 'path';
77
import resolve from 'resolve';
88

99
import { getPackageInfo, getPackageName } from './util';
10-
import { fileExists, realpath } from './fs';
10+
import { resolveSymlink } from './fs';
1111
import { isDirCached, isFileCached, readCachedFile } from './cache';
1212
import resolvePackageExports from './package/resolvePackageExports';
1313
import resolvePackageImports from './package/resolvePackageImports';
@@ -34,11 +34,8 @@ async function getPackageJson(importer, pkgName, resolveOptions, moduleDirectori
3434
}
3535
}
3636

37-
async function resolveId({
38-
importer,
37+
async function resolveIdClassic({
3938
importSpecifier,
40-
exportConditions,
41-
warn,
4239
packageInfoCache,
4340
extensions,
4441
mainFields,
@@ -85,21 +82,48 @@ async function resolveId({
8582
};
8683

8784
let location;
85+
try {
86+
location = await resolveImportPath(importSpecifier, resolveOptions);
87+
} catch (error) {
88+
if (error.code !== 'MODULE_NOT_FOUND') {
89+
throw error;
90+
}
91+
return null;
92+
}
8893

89-
const pkgName = getPackageName(importSpecifier);
94+
return {
95+
location: preserveSymlinks ? location : await resolveSymlink(location),
96+
hasModuleSideEffects,
97+
hasPackageEntry,
98+
packageBrowserField,
99+
packageInfo
100+
};
101+
}
102+
103+
async function resolveWithExportMap({
104+
importer,
105+
importSpecifier,
106+
exportConditions,
107+
packageInfoCache,
108+
extensions,
109+
mainFields,
110+
preserveSymlinks,
111+
useBrowserOverrides,
112+
baseDir,
113+
moduleDirectories,
114+
rootDir,
115+
ignoreSideEffectsForRoot
116+
}) {
90117
if (importSpecifier.startsWith('#')) {
91118
// this is a package internal import, resolve using package imports field
92119
const resolveResult = await resolvePackageImports({
93120
importSpecifier,
94121
importer,
95122
moduleDirs: moduleDirectories,
96123
conditions: exportConditions,
97-
resolveId(id, parent) {
98-
return resolveId({
124+
resolveId(id /* , parent*/) {
125+
return resolveIdClassic({
99126
importSpecifier: id,
100-
importer: parent,
101-
exportConditions,
102-
warn,
103127
packageInfoCache,
104128
extensions,
105129
mainFields,
@@ -110,71 +134,91 @@ async function resolveId({
110134
});
111135
}
112136
});
113-
location = fileURLToPath(resolveResult);
114-
} else if (pkgName) {
137+
138+
const location = fileURLToPath(resolveResult);
139+
return {
140+
location: preserveSymlinks ? location : await resolveSymlink(location),
141+
hasModuleSideEffects: () => null,
142+
hasPackageEntry: true,
143+
packageBrowserField: false,
144+
// eslint-disable-next-line no-undefined
145+
packageInfo: undefined
146+
};
147+
}
148+
149+
const pkgName = getPackageName(importSpecifier);
150+
if (pkgName) {
115151
// it's a bare import, find the package.json and resolve using package exports if available
152+
let hasModuleSideEffects = () => null;
153+
let hasPackageEntry = true;
154+
let packageBrowserField = false;
155+
let packageInfo;
156+
157+
const filter = (pkg, pkgPath) => {
158+
const info = getPackageInfo({
159+
cache: packageInfoCache,
160+
extensions,
161+
pkg,
162+
pkgPath,
163+
mainFields,
164+
preserveSymlinks,
165+
useBrowserOverrides,
166+
rootDir,
167+
ignoreSideEffectsForRoot
168+
});
169+
170+
({ packageInfo, hasModuleSideEffects, hasPackageEntry, packageBrowserField } = info);
171+
172+
return info.cachedPkg;
173+
};
174+
175+
const resolveOptions = {
176+
basedir: baseDir,
177+
readFile: readCachedFile,
178+
isFile: isFileCached,
179+
isDirectory: isDirCached,
180+
extensions,
181+
includeCoreModules: false,
182+
moduleDirectory: moduleDirectories,
183+
preserveSymlinks,
184+
packageFilter: filter
185+
};
186+
116187
const result = await getPackageJson(importer, pkgName, resolveOptions, moduleDirectories);
117188

118189
if (result && result.pkgJson.exports) {
119-
const { pkgJson, pkgJsonPath, pkgPath } = result;
120-
try {
121-
const subpath =
122-
pkgName === importSpecifier ? '.' : `.${importSpecifier.substring(pkgName.length)}`;
123-
const pkgURL = pathToFileURL(`${pkgPath}/`);
124-
125-
const context = {
126-
importer,
127-
importSpecifier,
128-
moduleDirs: moduleDirectories,
129-
pkgURL,
130-
pkgJsonPath,
131-
conditions: exportConditions
132-
};
133-
const resolvedPackageExport = await resolvePackageExports(
134-
context,
135-
subpath,
136-
pkgJson.exports
137-
);
138-
location = fileURLToPath(resolvedPackageExport);
139-
} catch (error) {
140-
if (error instanceof ResolveError) {
141-
return error;
142-
}
143-
throw error;
144-
}
145-
}
146-
}
190+
const { pkgJson, pkgJsonPath } = result;
191+
const subpath =
192+
pkgName === importSpecifier ? '.' : `.${importSpecifier.substring(pkgName.length)}`;
193+
const pkgDr = pkgJsonPath.replace('package.json', '');
194+
const pkgURL = pathToFileURL(pkgDr);
147195

148-
if (!location) {
149-
// package has no imports or exports, use classic node resolve
150-
try {
151-
location = await resolveImportPath(importSpecifier, resolveOptions);
152-
} catch (error) {
153-
if (error.code !== 'MODULE_NOT_FOUND') {
154-
throw error;
196+
const context = {
197+
importer,
198+
importSpecifier,
199+
moduleDirs: moduleDirectories,
200+
pkgURL,
201+
pkgJsonPath,
202+
conditions: exportConditions
203+
};
204+
const resolvedPackageExport = await resolvePackageExports(context, subpath, pkgJson.exports);
205+
const location = fileURLToPath(resolvedPackageExport);
206+
if (location) {
207+
return {
208+
location: preserveSymlinks ? location : await resolveSymlink(location),
209+
hasModuleSideEffects,
210+
hasPackageEntry,
211+
packageBrowserField,
212+
packageInfo
213+
};
155214
}
156-
return null;
157215
}
158216
}
159217

160-
if (!preserveSymlinks) {
161-
if (await fileExists(location)) {
162-
location = await realpath(location);
163-
}
164-
}
165-
166-
return {
167-
location,
168-
hasModuleSideEffects,
169-
hasPackageEntry,
170-
packageBrowserField,
171-
packageInfo
172-
};
218+
return null;
173219
}
174220

175-
// Resolve module specifiers in order. Promise resolves to the first module that resolves
176-
// successfully, or the error that resulted from the last attempted module resolution.
177-
export default async function resolveImportSpecifiers({
221+
async function resolveWithClassic({
178222
importer,
179223
importSpecifierList,
180224
exportConditions,
@@ -189,11 +233,9 @@ export default async function resolveImportSpecifiers({
189233
rootDir,
190234
ignoreSideEffectsForRoot
191235
}) {
192-
let lastResolveError;
193-
194236
for (let i = 0; i < importSpecifierList.length; i++) {
195237
// eslint-disable-next-line no-await-in-loop
196-
const result = await resolveId({
238+
const result = await resolveIdClassic({
197239
importer,
198240
importSpecifier: importSpecifierList[i],
199241
exportConditions,
@@ -209,16 +251,71 @@ export default async function resolveImportSpecifiers({
209251
ignoreSideEffectsForRoot
210252
});
211253

212-
if (result instanceof ResolveError) {
213-
lastResolveError = result;
214-
} else if (result) {
254+
if (result) {
215255
return result;
216256
}
217257
}
218258

219-
if (lastResolveError) {
220-
// only log the last failed resolve error
221-
warn(lastResolveError);
222-
}
223259
return null;
224260
}
261+
262+
// Resolves to the module if found or `null`.
263+
// The first import specificer will first be attempted with the exports algorithm.
264+
// If this is unsuccesful because export maps are not being used, then all of `importSpecifierList`
265+
// will be tried with the classic resolution algorithm
266+
export default async function resolveImportSpecifiers({
267+
importer,
268+
importSpecifierList,
269+
exportConditions,
270+
warn,
271+
packageInfoCache,
272+
extensions,
273+
mainFields,
274+
preserveSymlinks,
275+
useBrowserOverrides,
276+
baseDir,
277+
moduleDirectories,
278+
rootDir,
279+
ignoreSideEffectsForRoot
280+
}) {
281+
try {
282+
const exportMapRes = await resolveWithExportMap({
283+
importer,
284+
importSpecifier: importSpecifierList[0],
285+
exportConditions,
286+
packageInfoCache,
287+
extensions,
288+
mainFields,
289+
preserveSymlinks,
290+
useBrowserOverrides,
291+
baseDir,
292+
moduleDirectories,
293+
rootDir,
294+
ignoreSideEffectsForRoot
295+
});
296+
if (exportMapRes) return exportMapRes;
297+
} catch (error) {
298+
if (error instanceof ResolveError) {
299+
warn(error);
300+
return null;
301+
}
302+
throw error;
303+
}
304+
305+
// package has no imports or exports, use classic node resolve
306+
return resolveWithClassic({
307+
importer,
308+
importSpecifierList,
309+
exportConditions,
310+
warn,
311+
packageInfoCache,
312+
extensions,
313+
mainFields,
314+
preserveSymlinks,
315+
useBrowserOverrides,
316+
baseDir,
317+
moduleDirectories,
318+
rootDir,
319+
ignoreSideEffectsForRoot
320+
});
321+
}

packages/node-resolve/test/fixtures/node_modules/exports-error-no-fallback/main.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)