Skip to content

Commit b12b0f7

Browse files
committed
feat(node-resolve): support package entry points
1 parent e4d21ba commit b12b0f7

Some content is hidden

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

57 files changed

+528
-28
lines changed

β€Ž.eslintignoreβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
.eslintrc.js
1+
.eslintrc.js

β€Žpackages/node-resolve/README.mdβ€Ž

Lines changed: 11 additions & 4 deletions

β€Žpackages/node-resolve/src/index.jsβ€Ž

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
const builtins = new Set(builtinList);
1919
const ES6_BROWSER_EMPTY = '\0node-resolve:empty.js';
2020
const nullFn = () => null;
21-
const deepFreeze = object => {
21+
const deepFreeze = (object) => {
2222
Object.freeze(object);
2323

2424
for (const value of Object.values(object)) {
@@ -30,6 +30,7 @@ const deepFreeze = object => {
3030
return object;
3131
};
3232
const defaults = {
33+
exportConditions: ['module', 'default'],
3334
customResolveOptions: {},
3435
dedupe: [],
3536
// It's important that .mjs is listed before .js so that Rollup will interpret npm modules
@@ -41,7 +42,7 @@ export const DEFAULTS = deepFreeze(deepMerge({}, defaults));
4142

4243
export function nodeResolve(opts = {}) {
4344
const options = Object.assign({}, defaults, opts);
44-
const { customResolveOptions, extensions, jail } = options;
45+
const { exportConditions, customResolveOptions, extensions, jail } = options;
4546
const warnings = [];
4647
const packageInfoCache = new Map();
4748
const idToPackageInfo = new Map();
@@ -220,7 +221,13 @@ export function nodeResolve(opts = {}) {
220221
resolveOptions = Object.assign(resolveOptions, customResolveOptions);
221222

222223
try {
223-
let resolved = await resolveImportSpecifiers(importSpecifierList, resolveOptions);
224+
const warn = (...args) => this.warn(...args);
225+
let resolved = await resolveImportSpecifiers(
226+
importSpecifierList,
227+
resolveOptions,
228+
exportConditions,
229+
warn
230+
);
224231

225232
if (resolved && packageBrowserField) {
226233
if (Object.prototype.hasOwnProperty.call(packageBrowserField, resolved)) {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { promisify } from 'util';
4+
5+
import resolve from 'resolve';
6+
7+
import { getPackageName } from './util';
8+
9+
const resolveImportPath = promisify(resolve);
10+
const readFile = promisify(fs.readFile);
11+
12+
const pathNotFoundError = (subPath, pkgPath) =>
13+
new Error(`Package subpath '${subPath}' is not defined by "exports" in ${pkgPath}`);
14+
15+
function mapSubPath(pkgJsonPath, subPath, value) {
16+
if (typeof value === 'string') {
17+
// mapping is a string, for example { "./foo": "./dist/foo.js" }
18+
return value;
19+
}
20+
21+
if (Array.isArray(value)) {
22+
// mapping is an array with fallbacks, for example { "./foo": ["foo:bar", "./dist/foo.js"] }
23+
return value.find((v) => v.startsWith('./'));
24+
}
25+
26+
throw pathNotFoundError(subPath, pkgJsonPath);
27+
}
28+
29+
function findEntrypoint(pkgJsonPath, subPath, exportMap, conditions) {
30+
if (typeof exportMap !== 'object') {
31+
return mapSubPath(pkgJsonPath, subPath, exportMap);
32+
}
33+
34+
// iterate conditions recursively, find the first that matches all conditions
35+
for (const [condition, subExportMap] of Object.entries(exportMap)) {
36+
if (conditions.includes(condition)) {
37+
const mappedSubPath = findEntrypoint(pkgJsonPath, subPath, subExportMap, conditions);
38+
if (mappedSubPath) {
39+
return mappedSubPath;
40+
}
41+
}
42+
}
43+
throw pathNotFoundError(subPath, pkgJsonPath);
44+
}
45+
46+
export function findEntrypointTopLevel(pkgJsonPath, subPath, exportMap, conditions) {
47+
if (typeof exportMap !== 'object') {
48+
// the export map shorthand, for example { exports: "./index.js" }
49+
if (subPath !== '.') {
50+
// shorthand only supports a main entrypoint
51+
throw pathNotFoundError(subPath, pkgJsonPath);
52+
}
53+
return mapSubPath(pkgJsonPath, subPath, exportMap);
54+
}
55+
56+
// export map is an object, the top level can be either conditions or sub path mappings
57+
const keys = Object.keys(exportMap);
58+
const isConditions = keys.every((k) => !k.startsWith('.'));
59+
const isMappings = keys.every((k) => k.startsWith('.'));
60+
61+
if (!isConditions && !isMappings) {
62+
throw new Error(
63+
`Invalid package config ${pkgJsonPath}, "exports" cannot contain some keys starting with '.'` +
64+
' and some not. The exports object must either be an object of package subpath keys or an object of main entry' +
65+
' condition name keys only.'
66+
);
67+
}
68+
69+
let exportMapForSubPath;
70+
71+
if (isConditions) {
72+
// top level is conditions, for example { "import": ..., "require": ..., "module": ... }
73+
if (subPath !== '.') {
74+
// package with top level conditions means it only supports a main entrypoint
75+
throw pathNotFoundError(subPath, pkgJsonPath);
76+
}
77+
exportMapForSubPath = exportMap;
78+
} else {
79+
// top level is sub path mappings, for example { ".": ..., "./foo": ..., "./bar": ... }
80+
if (!(subPath in exportMap)) {
81+
throw pathNotFoundError(subPath, pkgJsonPath);
82+
}
83+
exportMapForSubPath = exportMap[subPath];
84+
}
85+
86+
return findEntrypoint(pkgJsonPath, subPath, exportMapForSubPath, conditions);
87+
}
88+
89+
export default async function resolveId(importPath, options, exportConditions, warn) {
90+
const pkgName = getPackageName(importPath);
91+
if (pkgName) {
92+
let pkgJsonPath;
93+
let pkgJson;
94+
try {
95+
pkgJsonPath = await resolveImportPath(`${pkgName}/package.json`, options);
96+
pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
97+
} catch (_) {
98+
// if there is no package.json we defer to regular resolve behavior
99+
}
100+
101+
if (pkgJsonPath && pkgJson && pkgJson.exports) {
102+
try {
103+
const packageSubPath =
104+
pkgName === importPath ? '.' : `.${importPath.substring(pkgName.length)}`;
105+
const mappedSubPath = findEntrypointTopLevel(
106+
pkgJsonPath,
107+
packageSubPath,
108+
pkgJson.exports,
109+
exportConditions
110+
);
111+
const pkgDir = path.dirname(pkgJsonPath);
112+
return path.join(pkgDir, mappedSubPath);
113+
} catch (error) {
114+
warn(error);
115+
return null;
116+
}
117+
}
118+
}
119+
120+
return resolveImportPath(importPath, options);
121+
}

β€Žpackages/node-resolve/src/util.jsβ€Ž

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { dirname, extname, resolve } from 'path';
2-
import { promisify } from 'util';
32

43
import { createFilter } from '@rollup/pluginutils';
54

6-
import resolveModule from 'resolve';
5+
import resolveId from './resolveId';
76

87
import { realpathSync } from './fs';
98

10-
const resolveId = promisify(resolveModule);
11-
129
// returns the imported package name for bare module imports
1310
export function getPackageName(id) {
1411
if (id.startsWith('.') || id.startsWith('/')) {
@@ -161,7 +158,12 @@ export function normalizeInput(input) {
161158

162159
// Resolve module specifiers in order. Promise resolves to the first module that resolves
163160
// successfully, or the error that resulted from the last attempted module resolution.
164-
export function resolveImportSpecifiers(importSpecifierList, resolveOptions) {
161+
export function resolveImportSpecifiers(
162+
importSpecifierList,
163+
resolveOptions,
164+
exportConditions,
165+
warn
166+
) {
165167
let promise = Promise.resolve();
166168

167169
for (let i = 0; i < importSpecifierList.length; i++) {
@@ -171,12 +173,14 @@ export function resolveImportSpecifiers(importSpecifierList, resolveOptions) {
171173
return value;
172174
}
173175

174-
return resolveId(importSpecifierList[i], resolveOptions).then((result) => {
175-
if (!resolveOptions.preserveSymlinks) {
176-
result = realpathSync(result);
176+
return resolveId(importSpecifierList[i], resolveOptions, exportConditions, warn).then(
177+
(result) => {
178+
if (!resolveOptions.preserveSymlinks) {
179+
result = realpathSync(result);
180+
}
181+
return result;
177182
}
178-
return result;
179-
});
183+
);
180184
});
181185

182186
if (i < importSpecifierList.length - 1) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import main from 'exports-mappings-and-conditions';
2+
import foo from 'exports-mappings-and-conditions/foo';
3+
import bar from 'exports-mappings-and-conditions/bar';
4+
5+
export default { main, foo, bar };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import main from 'exports-nested-conditions';
2+
3+
export default main;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import bar from 'exports-non-existing-subpath/bar';
2+
3+
export default bar;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import bar from 'exports-top-level-mappings/bar';
2+
3+
export default bar;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import exportsMapEntry from 'exports-shorthand';
2+
3+
export default exportsMapEntry;

0 commit comments

Comments
Β (0)