Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(node-resolve): Fix package exports / imports resolution algorithm #1549

Merged
merged 1 commit into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/node-resolve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,31 @@ rootDir: path.join(process.cwd(), '..')

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.

### `allowExportsFolderMapping`

Older Node versions supported exports mappings of folders like

```json
{
"exports": {
"./foo/": "./dist/foo/"
}
}
```

This was deprecated with Node 14 and removed in Node 17, instead it is recommended to use exports patterns like

```json
{
"exports": {
"./foo/*": "./dist/foo/*"
}
}
```

But for backwards compatibility this behavior is still supported by enabling the `allowExportsFolderMapping` (defaults to `true`).
The default value might change in a futur major release.

## Preserving symlinks

This plugin honours the rollup [`preserveSymlinks`](https://rollupjs.org/guide/en/#preservesymlinks) option.
Expand Down
3 changes: 2 additions & 1 deletion packages/node-resolve/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'fs';

import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript';

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

Expand All @@ -9,5 +10,5 @@ export default {
pkg: JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
}),
input: 'src/index.js',
plugins: [json()]
plugins: [json(), typescript()]
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const realpath = promisify(fs.realpath);
export { realpathSync } from 'fs';
export const stat = promisify(fs.stat);

export async function fileExists(filePath) {
export async function fileExists(filePath: fs.PathLike) {
try {
const res = await stat(filePath);
return res.isFile();
Expand All @@ -17,6 +17,6 @@ export async function fileExists(filePath) {
}
}

export async function resolveSymlink(path) {
export async function resolveSymlink(path: fs.PathLike) {
return (await fileExists(path)) ? realpath(path) : path;
}
7 changes: 5 additions & 2 deletions packages/node-resolve/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ const defaults = {
extensions: ['.mjs', '.js', '.json', '.node'],
resolveOnly: [],
moduleDirectories: ['node_modules'],
ignoreSideEffectsForRoot: false
ignoreSideEffectsForRoot: false,
// TODO: set to false in next major release or remove
allowExportsFolderMapping: true
};
export const DEFAULTS = deepFreeze(deepMerge({}, defaults));

Expand Down Expand Up @@ -183,7 +185,8 @@ export function nodeResolve(opts = {}) {
moduleDirectories,
modulePaths,
rootDir,
ignoreSideEffectsForRoot
ignoreSideEffectsForRoot,
allowExportsFolderMapping: options.allowExportsFolderMapping
});

const importeeIsBuiltin = isBuiltinModule(importee);
Expand Down
48 changes: 0 additions & 48 deletions packages/node-resolve/src/package/resolvePackageExports.js

This file was deleted.

71 changes: 71 additions & 0 deletions packages/node-resolve/src/package/resolvePackageExports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
InvalidModuleSpecifierError,
InvalidConfigurationError,
isMappings,
isConditions,
isMixedExports
} from './utils';
import resolvePackageTarget from './resolvePackageTarget';
import resolvePackageImportsExports from './resolvePackageImportsExports';

/**
* Implementation of PACKAGE_EXPORTS_RESOLVE
*/
async function resolvePackageExports(context: any, subpath: string, exports: any) {
// If exports is an Object with both a key starting with "." and a key not starting with "."
if (isMixedExports(exports)) {
// throw an Invalid Package Configuration error.
throw new InvalidConfigurationError(
context,
'All keys must either start with ./, or without one.'
);
}

// If subpath is equal to ".", then
if (subpath === '.') {
// Let mainExport be undefined.
let mainExport: string | string[] | Record<string, any> | undefined;
// If exports is a String or Array, or an Object containing no keys starting with ".", then
if (typeof exports === 'string' || Array.isArray(exports) || isConditions(exports)) {
// Set mainExport to exports
mainExport = exports;
// Otherwise if exports is an Object containing a "." property, then
} else if (isMappings(exports)) {
// Set mainExport to exports["."]
mainExport = exports['.'];
}

// If mainExport is not undefined, then
if (mainExport) {
// Let resolved be the result of PACKAGE_TARGET_RESOLVE with target = mainExport
const resolved = await resolvePackageTarget(context, {
target: mainExport,
patternMatch: '',
isImports: false
});
// If resolved is not null or undefined, return resolved.
if (resolved) {
return resolved;
}
}

// Otherwise, if exports is an Object and all keys of exports start with ".", then
} else if (isMappings(exports)) {
// Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE
const resolvedMatch = await resolvePackageImportsExports(context, {
matchKey: subpath,
matchObj: exports,
isImports: false
});

// If resolved is not null or undefined, return resolved.
if (resolvedMatch) {
return resolvedMatch;
}
}

// Throw a Package Path Not Exported error.
throw new InvalidModuleSpecifierError(context);
}

export default resolvePackageExports;
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ import { pathToFileURL } from 'url';
import { createBaseErrorMsg, findPackageJson, InvalidModuleSpecifierError } from './utils';
import resolvePackageImportsExports from './resolvePackageImportsExports';

interface ParamObject {
importSpecifier: string;
importer: string;
moduleDirs: readonly string[];
conditions: readonly string[];
resolveId: (id: string) => any;
}

async function resolvePackageImports({
importSpecifier,
importer,
moduleDirs,
conditions,
resolveId
}) {
}: ParamObject) {
const result = await findPackageJson(importer, moduleDirs);
if (!result) {
throw new Error(createBaseErrorMsg('. Could not find a parent package.json.'));
throw new Error(
`${createBaseErrorMsg(importSpecifier, importer)}. Could not find a parent package.json.`
);
}

const { pkgPath, pkgJsonPath, pkgJson } = result;
Expand All @@ -27,19 +37,28 @@ async function resolvePackageImports({
resolveId
};

const { imports } = pkgJson;
if (!imports) {
throw new InvalidModuleSpecifierError(context, true);
// Assert: specifier begins with "#".
if (!importSpecifier.startsWith('#')) {
throw new InvalidModuleSpecifierError(context, true, 'Invalid import specifier.');
}

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

const { imports } = pkgJson;
if (!imports) {
throw new InvalidModuleSpecifierError(context, true);
}

// Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
// If packageURL is not null, then
return resolvePackageImportsExports(context, {
matchKey: importSpecifier,
matchObj: imports,
internal: true
isImports: true
});
}

Expand Down

This file was deleted.

Loading