source: frontend/node_modules/enhanced-resolve/lib/util/entrypoints.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 21.2 KB
RevLine 
[9af201e]1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Ivan Kopeykin @vankop
4*/
5
6"use strict";
7
8const { parseIdentifier } = require("./identifier");
9
10/** @typedef {string | (string | ConditionalMapping)[]} DirectMapping */
11/** @typedef {{ [k: string]: MappingValue }} ConditionalMapping */
12/** @typedef {ConditionalMapping | DirectMapping | null} MappingValue */
13/** @typedef {Record<string, MappingValue> | ConditionalMapping | DirectMapping} ExportsField */
14/** @typedef {Record<string, MappingValue>} ImportsField */
15
16/**
17 * Processing exports/imports field
18 * @callback FieldProcessor
19 * @param {string} request request
20 * @param {Set<string>} conditionNames condition names
21 * @returns {[string[], string | null]} resolved paths with used field
22 */
23
24/*
25Example exports field:
26{
27 ".": "./main.js",
28 "./feature": {
29 "browser": "./feature-browser.js",
30 "default": "./feature.js"
31 }
32}
33Terminology:
34
35Enhanced-resolve name keys ("." and "./feature") as exports field keys.
36
37If value is string or string[], mapping is called as a direct mapping
38and value called as a direct export.
39
40If value is key-value object, mapping is called as a conditional mapping
41and value called as a conditional export.
42
43Key in conditional mapping is called condition name.
44
45Conditional mapping nested in another conditional mapping is called nested mapping.
46
47----------
48
49Example imports field:
50{
51 "#a": "./main.js",
52 "#moment": {
53 "browser": "./moment/index.js",
54 "default": "moment"
55 },
56 "#moment/": {
57 "browser": "./moment/",
58 "default": "moment/"
59 }
60}
61Terminology:
62
63Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
64
65If value is string or string[], mapping is called as a direct mapping
66and value called as a direct export.
67
68If value is key-value object, mapping is called as a conditional mapping
69and value called as a conditional export.
70
71Key in conditional mapping is called condition name.
72
73Conditional mapping nested in another conditional mapping is called nested mapping.
74
75*/
76
77const slashCode = "/".charCodeAt(0);
78const dotCode = ".".charCodeAt(0);
79const hashCode = "#".charCodeAt(0);
80const patternRegEx = /\*/g;
81const DOLLAR_ESCAPE_RE = /\$/g;
82
83/** @typedef {Record<string, MappingValue>} RecordMapping */
84
85/**
86 * Cached `Object.keys()` for objects whose shape does not change after the
87 * first observation — i.e. parsed `package.json` fields and the nested
88 * conditional mappings inside them. `Object.keys` allocates a fresh array
89 * on every call; since `findMatch` / `conditionalMapping` run on every
90 * bare-specifier resolve, the allocation adds up quickly.
91 * @type {WeakMap<RecordMapping, string[]>}
92 */
93const _keysCache = new WeakMap();
94
95/**
96 * @param {RecordMapping} obj object to read keys from
97 * @returns {string[]} cached keys array (DO NOT mutate)
98 */
99function cachedKeys(obj) {
100 let keys = _keysCache.get(obj);
101 if (keys === undefined) {
102 keys = Object.keys(obj);
103 _keysCache.set(obj, keys);
104 }
105 return keys;
106}
107
108/**
109 * Per-key precomputed info used by `findMatch`. Equivalent to what the
110 * previous implementation recomputed inline on every resolve.
111 * @typedef {object} FieldKeyInfo
112 * @property {string} key the original key
113 * @property {number} patternIndex position of the single "*" in the key, or -1 when absent
114 * @property {string} wildcardPrefix substring before "*" (empty when patternIndex === -1)
115 * @property {string} wildcardSuffix substring after "*" (empty when patternIndex === -1)
116 * @property {boolean} isLegacySubpath true when key is a legacy `./foo/`-style folder key with no "*"
117 * @property {boolean} isPattern true when key contains "*"
118 * @property {boolean} isSubpathMapping true when key ends with "/"
119 * @property {boolean} isValidPattern true when key has at most one "*"
120 */
121
122/**
123 * Cached per-field key metadata, keyed by the exports/imports field
124 * object. Computed lazily on first `findMatch` call and reused forever.
125 * Safe because `package.json` fields are immutable JSON values.
126 * @type {WeakMap<RecordMapping, FieldKeyInfo[]>}
127 */
128const _fieldKeyInfoCache = new WeakMap();
129
130/**
131 * @param {ExportsField | ImportsField} field field object
132 * @returns {FieldKeyInfo[]} precomputed per-key info
133 */
134function getFieldKeyInfos(field) {
135 const fieldKey = /** @type {RecordMapping} */ (field);
136 let infos = _fieldKeyInfoCache.get(fieldKey);
137 if (infos !== undefined) return infos;
138 const keys = Object.getOwnPropertyNames(field);
139 infos = Array.from({ length: keys.length });
140 for (let i = 0; i < keys.length; i++) {
141 const key = keys[i];
142 const patternIndex = key.indexOf("*");
143 // `isValidPattern` is true when the key has at most one `*`. Searching
144 // from `patternIndex + 1` stops as soon as a second `*` is found, so
145 // we avoid the full-string scan that `lastIndexOf` would do — and the
146 // single-star common case finishes in one pass.
147 const isValidPattern =
148 patternIndex === -1 || !key.includes("*", patternIndex + 1);
149 const keyLen = key.length;
150 const endsWithSlash =
151 keyLen > 0 && key.charCodeAt(keyLen - 1) === slashCode;
152 infos[i] = {
153 key,
154 patternIndex,
155 wildcardPrefix: patternIndex === -1 ? "" : key.slice(0, patternIndex),
156 wildcardSuffix: patternIndex === -1 ? "" : key.slice(patternIndex + 1),
157 isLegacySubpath: patternIndex === -1 && endsWithSlash,
158 isPattern: patternIndex !== -1,
159 isSubpathMapping: endsWithSlash,
160 isValidPattern,
161 };
162 }
163 _fieldKeyInfoCache.set(fieldKey, infos);
164 return infos;
165}
166
167/**
168 * @param {string} a first string
169 * @param {string} b second string
170 * @returns {number} compare result
171 */
172function patternKeyCompare(a, b) {
173 const aPatternIndex = a.indexOf("*");
174 const bPatternIndex = b.indexOf("*");
175 const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
176 const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
177
178 if (baseLenA > baseLenB) return -1;
179 if (baseLenB > baseLenA) return 1;
180 if (aPatternIndex === -1) return 1;
181 if (bPatternIndex === -1) return -1;
182 if (a.length > b.length) return -1;
183 if (b.length > a.length) return 1;
184
185 return 0;
186}
187
188/** @typedef {[MappingValue, string, boolean, boolean, string] | null} MatchTuple */
189
190/**
191 * Per-field memoization of `findMatch(request, field)`. For a given field
192 * the result depends only on the `request` string (it does NOT depend on
193 * `conditionNames` — that's applied separately by `conditionalMapping`),
194 * so we can cache the tuple keyed by request.
195 *
196 * Typical build traffic runs the same request through the resolver
197 * repeatedly (same import re-resolved from different source files, module
198 * graph traversals that revisit a package, etc.), and every one of those
199 * hits walks the same key list and allocates the same tuple. Caching the
200 * tuple turns the second-and-onward call into a single Map lookup.
201 *
202 * Keyed on the field object via a module-level `WeakMap`, so the cache
203 * is freed automatically when the owning description file is GC'd.
204 * @type {WeakMap<RecordMapping, Map<string, MatchTuple>>}
205 */
206const _findMatchCache = new WeakMap();
207
208/**
209 * @param {string} request request
210 * @param {ExportsField | ImportsField} field exports or import field
211 * @returns {MatchTuple} match result (uncached)
212 */
213function computeFindMatch(request, field) {
214 const requestLen = request.length;
215 const requestEndsWithSlash =
216 requestLen > 0 && request.charCodeAt(requestLen - 1) === slashCode;
217 const requestHasStar = request.includes("*");
218
219 if (
220 !requestHasStar &&
221 !requestEndsWithSlash &&
222 Object.prototype.hasOwnProperty.call(field, request)
223 ) {
224 const target = /** @type {{ [k: string]: MappingValue }} */ (field)[
225 request
226 ];
227
228 return [target, "", false, false, request];
229 }
230
231 /** @type {string} */
232 let bestMatch = "";
233 /** @type {FieldKeyInfo | null} */
234 let bestMatchInfo = null;
235 /** @type {string | undefined} */
236 let bestMatchSubpath;
237
238 const infos = getFieldKeyInfos(field);
239
240 for (let i = 0; i < infos.length; i++) {
241 const info = infos[i];
242 const { key, patternIndex } = info;
243
244 if (patternIndex !== -1) {
245 if (
246 !info.isValidPattern ||
247 !request.startsWith(info.wildcardPrefix) ||
248 requestLen < key.length ||
249 !request.endsWith(info.wildcardSuffix) ||
250 patternKeyCompare(bestMatch, key) !== 1
251 ) {
252 continue;
253 }
254 bestMatch = key;
255 bestMatchInfo = info;
256 bestMatchSubpath = request.slice(
257 patternIndex,
258 requestLen - info.wildcardSuffix.length,
259 );
260 } else if (
261 info.isLegacySubpath &&
262 request.startsWith(key) &&
263 patternKeyCompare(bestMatch, key) === 1
264 ) {
265 bestMatch = key;
266 bestMatchInfo = info;
267 bestMatchSubpath = request.slice(key.length);
268 }
269 }
270
271 if (bestMatch === "") return null;
272
273 const target =
274 /** @type {{ [k: string]: MappingValue }} */
275 (field)[bestMatch];
276
277 return [
278 target,
279 /** @type {string} */ (bestMatchSubpath),
280 /** @type {FieldKeyInfo} */ (bestMatchInfo).isSubpathMapping,
281 /** @type {FieldKeyInfo} */ (bestMatchInfo).isPattern,
282 bestMatch,
283 ];
284}
285
286/**
287 * Trying to match request to field
288 * @param {string} request request
289 * @param {ExportsField | ImportsField} field exports or import field
290 * @returns {MatchTuple} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
291 */
292function findMatch(request, field) {
293 const fieldKey = /** @type {RecordMapping} */ (field);
294 let perRequest = _findMatchCache.get(fieldKey);
295 if (perRequest === undefined) {
296 perRequest = new Map();
297 _findMatchCache.set(fieldKey, perRequest);
298 } else {
299 // `computeFindMatch` only ever returns `MatchTuple | null` — never
300 // `undefined` — and `Map.set(k, null)` then `Map.get(k)` returns
301 // `null`, not `undefined`. So `get(...) === undefined` already
302 // unambiguously means "not cached yet"; one Map lookup is enough,
303 // no follow-up `has` needed to disambiguate "cached null".
304 const cached = perRequest.get(request);
305 if (cached !== undefined) return cached;
306 }
307
308 const result = computeFindMatch(request, field);
309 perRequest.set(request, result);
310 return result;
311}
312
313/**
314 * Sentinel stored in the conditional-mapping cache for inputs whose walk
315 * returns `null` ("no condition matched"). Using a non-null marker lets the
316 * cache-hit path be a single `WeakMap.get()` — we distinguish
317 * "cached null" from "not cached yet" without a second `has` call.
318 */
319const NULL_RESULT = Symbol("NULL_RESULT");
320
321/**
322 * Memoization of `conditionalMapping(mapping, conditionNames)`. The result
323 * depends only on the mapping object (immutable — owned by a parsed
324 * `package.json`) and the `conditionNames` Set (owned by the resolver's
325 * options and stable for its lifetime), so it is safe to cache per (mapping,
326 * conditionNames) pair.
327 *
328 * A conditional `exports` entry that appears inside a `directMapping` array
329 * (the common `"browser": [...fallback list...]` shape, plus nested
330 * conditions) gets walked on every resolve that traverses the parent entry.
331 * Without this cache each of those walks re-reads `Object.keys` on the
332 * mapping and re-visits every condition until one matches, even though the
333 * inputs are identical.
334 *
335 * Outer key is the conditional mapping itself; inner key is the condition
336 * Set. Both are object references, so WeakMap-of-WeakMap lets both levels
337 * be collected automatically when the description file or resolver go away.
338 * @type {WeakMap<ConditionalMapping, WeakMap<Set<string>, DirectMapping | typeof NULL_RESULT>>}
339 */
340const _conditionalMappingCache = new WeakMap();
341
342/**
343 * @param {ConditionalMapping} conditionalMapping_ conditional mapping
344 * @param {Set<string>} conditionNames condition names
345 * @returns {DirectMapping | null} direct mapping if found (uncached)
346 */
347function computeConditionalMapping(conditionalMapping_, conditionNames) {
348 /** @type {[ConditionalMapping, string[], number][]} */
349 const lookup = [[conditionalMapping_, cachedKeys(conditionalMapping_), 0]];
350
351 loop: while (lookup.length > 0) {
352 const top = lookup[lookup.length - 1];
353 const [mapping, conditions, j] = top;
354
355 for (let i = j; i < conditions.length; i++) {
356 const condition = conditions[i];
357
358 if (condition === "default" || conditionNames.has(condition)) {
359 const innerMapping = mapping[condition];
360 if (
361 innerMapping !== null &&
362 typeof innerMapping === "object" &&
363 !Array.isArray(innerMapping)
364 ) {
365 const nested = /** @type {ConditionalMapping} */ (innerMapping);
366 top[2] = i + 1;
367 lookup.push([nested, cachedKeys(nested), 0]);
368 continue loop;
369 }
370
371 return /** @type {DirectMapping} */ (innerMapping);
372 }
373 }
374
375 lookup.pop();
376 }
377
378 return null;
379}
380
381/**
382 * @param {ConditionalMapping} conditionalMapping_ conditional mapping
383 * @param {Set<string>} conditionNames condition names
384 * @returns {DirectMapping | null} direct mapping if found
385 */
386function conditionalMapping(conditionalMapping_, conditionNames) {
387 let perSet = _conditionalMappingCache.get(conditionalMapping_);
388 if (perSet !== undefined) {
389 const cached = perSet.get(conditionNames);
390 if (cached !== undefined) {
391 return cached === NULL_RESULT
392 ? null
393 : /** @type {DirectMapping} */ (cached);
394 }
395 } else {
396 perSet = new WeakMap();
397 _conditionalMappingCache.set(conditionalMapping_, perSet);
398 }
399 const result = computeConditionalMapping(conditionalMapping_, conditionNames);
400 perSet.set(conditionNames, result === null ? NULL_RESULT : result);
401 return result;
402}
403
404/**
405 * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
406 * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
407 * @param {boolean} isSubpathMapping true, for subpath mappings
408 * @param {string} mappingTarget direct export
409 * @param {(d: string, f: boolean) => void} assert asserting direct value
410 * @returns {string} mapping result
411 */
412function targetMapping(
413 remainingRequest,
414 isPattern,
415 isSubpathMapping,
416 mappingTarget,
417 assert,
418) {
419 if (remainingRequest === undefined) {
420 assert(mappingTarget, false);
421
422 return mappingTarget;
423 }
424
425 if (isSubpathMapping) {
426 assert(mappingTarget, true);
427
428 return mappingTarget + remainingRequest;
429 }
430
431 assert(mappingTarget, false);
432
433 let result = mappingTarget;
434
435 if (isPattern) {
436 const escapedRemainder = remainingRequest.includes("$")
437 ? remainingRequest.replace(DOLLAR_ESCAPE_RE, "$$")
438 : remainingRequest;
439 result = result.replace(patternRegEx, escapedRemainder);
440 }
441
442 return result;
443}
444
445/**
446 * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
447 * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
448 * @param {boolean} isSubpathMapping true, for subpath mappings
449 * @param {DirectMapping | null} mappingTarget direct export
450 * @param {Set<string>} conditionNames condition names
451 * @param {(d: string, f: boolean) => void} assert asserting direct value
452 * @returns {string[]} mapping result
453 */
454function directMapping(
455 remainingRequest,
456 isPattern,
457 isSubpathMapping,
458 mappingTarget,
459 conditionNames,
460 assert,
461) {
462 if (mappingTarget === null) return [];
463
464 if (typeof mappingTarget === "string") {
465 return [
466 targetMapping(
467 remainingRequest,
468 isPattern,
469 isSubpathMapping,
470 mappingTarget,
471 assert,
472 ),
473 ];
474 }
475
476 /** @type {string[]} */
477 const targets = [];
478
479 for (let i = 0, len = mappingTarget.length; i < len; i++) {
480 const exp = mappingTarget[i];
481 if (typeof exp === "string") {
482 targets.push(
483 targetMapping(
484 remainingRequest,
485 isPattern,
486 isSubpathMapping,
487 exp,
488 assert,
489 ),
490 );
491 continue;
492 }
493
494 const mapping = conditionalMapping(exp, conditionNames);
495 if (!mapping) continue;
496 const innerExports = directMapping(
497 remainingRequest,
498 isPattern,
499 isSubpathMapping,
500 mapping,
501 conditionNames,
502 assert,
503 );
504 for (let j = 0, innerLen = innerExports.length; j < innerLen; j++) {
505 targets.push(innerExports[j]);
506 }
507 }
508
509 return targets;
510}
511
512/** @type {[string[], null]} */
513const EMPTY_NO_MATCH = /** @type {[string[], null]} */ ([[], null]);
514
515/**
516 * @param {ExportsField | ImportsField} field root
517 * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
518 * @param {(s: string) => string} assertRequest assertRequest
519 * @param {(s: string, f: boolean) => void} assertTarget assertTarget
520 * @returns {FieldProcessor} field processor
521 */
522function createFieldProcessor(
523 field,
524 normalizeRequest,
525 assertRequest,
526 assertTarget,
527) {
528 return function fieldProcessor(request, conditionNames) {
529 const match = findMatch(normalizeRequest(assertRequest(request)), field);
530
531 if (match === null) return EMPTY_NO_MATCH;
532
533 const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
534 match;
535
536 /** @type {DirectMapping | null} */
537 let direct;
538 if (
539 mapping !== null &&
540 typeof mapping === "object" &&
541 !Array.isArray(mapping)
542 ) {
543 direct = conditionalMapping(
544 /** @type {ConditionalMapping} */ (mapping),
545 conditionNames,
546 );
547 if (direct === null) return EMPTY_NO_MATCH;
548 } else {
549 direct = /** @type {DirectMapping} */ (mapping);
550 }
551
552 return [
553 directMapping(
554 remainingRequest,
555 isPattern,
556 isSubpathMapping,
557 direct,
558 conditionNames,
559 assertTarget,
560 ),
561 usedField,
562 ];
563 };
564}
565
566/**
567 * @param {string} request request
568 * @returns {string} updated request
569 */
570function assertExportsFieldRequest(request) {
571 if (request.charCodeAt(0) !== dotCode) {
572 throw new Error('Request should be relative path and start with "."');
573 }
574 if (request.length === 1) return "";
575 if (request.charCodeAt(1) !== slashCode) {
576 throw new Error('Request should be relative path and start with "./"');
577 }
578 if (request.charCodeAt(request.length - 1) === slashCode) {
579 throw new Error("Only requesting file allowed");
580 }
581
582 return request.slice(2);
583}
584
585/**
586 * @param {ExportsField} field exports field
587 * @returns {ExportsField} normalized exports field
588 */
589function buildExportsField(field) {
590 // handle syntax sugar, if exports field is direct mapping for "."
591 if (typeof field === "string" || Array.isArray(field)) {
592 return { ".": field };
593 }
594
595 const keys = Object.keys(field);
596
597 for (let i = 0; i < keys.length; i++) {
598 const key = keys[i];
599
600 if (key.charCodeAt(0) !== dotCode) {
601 // handle syntax sugar, if exports field is conditional mapping for "."
602 if (i === 0) {
603 while (i < keys.length) {
604 const charCode = keys[i].charCodeAt(0);
605 if (charCode === dotCode || charCode === slashCode) {
606 throw new Error(
607 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
608 key,
609 )})`,
610 );
611 }
612 i++;
613 }
614
615 return { ".": field };
616 }
617
618 throw new Error(
619 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
620 key,
621 )})`,
622 );
623 }
624
625 if (key.length === 1) {
626 continue;
627 }
628
629 if (key.charCodeAt(1) !== slashCode) {
630 throw new Error(
631 `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
632 key,
633 )})`,
634 );
635 }
636 }
637
638 return field;
639}
640
641/**
642 * @param {string} exp export target
643 * @param {boolean} expectFolder is folder expected
644 */
645function assertExportTarget(exp, expectFolder) {
646 const parsedIdentifier = parseIdentifier(exp);
647
648 if (!parsedIdentifier) {
649 return;
650 }
651
652 const [relativePath] = parsedIdentifier;
653 const isFolder =
654 relativePath.charCodeAt(relativePath.length - 1) === slashCode;
655
656 if (isFolder !== expectFolder) {
657 throw new Error(
658 expectFolder
659 ? `Expecting folder to folder mapping. ${JSON.stringify(
660 exp,
661 )} should end with "/"`
662 : `Expecting file to file mapping. ${JSON.stringify(
663 exp,
664 )} should not end with "/"`,
665 );
666 }
667}
668
669/**
670 * @param {ExportsField} exportsField the exports field
671 * @returns {FieldProcessor} process callback
672 */
673module.exports.processExportsField = function processExportsField(
674 exportsField,
675) {
676 return createFieldProcessor(
677 buildExportsField(exportsField),
678 (request) => (request.length === 0 ? "." : `./${request}`),
679 assertExportsFieldRequest,
680 assertExportTarget,
681 );
682};
683
684/**
685 * @param {string} request request
686 * @returns {string} updated request
687 */
688function assertImportsFieldRequest(request) {
689 if (request.charCodeAt(0) !== hashCode) {
690 throw new Error('Request should start with "#"');
691 }
692 if (request.length === 1) {
693 throw new Error("Request should have at least 2 characters");
694 }
695 // Note: #/ patterns are now allowed per Node.js PR #60864
696 // https://github.com/nodejs/node/pull/60864
697 if (request.charCodeAt(request.length - 1) === slashCode) {
698 throw new Error("Only requesting file allowed");
699 }
700
701 return request.slice(1);
702}
703
704/**
705 * @param {string} imp import target
706 * @param {boolean} expectFolder is folder expected
707 */
708function assertImportTarget(imp, expectFolder) {
709 const parsedIdentifier = parseIdentifier(imp);
710
711 if (!parsedIdentifier) {
712 return;
713 }
714
715 const [relativePath] = parsedIdentifier;
716 const isFolder =
717 relativePath.charCodeAt(relativePath.length - 1) === slashCode;
718
719 if (isFolder !== expectFolder) {
720 throw new Error(
721 expectFolder
722 ? `Expecting folder to folder mapping. ${JSON.stringify(
723 imp,
724 )} should end with "/"`
725 : `Expecting file to file mapping. ${JSON.stringify(
726 imp,
727 )} should not end with "/"`,
728 );
729 }
730}
731
732/**
733 * @param {ImportsField} importsField the exports field
734 * @returns {FieldProcessor} process callback
735 */
736module.exports.processImportsField = function processImportsField(
737 importsField,
738) {
739 return createFieldProcessor(
740 importsField,
741 (request) => `#${request}`,
742 assertImportsFieldRequest,
743 assertImportTarget,
744 );
745};
Note: See TracBrowser for help on using the repository browser.