-
Notifications
You must be signed in to change notification settings - Fork 916
/
Copy pathentrypoints.ts
337 lines (284 loc) · 10.2 KB
/
entrypoints.ts
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import {readdirSync, existsSync, realpathSync} from 'fs';
import path from 'path';
import validatePackageName from 'validate-npm-package-name';
import {ExportField, ExportMapEntry, PackageManifestWithExports, PackageManifest} from './types';
import {parsePackageImportSpecifier, resolveDependencyManifest} from './util';
import pm from 'picomatch';
// Rarely, a package will ship a broken "browser" package.json entrypoint.
// Ignore the "browser" entrypoint in those packages.
const BROKEN_BROWSER_ENTRYPOINT = ['@sheerun/mutationobserver-shim'];
function hasTypes(manifest: PackageManifest): boolean {
return !!(manifest.types || manifest.typings);
}
type FindManifestEntryOptions = {
packageLookupFields?: string[];
packageName?: string;
};
/**
*
*/
export function findManifestEntry(
manifest: PackageManifest,
entry?: string,
{packageLookupFields = [], packageName}: FindManifestEntryOptions = {},
): string | undefined {
let foundEntrypoint: string | undefined;
if (manifest.exports) {
foundEntrypoint =
typeof manifest.exports === 'string'
? manifest.exports
: findExportMapEntry(manifest.exports['.'] || manifest.exports);
if (typeof foundEntrypoint === 'string') {
return foundEntrypoint;
}
}
foundEntrypoint = [
...packageLookupFields,
'browser:module',
'module',
'main:esnext',
'jsnext:main',
]
.map((e) => manifest[e])
.find(Boolean);
if (foundEntrypoint) {
return foundEntrypoint;
}
if (!(packageName && BROKEN_BROWSER_ENTRYPOINT.includes(packageName))) {
// Some packages define "browser" as an object. We'll do our best to find the
// right entrypoint in an entrypoint object, or fail otherwise.
// See: https://github.com/defunctzombie/package-browser-field-spec
let browserField = manifest.browser;
if (typeof browserField === 'string') {
return browserField;
} else if (typeof browserField === 'object') {
let browserEntrypoint =
(entry && browserField[entry]) ||
browserField['./index.js'] ||
browserField['./index'] ||
browserField['index.js'] ||
browserField['index'] ||
browserField['./'] ||
browserField['.'];
if (typeof browserEntrypoint === 'string') {
return browserEntrypoint;
}
}
}
// If browser object is set but no relevant entrypoint is found, fall back to "main".
return manifest.main;
}
/**
* Given an ExportMapEntry find the entry point, resolving recursively.
*/
export function findExportMapEntry(
exportMapEntry?: ExportMapEntry,
conditions?: string[],
): string | undefined {
// If this is a string or undefined we can skip checking for conditions
if (typeof exportMapEntry === 'string' || typeof exportMapEntry === 'undefined') {
return exportMapEntry;
}
let entry = exportMapEntry;
if (conditions) {
for (let condition of conditions) {
if (entry[condition]) {
entry = entry[condition];
}
}
}
return (
findExportMapEntry(entry?.browser) ||
findExportMapEntry(entry?.import) ||
findExportMapEntry(entry?.default) ||
findExportMapEntry(entry?.require) ||
undefined
);
}
type ResolveEntrypointOptions = {
cwd: string;
packageLookupFields: string[];
};
/**
* Resolve a "webDependencies" input value to the correct absolute file location.
* Supports both npm package names, and file paths relative to the node_modules directory.
* Follows logic similar to Node's resolution logic, but using a package.json's ESM "module"
* field instead of the CJS "main" field.
*/
export function resolveEntrypoint(
dep: string,
{cwd, packageLookupFields}: ResolveEntrypointOptions,
): string {
// We first need to check for an export map in the package.json. If one exists, resolve to it.
const [packageName, packageEntrypoint] = parsePackageImportSpecifier(dep);
const [packageManifestLoc, packageManifest] = resolveDependencyManifest(packageName, cwd);
if (packageManifestLoc && packageManifest && typeof packageManifest.exports !== 'undefined') {
const exportField = (packageManifest as PackageManifestWithExports).exports;
// If this is a non-main entry point
if (packageEntrypoint) {
const normalizedMap = explodeExportMap(exportField, {
cwd: path.dirname(packageManifestLoc),
});
const mapValue = normalizedMap && Reflect.get(normalizedMap, './' + packageEntrypoint);
if (typeof mapValue !== 'string') {
throw new Error(
`Package "${packageName}" exists but package.json "exports" does not include entry for "./${packageEntrypoint}".`,
);
}
return path.join(packageManifestLoc, '..', mapValue);
} else {
const exportMapEntry = exportField['.'] || exportField;
const mapValue = findExportMapEntry(exportMapEntry);
if (mapValue) {
return path.join(packageManifestLoc, '..', mapValue);
}
}
}
// if, no export map and dep points directly to a file within a package, return that reference.
if (path.extname(dep) && !validatePackageName(dep).validForNewPackages) {
return realpathSync.native(require.resolve(dep, {paths: [cwd]}));
}
// Otherwise, resolve directly to the dep specifier. Note that this supports both
// "package-name" & "package-name/some/path" where "package-name/some/path/package.json"
// exists at that lower path, that must be used to resolve. In that case, export
// maps should not be supported.
const [depManifestLoc, depManifest] = resolveDependencyManifest(dep, cwd);
if (!depManifest) {
try {
const maybeLoc = realpathSync.native(require.resolve(dep, {paths: [cwd]}));
return maybeLoc;
} catch {
// Oh well, was worth a try
}
}
if (!depManifestLoc || !depManifest) {
throw new Error(
`Package "${dep}" not found. Have you installed it? ${depManifestLoc ? depManifestLoc : ''}`,
);
}
let foundEntrypoint = findManifestEntry(depManifest, dep, {
packageName,
packageLookupFields,
});
// Some packages are types-only. If this is one of those packages, resolve with that.
if (!foundEntrypoint && hasTypes(depManifest)) {
const typesLoc = (depManifest.types || depManifest.typings) as string;
return path.join(depManifestLoc, '..', typesLoc);
}
// Sometimes packages don't give an entrypoint, assuming you'll fall back to "index.js".
if (!foundEntrypoint) {
foundEntrypoint = 'index.js';
}
if (typeof foundEntrypoint !== 'string') {
throw new Error(`"${dep}" has unexpected entrypoint: ${JSON.stringify(foundEntrypoint)}.`);
}
return realpathSync.native(
require.resolve(path.join(depManifestLoc || '', '..', foundEntrypoint)),
);
}
const picoMatchGlobalOptions = Object.freeze({
capture: true,
noglobstar: true,
});
function* forEachExportEntry(
exportField: ExportField,
): Generator<[string, unknown], any, undefined> {
const simpleExportMap = findExportMapEntry(exportField);
// Handle case where export map is a string, or if there‘s only one file in the entire export map
if (simpleExportMap) {
yield ['.', simpleExportMap];
return;
}
for (const [key, val] of Object.entries(exportField)) {
// skip invalid entries
if (!key.startsWith('.')) {
continue;
}
yield [key, val];
}
}
function* forEachWildcardEntry(
key: string,
value: string,
cwd: string,
): Generator<[string, string], any, undefined> {
// Creates a regex from a pattern like ./src/extras/*
let expr = pm.makeRe(value, picoMatchGlobalOptions);
// The directory, ie ./src/extras
let valueDirectoryName = path.dirname(value);
let valueDirectoryFullPath = path.join(cwd, valueDirectoryName);
if (existsSync(valueDirectoryFullPath)) {
let filesInDirectory = readdirSync(valueDirectoryFullPath);
for (let filename of filesInDirectory) {
// Create a relative path for this file to match against the regex
// ex, ./src/extras/one.js
let relativeFilePath = path.join(valueDirectoryName, filename);
let match = expr.exec(relativeFilePath);
if (match && match[1]) {
let [matchingPath, matchGroup] = match;
let normalizedKey = key.replace('*', matchGroup);
// Normalized to posix paths, like ./src/extras/one.js
let normalizedFilePath =
'.' + path.posix.sep + matchingPath.split(path.sep).join(path.posix.sep);
// Yield out a non-wildcard match, for ex.
// ['./src/extras/one', './src/extras/one.js']
yield [normalizedKey, normalizedFilePath];
}
}
}
}
function* forEachExportEntryExploded(
exportField: ExportField,
cwd: string,
): Generator<[string, unknown], any, undefined> {
for (const [key, val] of forEachExportEntry(exportField)) {
// Deprecated but we still want to support this.
// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings
if (key.endsWith('/')) {
if (typeof val !== 'string') {
continue;
}
// There isn't a clear use-case for this, so we are assuming it's not needed for now.
if (key === './') {
continue;
}
yield* forEachWildcardEntry(key + '*', val + '*', cwd);
continue;
}
// Wildcards https://nodejs.org/api/packages.html#packages_subpath_patterns
if (key.includes('*')) {
if (typeof val !== 'string') {
continue;
}
yield* forEachWildcardEntry(key, val, cwd);
continue;
}
yield [key, val];
}
}
/**
* Given an export map and all of the crazy variations, condense down to a key/value map of string keys to string values.
*/
export function explodeExportMap(
exportField: ExportField | undefined,
{cwd}: {cwd: string},
): Record<string, string> | undefined {
if (!exportField) {
return;
}
const cleanExportMap: Record<string, string> = {};
for (const [key, val] of forEachExportEntryExploded(exportField, cwd)) {
// If entry is an array, assume that we can always support the first value
const firstVal = Array.isArray(val) ? val[0] : val;
// Support these entries, in this order.
const cleanValue = findExportMapEntry(firstVal);
if (typeof cleanValue !== 'string') {
continue;
}
cleanExportMap[key] = cleanValue;
}
if (Object.keys(cleanExportMap).length === 0) {
return;
}
return cleanExportMap;
}