Skip to content

Commit daf06f7

Browse files
authored
fix(node-resolve): Implement package exports / imports resolution algorithm according to Node documentation (#1549)
This fixes the package exports and imports resolution algorithm by strictly following the Node API documentation. For backwards compatibility a new option `allowExportsFolderMapping` is introduced which will enable deprecated folder mappings. Test case included Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 49dcfe5 commit daf06f7

19 files changed

+464
-233
lines changed

packages/node-resolve/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,31 @@ rootDir: path.join(process.cwd(), '..')
175175

176176
If you use the `sideEffects` property in the package.json, by default this is respected for files in the root package. Set to `true` to ignore the `sideEffects` configuration for the root package.
177177

178+
### `allowExportsFolderMapping`
179+
180+
Older Node versions supported exports mappings of folders like
181+
182+
```json
183+
{
184+
"exports": {
185+
"./foo/": "./dist/foo/"
186+
}
187+
}
188+
```
189+
190+
This was deprecated with Node 14 and removed in Node 17, instead it is recommended to use exports patterns like
191+
192+
```json
193+
{
194+
"exports": {
195+
"./foo/*": "./dist/foo/*"
196+
}
197+
}
198+
```
199+
200+
But for backwards compatibility this behavior is still supported by enabling the `allowExportsFolderMapping` (defaults to `true`).
201+
The default value might change in a futur major release.
202+
178203
## Preserving symlinks
179204

180205
This plugin honours the rollup [`preserveSymlinks`](https://rollupjs.org/guide/en/#preservesymlinks) option.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync } from 'fs';
22

33
import json from '@rollup/plugin-json';
4+
import typescript from '@rollup/plugin-typescript';
45

56
import { createConfig } from '../../shared/rollup.config.mjs';
67

@@ -9,5 +10,5 @@ export default {
910
pkg: JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
1011
}),
1112
input: 'src/index.js',
12-
plugins: [json()]
13+
plugins: [json(), typescript()]
1314
};

packages/node-resolve/src/fs.js renamed to packages/node-resolve/src/fs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const realpath = promisify(fs.realpath);
88
export { realpathSync } from 'fs';
99
export const stat = promisify(fs.stat);
1010

11-
export async function fileExists(filePath) {
11+
export async function fileExists(filePath: fs.PathLike) {
1212
try {
1313
const res = await stat(filePath);
1414
return res.isFile();
@@ -17,6 +17,6 @@ export async function fileExists(filePath) {
1717
}
1818
}
1919

20-
export async function resolveSymlink(path) {
20+
export async function resolveSymlink(path: fs.PathLike) {
2121
return (await fileExists(path)) ? realpath(path) : path;
2222
}

packages/node-resolve/src/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ const defaults = {
3737
extensions: ['.mjs', '.js', '.json', '.node'],
3838
resolveOnly: [],
3939
moduleDirectories: ['node_modules'],
40-
ignoreSideEffectsForRoot: false
40+
ignoreSideEffectsForRoot: false,
41+
// TODO: set to false in next major release or remove
42+
allowExportsFolderMapping: true
4143
};
4244
export const DEFAULTS = deepFreeze(deepMerge({}, defaults));
4345

@@ -183,7 +185,8 @@ export function nodeResolve(opts = {}) {
183185
moduleDirectories,
184186
modulePaths,
185187
rootDir,
186-
ignoreSideEffectsForRoot
188+
ignoreSideEffectsForRoot,
189+
allowExportsFolderMapping: options.allowExportsFolderMapping
187190
});
188191

189192
const importeeIsBuiltin = isBuiltinModule(importee);

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

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
InvalidModuleSpecifierError,
3+
InvalidConfigurationError,
4+
isMappings,
5+
isConditions,
6+
isMixedExports
7+
} from './utils';
8+
import resolvePackageTarget from './resolvePackageTarget';
9+
import resolvePackageImportsExports from './resolvePackageImportsExports';
10+
11+
/**
12+
* Implementation of PACKAGE_EXPORTS_RESOLVE
13+
*/
14+
async function resolvePackageExports(context: any, subpath: string, exports: any) {
15+
// If exports is an Object with both a key starting with "." and a key not starting with "."
16+
if (isMixedExports(exports)) {
17+
// throw an Invalid Package Configuration error.
18+
throw new InvalidConfigurationError(
19+
context,
20+
'All keys must either start with ./, or without one.'
21+
);
22+
}
23+
24+
// If subpath is equal to ".", then
25+
if (subpath === '.') {
26+
// Let mainExport be undefined.
27+
let mainExport: string | string[] | Record<string, any> | undefined;
28+
// If exports is a String or Array, or an Object containing no keys starting with ".", then
29+
if (typeof exports === 'string' || Array.isArray(exports) || isConditions(exports)) {
30+
// Set mainExport to exports
31+
mainExport = exports;
32+
// Otherwise if exports is an Object containing a "." property, then
33+
} else if (isMappings(exports)) {
34+
// Set mainExport to exports["."]
35+
mainExport = exports['.'];
36+
}
37+
38+
// If mainExport is not undefined, then
39+
if (mainExport) {
40+
// Let resolved be the result of PACKAGE_TARGET_RESOLVE with target = mainExport
41+
const resolved = await resolvePackageTarget(context, {
42+
target: mainExport,
43+
patternMatch: '',
44+
isImports: false
45+
});
46+
// If resolved is not null or undefined, return resolved.
47+
if (resolved) {
48+
return resolved;
49+
}
50+
}
51+
52+
// Otherwise, if exports is an Object and all keys of exports start with ".", then
53+
} else if (isMappings(exports)) {
54+
// Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE
55+
const resolvedMatch = await resolvePackageImportsExports(context, {
56+
matchKey: subpath,
57+
matchObj: exports,
58+
isImports: false
59+
});
60+
61+
// If resolved is not null or undefined, return resolved.
62+
if (resolvedMatch) {
63+
return resolvedMatch;
64+
}
65+
}
66+
67+
// Throw a Package Path Not Exported error.
68+
throw new InvalidModuleSpecifierError(context);
69+
}
70+
71+
export default resolvePackageExports;

packages/node-resolve/src/package/resolvePackageImports.js renamed to packages/node-resolve/src/package/resolvePackageImports.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@ import { pathToFileURL } from 'url';
33
import { createBaseErrorMsg, findPackageJson, InvalidModuleSpecifierError } from './utils';
44
import resolvePackageImportsExports from './resolvePackageImportsExports';
55

6+
interface ParamObject {
7+
importSpecifier: string;
8+
importer: string;
9+
moduleDirs: readonly string[];
10+
conditions: readonly string[];
11+
resolveId: (id: string) => any;
12+
}
13+
614
async function resolvePackageImports({
715
importSpecifier,
816
importer,
917
moduleDirs,
1018
conditions,
1119
resolveId
12-
}) {
20+
}: ParamObject) {
1321
const result = await findPackageJson(importer, moduleDirs);
1422
if (!result) {
15-
throw new Error(createBaseErrorMsg('. Could not find a parent package.json.'));
23+
throw new Error(
24+
`${createBaseErrorMsg(importSpecifier, importer)}. Could not find a parent package.json.`
25+
);
1626
}
1727

1828
const { pkgPath, pkgJsonPath, pkgJson } = result;
@@ -27,19 +37,28 @@ async function resolvePackageImports({
2737
resolveId
2838
};
2939

30-
const { imports } = pkgJson;
31-
if (!imports) {
32-
throw new InvalidModuleSpecifierError(context, true);
40+
// Assert: specifier begins with "#".
41+
if (!importSpecifier.startsWith('#')) {
42+
throw new InvalidModuleSpecifierError(context, true, 'Invalid import specifier.');
3343
}
3444

45+
// If specifier is exactly equal to "#" or starts with "#/", then
3546
if (importSpecifier === '#' || importSpecifier.startsWith('#/')) {
47+
// Throw an Invalid Module Specifier error.
3648
throw new InvalidModuleSpecifierError(context, true, 'Invalid import specifier.');
3749
}
3850

51+
const { imports } = pkgJson;
52+
if (!imports) {
53+
throw new InvalidModuleSpecifierError(context, true);
54+
}
55+
56+
// Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
57+
// If packageURL is not null, then
3958
return resolvePackageImportsExports(context, {
4059
matchKey: importSpecifier,
4160
matchObj: imports,
42-
internal: true
61+
isImports: true
4362
});
4463
}
4564

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

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy