-
-
Notifications
You must be signed in to change notification settings - Fork 582
/
resolveImportSpecifiers.js
197 lines (169 loc) · 6.09 KB
/
resolveImportSpecifiers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import resolve from 'resolve';
import { getPackageName } from './util';
import { exists, realpath } from './fs';
const resolveImportPath = promisify(resolve);
const readFile = promisify(fs.readFile);
const pathNotFoundError = (subPath, pkgPath) =>
new Error(`Package subpath '${subPath}' is not defined by "exports" in ${pkgPath}`);
function findExportKeyMatch(exportMap, subPath) {
for (const key of Object.keys(exportMap)) {
if (key.endsWith('*')) {
// star match: "./foo/*": "./foo/*.js"
const keyWithoutStar = key.substring(0, key.length - 1);
if (subPath.startsWith(keyWithoutStar)) {
return key;
}
}
if (key.endsWith('/') && subPath.startsWith(key)) {
// directory match (deprecated by node): "./foo/": "./foo/.js"
return key;
}
if (key === subPath) {
// literal match
return key;
}
}
return null;
}
function mapSubPath(pkgJsonPath, subPath, key, value) {
if (typeof value === 'string') {
if (typeof key === 'string' && key.endsWith('*')) {
// star match: "./foo/*": "./foo/*.js"
const keyWithoutStar = key.substring(0, key.length - 1);
const subPathAfterKey = subPath.substring(keyWithoutStar.length);
return value.replace(/\*/g, subPathAfterKey);
}
if (value.endsWith('/')) {
// directory match (deprecated by node): "./foo/": "./foo/.js"
return `${value}${subPath.substring(key.length)}`;
}
// mapping is a string, for example { "./foo": "./dist/foo.js" }
return value;
}
if (Array.isArray(value)) {
// mapping is an array with fallbacks, for example { "./foo": ["foo:bar", "./dist/foo.js"] }
return value.find((v) => v.startsWith('./'));
}
throw pathNotFoundError(subPath, pkgJsonPath);
}
function findEntrypoint(pkgJsonPath, subPath, exportMap, conditions, key) {
if (typeof exportMap !== 'object') {
return mapSubPath(pkgJsonPath, subPath, key, exportMap);
}
// iterate conditions recursively, find the first that matches all conditions
for (const [condition, subExportMap] of Object.entries(exportMap)) {
if (conditions.includes(condition)) {
const mappedSubPath = findEntrypoint(pkgJsonPath, subPath, subExportMap, conditions, key);
if (mappedSubPath) {
return mappedSubPath;
}
}
}
throw pathNotFoundError(subPath, pkgJsonPath);
}
export function findEntrypointTopLevel(pkgJsonPath, subPath, exportMap, conditions) {
if (typeof exportMap !== 'object') {
// the export map shorthand, for example { exports: "./index.js" }
if (subPath !== '.') {
// shorthand only supports a main entrypoint
throw pathNotFoundError(subPath, pkgJsonPath);
}
return mapSubPath(pkgJsonPath, subPath, null, exportMap);
}
// export map is an object, the top level can be either conditions or sub path mappings
const keys = Object.keys(exportMap);
const isConditions = keys.every((k) => !k.startsWith('.'));
const isMappings = keys.every((k) => k.startsWith('.'));
if (!isConditions && !isMappings) {
throw new Error(
`Invalid package config ${pkgJsonPath}, "exports" cannot contain some keys starting with '.'` +
' and some not. The exports object must either be an object of package subpath keys or an object of main entry' +
' condition name keys only.'
);
}
let key = null;
let exportMapForSubPath;
if (isConditions) {
// top level is conditions, for example { "import": ..., "require": ..., "module": ... }
if (subPath !== '.') {
// package with top level conditions means it only supports a main entrypoint
throw pathNotFoundError(subPath, pkgJsonPath);
}
exportMapForSubPath = exportMap;
} else {
// top level is sub path mappings, for example { ".": ..., "./foo": ..., "./bar": ... }
key = findExportKeyMatch(exportMap, subPath);
if (!key) {
throw pathNotFoundError(subPath, pkgJsonPath);
}
exportMapForSubPath = exportMap[key];
}
return findEntrypoint(pkgJsonPath, subPath, exportMapForSubPath, conditions, key);
}
async function resolveId(importPath, options, exportConditions, warn) {
const pkgName = getPackageName(importPath);
if (pkgName) {
let pkgJsonPath;
let pkgJson;
try {
pkgJsonPath = await resolveImportPath(`${pkgName}/package.json`, options);
pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
} catch (_) {
// if there is no package.json we defer to regular resolve behavior
}
if (pkgJsonPath && pkgJson && pkgJson.exports) {
try {
const packageSubPath =
pkgName === importPath ? '.' : `.${importPath.substring(pkgName.length)}`;
const mappedSubPath = findEntrypointTopLevel(
pkgJsonPath,
packageSubPath,
pkgJson.exports,
exportConditions
);
const pkgDir = path.dirname(pkgJsonPath);
return path.join(pkgDir, mappedSubPath);
} catch (error) {
warn(error);
return null;
}
}
}
return resolveImportPath(importPath, options);
}
// Resolve module specifiers in order. Promise resolves to the first module that resolves
// successfully, or the error that resulted from the last attempted module resolution.
export function resolveImportSpecifiers(
importSpecifierList,
resolveOptions,
exportConditions,
warn
) {
let promise = Promise.resolve();
for (let i = 0; i < importSpecifierList.length; i++) {
// eslint-disable-next-line no-loop-func
promise = promise.then(async (value) => {
// if we've already resolved to something, just return it.
if (value) {
return value;
}
let result = await resolveId(importSpecifierList[i], resolveOptions, exportConditions, warn);
if (!resolveOptions.preserveSymlinks) {
if (await exists(result)) {
result = await realpath(result);
}
}
return result;
});
// swallow MODULE_NOT_FOUND errors
promise = promise.catch((error) => {
if (error.code !== 'MODULE_NOT_FOUND') {
throw error;
}
});
}
return promise;
}