Skip to content

Commit ea90365

Browse files
committed
feat(node-resolve): support pkg imports and export array
1 parent 05fc8b6 commit ea90365

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+607
-188
lines changed

packages/node-resolve/README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@ export default {
3434
input: 'src/index.js',
3535
output: {
3636
dir: 'output',
37-
format: 'cjs',
37+
format: 'cjs'
3838
},
39-
plugins: [nodeResolve()],
39+
plugins: [nodeResolve()]
4040
};
4141
```
4242

4343
Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api).
4444

45+
## Package entrypoints
46+
47+
This plugin supports the package entrypoints feature from node js, specified in the `exports` or `imports` field of a package. Check the [official documentation](https://nodejs.org/api/packages.html#packages_package_entry_points) for more information on how this works.
48+
4549
## Options
4650

4751
### `exportConditions`
@@ -62,6 +66,8 @@ Default: `false`
6266

6367
If `true`, instructs the plugin to use the `"browser"` property in `package.json` files to specify alternative files to load for bundling. This is useful when bundling for a browser environment. Alternatively, a value of `'browser'` can be added to the `mainFields` option. If `false`, any `"browser"` properties in package files will be ignored. This option takes precedence over `mainFields`.
6468

69+
> This option does not work when a package is using [package entrypoints](https://nodejs.org/api/packages.html#packages_package_entry_points)
70+
6571
### `moduleDirectories`
6672

6773
Type: `Array[...String]`<br>
@@ -169,9 +175,9 @@ export default {
169175
output: {
170176
file: 'bundle.js',
171177
format: 'iife',
172-
name: 'MyModule',
178+
name: 'MyModule'
173179
},
174-
plugins: [nodeResolve(), commonjs()],
180+
plugins: [nodeResolve(), commonjs()]
175181
};
176182
```
177183

@@ -203,7 +209,7 @@ The node resolve plugin uses `import` by default, you can opt into using the `re
203209
```js
204210
this.resolve(importee, importer, {
205211
skipSelf: true,
206-
custom: { 'node-resolve': { isRequire: true } },
212+
custom: { 'node-resolve': { isRequire: true } }
207213
});
208214
```
209215

packages/node-resolve/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import isModule from 'is-module';
77

88
import { isDirCached, isFileCached, readCachedFile } from './cache';
99
import { exists, readFile, realpath } from './fs';
10-
import { resolveImportSpecifiers } from './resolveImportSpecifiers';
10+
import resolveImportSpecifiers from './resolveImportSpecifiers';
1111
import { getMainFields, getPackageName, normalizeInput } from './util';
1212
import handleDeprecatedOptions from './deprecated-options';
1313

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
async function resolvePackageExports(context, subpath, exports) {
12+
if (isMixedExports(exports)) {
13+
throw new InvalidConfigurationError(
14+
context,
15+
'All keys must either start with ./, or without one.'
16+
);
17+
}
18+
19+
if (subpath === '.') {
20+
let mainExport;
21+
// If exports is a String or Array, or an Object containing no keys starting with ".", then
22+
if (typeof exports === 'string' || Array.isArray(exports) || isConditions(exports)) {
23+
mainExport = exports;
24+
} else if (isMappings(exports)) {
25+
mainExport = exports['.'];
26+
}
27+
28+
if (mainExport) {
29+
const resolved = await resolvePackageTarget(context, { target: mainExport, subpath: '' });
30+
if (resolved) {
31+
return resolved;
32+
}
33+
}
34+
} else if (isMappings(exports)) {
35+
const resolvedMatch = await resolvePackageImportsExports(context, {
36+
matchKey: subpath,
37+
matchObj: exports
38+
});
39+
40+
if (resolvedMatch) {
41+
return resolvedMatch;
42+
}
43+
}
44+
45+
throw new InvalidModuleSpecifierError(context);
46+
}
47+
48+
export default resolvePackageExports;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
import { pathToFileURL } from 'url';
4+
5+
import { createBaseErrorMsg, InvalidModuleSpecifierError } from './utils';
6+
import resolvePackageImportsExports from './resolvePackageImportsExports';
7+
8+
function findPackageJson(base) {
9+
const { root } = path.parse(base);
10+
let current = base;
11+
12+
while (current !== root && !current.endsWith('node_modules')) {
13+
const pkgJsonPath = path.join(current, 'package.json');
14+
if (fs.existsSync(pkgJsonPath)) {
15+
const pkgJsonString = fs.readFileSync(pkgJsonPath, 'utf-8');
16+
return { pkgJson: JSON.parse(pkgJsonString), pkgPath: current, pkgJsonPath };
17+
}
18+
current = path.resolve(current, '..');
19+
}
20+
return null;
21+
}
22+
23+
function resolvePackageImports({ importSpecifier, importer, conditions, resolveId }) {
24+
const result = findPackageJson(importer);
25+
if (!result) {
26+
throw new Error(createBaseErrorMsg('. Could not find a parent package.json.'));
27+
}
28+
29+
const { pkgPath, pkgJsonPath, pkgJson } = result;
30+
const pkgURL = pathToFileURL(`${pkgPath}/`);
31+
const context = { importer, importSpecifier, pkgURL, pkgJsonPath, conditions, resolveId };
32+
33+
const { imports } = pkgJson;
34+
if (!imports) {
35+
throw new InvalidModuleSpecifierError(context, true);
36+
}
37+
38+
if (importSpecifier === '#' || importSpecifier.startsWith('#/')) {
39+
throw new InvalidModuleSpecifierError(context, 'Invalid import specifier.');
40+
}
41+
42+
return resolvePackageImportsExports(context, {
43+
matchKey: importSpecifier,
44+
matchObj: imports,
45+
internal: true
46+
});
47+
}
48+
49+
export default resolvePackageImports;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable no-await-in-loop */
2+
import resolvePackageTarget from './resolvePackageTarget';
3+
4+
import { InvalidModuleSpecifierError } from './utils';
5+
6+
async function resolvePackageImportsExports(context, { matchKey, matchObj, internal }) {
7+
if (!matchKey.endsWith('*') && matchKey in matchObj) {
8+
const target = matchObj[matchKey];
9+
const resolved = await resolvePackageTarget(context, { target, subpath: '', internal });
10+
return resolved;
11+
}
12+
13+
const expansionKeys = Object.keys(matchObj)
14+
.filter((k) => k.endsWith('/') || k.endsWith('*'))
15+
.sort((a, b) => b.length - a.length);
16+
17+
for (const expansionKey of expansionKeys) {
18+
const prefix = expansionKey.substring(0, expansionKey.length - 1);
19+
20+
if (expansionKey.endsWith('*') && matchKey.startsWith(prefix)) {
21+
const target = matchObj[expansionKey];
22+
const subpath = matchKey.substring(expansionKey.length - 1);
23+
const resolved = await resolvePackageTarget(context, {
24+
target,
25+
subpath,
26+
pattern: true,
27+
internal
28+
});
29+
return resolved;
30+
}
31+
32+
if (matchKey.startsWith(expansionKey)) {
33+
const target = matchObj[expansionKey];
34+
const subpath = matchKey.substring(expansionKey.length);
35+
36+
const resolved = await resolvePackageTarget(context, { target, subpath, internal });
37+
return resolved;
38+
}
39+
}
40+
41+
throw new InvalidModuleSpecifierError(context, internal);
42+
}
43+
44+
export default resolvePackageImportsExports;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* eslint-disable no-await-in-loop */
2+
import { pathToFileURL } from 'url';
3+
4+
import { isUrl, InvalidModuleSpecifierError, InvalidPackageTargetError } from './utils';
5+
6+
function includesInvalidSegments(pathSegments) {
7+
return pathSegments
8+
.split('/')
9+
.slice(1)
10+
.some((t) => ['.', '..', 'node_modules'].includes(t));
11+
}
12+
13+
async function resolvePackageTarget(context, { target, subpath, pattern, internal }) {
14+
if (target == null) {
15+
return null;
16+
}
17+
18+
if (typeof target === 'string') {
19+
if (!pattern && subpath.length > 0 && !target.endsWith('/')) {
20+
throw new InvalidModuleSpecifierError(context);
21+
}
22+
23+
if (!target.startsWith('./')) {
24+
if (internal && !['/', '../'].some((p) => target.startsWith(p)) && !isUrl(target)) {
25+
// this is a bare package import, remap it and resolve it using regular node resolve
26+
if (pattern) {
27+
const result = await context.resolveId(
28+
target.replace(/\*/g, subpath),
29+
context.pkgURL.href
30+
);
31+
return result ? pathToFileURL(result.location) : null;
32+
}
33+
34+
const result = await context.resolveId(`${target}${subpath}`, context.pkgURL.href);
35+
return result ? pathToFileURL(result.location) : null;
36+
}
37+
throw new InvalidPackageTargetError(context, `Invalid mapping: "${target}".`);
38+
}
39+
40+
if (includesInvalidSegments(target)) {
41+
throw new InvalidPackageTargetError(context, `Invalid mapping: "${target}".`);
42+
}
43+
44+
const resolvedTarget = new URL(target, context.pkgURL);
45+
if (!resolvedTarget.href.startsWith(context.pkgURL.href)) {
46+
throw new InvalidPackageTargetError(
47+
context,
48+
`Resolved to ${resolvedTarget.href} which is outside package ${context.pkgURL.href}`
49+
);
50+
}
51+
52+
if (includesInvalidSegments(subpath)) {
53+
throw new InvalidModuleSpecifierError(context);
54+
}
55+
56+
if (pattern) {
57+
return resolvedTarget.href.replace(/\*/g, subpath);
58+
}
59+
return new URL(subpath, resolvedTarget).href;
60+
}
61+
62+
if (Array.isArray(target)) {
63+
let lastError;
64+
for (const item of target) {
65+
try {
66+
const resolved = await resolvePackageTarget(context, {
67+
target: item,
68+
subpath,
69+
pattern,
70+
internal
71+
});
72+
73+
if (resolved) {
74+
return resolved;
75+
}
76+
} catch (error) {
77+
if (!(error instanceof InvalidPackageTargetError)) {
78+
throw error;
79+
} else {
80+
lastError = error;
81+
}
82+
}
83+
}
84+
85+
if (lastError) {
86+
throw lastError;
87+
}
88+
return null;
89+
}
90+
91+
if (typeof target === 'object') {
92+
for (const [key, value] of Object.entries(target)) {
93+
if (key === 'default' || context.conditions.includes(key)) {
94+
const resolved = await resolvePackageTarget(context, {
95+
target: value,
96+
subpath,
97+
pattern,
98+
internal
99+
});
100+
101+
if (resolved) {
102+
return resolved;
103+
}
104+
}
105+
}
106+
}
107+
108+
throw new InvalidPackageTargetError(context, `Invalid exports field.`);
109+
}
110+
111+
export default resolvePackageTarget;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export function isUrl(str) {
2+
try {
3+
return !!new URL(str);
4+
} catch (_) {
5+
return false;
6+
}
7+
}
8+
9+
export function isConditions(exports) {
10+
return typeof exports === 'object' && Object.keys(exports).every((k) => !k.startsWith('.'));
11+
}
12+
13+
export function isMappings(exports) {
14+
return typeof exports === 'object' && !isConditions(exports);
15+
}
16+
17+
export function isMixedExports(exports) {
18+
const keys = Object.keys(exports);
19+
return keys.some((k) => k.startsWith('.')) && keys.some((k) => !k.startsWith('.'));
20+
}
21+
22+
export function createBaseErrorMsg(importSpecifier, importer) {
23+
return `Could not resolve import "${importSpecifier}" in ${importer}`;
24+
}
25+
26+
export function createErrorMsg(context, reason, internal) {
27+
const { importSpecifier, importer, pkgJsonPath } = context;
28+
const base = createBaseErrorMsg(importSpecifier, importer);
29+
const field = internal ? 'imports' : 'exports';
30+
return `${base} using ${field} defined in ${pkgJsonPath}.${reason ? ` ${reason}` : ''}`;
31+
}
32+
33+
export class ResolveError extends Error {}
34+
35+
export class InvalidConfigurationError extends ResolveError {
36+
constructor(context, reason) {
37+
super(createErrorMsg(context, `Invalid "exports" field. ${reason}`));
38+
}
39+
}
40+
41+
export class InvalidModuleSpecifierError extends ResolveError {
42+
constructor(context, internal) {
43+
super(createErrorMsg(context, internal));
44+
}
45+
}
46+
47+
export class InvalidPackageTargetError extends ResolveError {
48+
constructor(context, reason) {
49+
super(createErrorMsg(context, reason));
50+
}
51+
}

0 commit comments

Comments
 (0)