source: trip-planner-front/node_modules/enhanced-resolve/lib/util/entrypoints.js@ fa375fe

Last change on this file since fa375fe was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 16.3 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Ivan Kopeykin @vankop
4*/
5
6"use strict";
7
8/** @typedef {string|(string|ConditionalMapping)[]} DirectMapping */
9/** @typedef {{[k: string]: MappingValue}} ConditionalMapping */
10/** @typedef {ConditionalMapping|DirectMapping|null} MappingValue */
11/** @typedef {Record<string, MappingValue>|ConditionalMapping|DirectMapping} ExportsField */
12/** @typedef {Record<string, MappingValue>} ImportsField */
13
14/**
15 * @typedef {Object} PathTreeNode
16 * @property {Map<string, PathTreeNode>|null} children
17 * @property {MappingValue} folder
18 * @property {Map<string, MappingValue>|null} wildcards
19 * @property {Map<string, MappingValue>} files
20 */
21
22/**
23 * Processing exports/imports field
24 * @callback FieldProcessor
25 * @param {string} request request
26 * @param {Set<string>} conditionNames condition names
27 * @returns {string[]} resolved paths
28 */
29
30/*
31Example exports field:
32{
33 ".": "./main.js",
34 "./feature": {
35 "browser": "./feature-browser.js",
36 "default": "./feature.js"
37 }
38}
39Terminology:
40
41Enhanced-resolve name keys ("." and "./feature") as exports field keys.
42
43If value is string or string[], mapping is called as a direct mapping
44and value called as a direct export.
45
46If value is key-value object, mapping is called as a conditional mapping
47and value called as a conditional export.
48
49Key in conditional mapping is called condition name.
50
51Conditional mapping nested in another conditional mapping is called nested mapping.
52
53----------
54
55Example imports field:
56{
57 "#a": "./main.js",
58 "#moment": {
59 "browser": "./moment/index.js",
60 "default": "moment"
61 },
62 "#moment/": {
63 "browser": "./moment/",
64 "default": "moment/"
65 }
66}
67Terminology:
68
69Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
70
71If value is string or string[], mapping is called as a direct mapping
72and value called as a direct export.
73
74If value is key-value object, mapping is called as a conditional mapping
75and value called as a conditional export.
76
77Key in conditional mapping is called condition name.
78
79Conditional mapping nested in another conditional mapping is called nested mapping.
80
81*/
82
83const slashCode = "/".charCodeAt(0);
84const dotCode = ".".charCodeAt(0);
85const hashCode = "#".charCodeAt(0);
86
87/**
88 * @param {ExportsField} exportsField the exports field
89 * @returns {FieldProcessor} process callback
90 */
91module.exports.processExportsField = function processExportsField(
92 exportsField
93) {
94 return createFieldProcessor(
95 buildExportsFieldPathTree(exportsField),
96 assertExportsFieldRequest,
97 assertExportTarget
98 );
99};
100
101/**
102 * @param {ImportsField} importsField the exports field
103 * @returns {FieldProcessor} process callback
104 */
105module.exports.processImportsField = function processImportsField(
106 importsField
107) {
108 return createFieldProcessor(
109 buildImportsFieldPathTree(importsField),
110 assertImportsFieldRequest,
111 assertImportTarget
112 );
113};
114
115/**
116 * @param {PathTreeNode} treeRoot root
117 * @param {(s: string) => string} assertRequest assertRequest
118 * @param {(s: string, f: boolean) => void} assertTarget assertTarget
119 * @returns {FieldProcessor} field processor
120 */
121function createFieldProcessor(treeRoot, assertRequest, assertTarget) {
122 return function fieldProcessor(request, conditionNames) {
123 request = assertRequest(request);
124
125 const match = findMatch(request, treeRoot);
126
127 if (match === null) return [];
128
129 const [mapping, remainRequestIndex] = match;
130
131 /** @type {DirectMapping|null} */
132 let direct = null;
133
134 if (isConditionalMapping(mapping)) {
135 direct = conditionalMapping(
136 /** @type {ConditionalMapping} */ (mapping),
137 conditionNames
138 );
139
140 // matching not found
141 if (direct === null) return [];
142 } else {
143 direct = /** @type {DirectMapping} */ (mapping);
144 }
145
146 const remainingRequest =
147 remainRequestIndex === request.length + 1
148 ? undefined
149 : remainRequestIndex < 0
150 ? request.slice(-remainRequestIndex - 1)
151 : request.slice(remainRequestIndex);
152
153 return directMapping(
154 remainingRequest,
155 remainRequestIndex < 0,
156 direct,
157 conditionNames,
158 assertTarget
159 );
160 };
161}
162
163/**
164 * @param {string} request request
165 * @returns {string} updated request
166 */
167function assertExportsFieldRequest(request) {
168 if (request.charCodeAt(0) !== dotCode) {
169 throw new Error('Request should be relative path and start with "."');
170 }
171 if (request.length === 1) return "";
172 if (request.charCodeAt(1) !== slashCode) {
173 throw new Error('Request should be relative path and start with "./"');
174 }
175 if (request.charCodeAt(request.length - 1) === slashCode) {
176 throw new Error("Only requesting file allowed");
177 }
178
179 return request.slice(2);
180}
181
182/**
183 * @param {string} request request
184 * @returns {string} updated request
185 */
186function assertImportsFieldRequest(request) {
187 if (request.charCodeAt(0) !== hashCode) {
188 throw new Error('Request should start with "#"');
189 }
190 if (request.length === 1) {
191 throw new Error("Request should have at least 2 characters");
192 }
193 if (request.charCodeAt(1) === slashCode) {
194 throw new Error('Request should not start with "#/"');
195 }
196 if (request.charCodeAt(request.length - 1) === slashCode) {
197 throw new Error("Only requesting file allowed");
198 }
199
200 return request.slice(1);
201}
202
203/**
204 * @param {string} exp export target
205 * @param {boolean} expectFolder is folder expected
206 */
207function assertExportTarget(exp, expectFolder) {
208 if (
209 exp.charCodeAt(0) === slashCode ||
210 (exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode)
211 ) {
212 throw new Error(
213 `Export should be relative path and start with "./", got ${JSON.stringify(
214 exp
215 )}.`
216 );
217 }
218
219 const isFolder = exp.charCodeAt(exp.length - 1) === slashCode;
220
221 if (isFolder !== expectFolder) {
222 throw new Error(
223 expectFolder
224 ? `Expecting folder to folder mapping. ${JSON.stringify(
225 exp
226 )} should end with "/"`
227 : `Expecting file to file mapping. ${JSON.stringify(
228 exp
229 )} should not end with "/"`
230 );
231 }
232}
233
234/**
235 * @param {string} imp import target
236 * @param {boolean} expectFolder is folder expected
237 */
238function assertImportTarget(imp, expectFolder) {
239 const isFolder = imp.charCodeAt(imp.length - 1) === slashCode;
240
241 if (isFolder !== expectFolder) {
242 throw new Error(
243 expectFolder
244 ? `Expecting folder to folder mapping. ${JSON.stringify(
245 imp
246 )} should end with "/"`
247 : `Expecting file to file mapping. ${JSON.stringify(
248 imp
249 )} should not end with "/"`
250 );
251 }
252}
253
254/**
255 * Trying to match request to field
256 * @param {string} request request
257 * @param {PathTreeNode} treeRoot path tree root
258 * @returns {[MappingValue, number]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
259 */
260function findMatch(request, treeRoot) {
261 if (request.length === 0) {
262 const value = treeRoot.files.get("");
263
264 return value ? [value, 1] : null;
265 }
266
267 if (
268 treeRoot.children === null &&
269 treeRoot.folder === null &&
270 treeRoot.wildcards === null
271 ) {
272 const value = treeRoot.files.get(request);
273
274 return value ? [value, request.length + 1] : null;
275 }
276
277 let node = treeRoot;
278 let lastNonSlashIndex = 0;
279 let slashIndex = request.indexOf("/", 0);
280
281 /** @type {[MappingValue, number]|null} */
282 let lastFolderMatch = null;
283
284 const applyFolderMapping = () => {
285 const folderMapping = node.folder;
286 if (folderMapping) {
287 if (lastFolderMatch) {
288 lastFolderMatch[0] = folderMapping;
289 lastFolderMatch[1] = -lastNonSlashIndex - 1;
290 } else {
291 lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1];
292 }
293 }
294 };
295
296 const applyWildcardMappings = (wildcardMappings, remainingRequest) => {
297 if (wildcardMappings) {
298 for (const [key, target] of wildcardMappings) {
299 if (remainingRequest.startsWith(key)) {
300 if (!lastFolderMatch) {
301 lastFolderMatch = [target, lastNonSlashIndex + key.length];
302 } else if (lastFolderMatch[1] < lastNonSlashIndex + key.length) {
303 lastFolderMatch[0] = target;
304 lastFolderMatch[1] = lastNonSlashIndex + key.length;
305 }
306 }
307 }
308 }
309 };
310
311 while (slashIndex !== -1) {
312 applyFolderMapping();
313
314 const wildcardMappings = node.wildcards;
315
316 if (!wildcardMappings && node.children === null) return lastFolderMatch;
317
318 const folder = request.slice(lastNonSlashIndex, slashIndex);
319
320 applyWildcardMappings(wildcardMappings, folder);
321
322 if (node.children === null) return lastFolderMatch;
323
324 const newNode = node.children.get(folder);
325
326 if (!newNode) {
327 return lastFolderMatch;
328 }
329
330 node = newNode;
331 lastNonSlashIndex = slashIndex + 1;
332 slashIndex = request.indexOf("/", lastNonSlashIndex);
333 }
334
335 const remainingRequest =
336 lastNonSlashIndex > 0 ? request.slice(lastNonSlashIndex) : request;
337
338 const value = node.files.get(remainingRequest);
339
340 if (value) {
341 return [value, request.length + 1];
342 }
343
344 applyFolderMapping();
345
346 applyWildcardMappings(node.wildcards, remainingRequest);
347
348 return lastFolderMatch;
349}
350
351/**
352 * @param {ConditionalMapping|DirectMapping|null} mapping mapping
353 * @returns {boolean} is conditional mapping
354 */
355function isConditionalMapping(mapping) {
356 return (
357 mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
358 );
359}
360
361/**
362 * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
363 * @param {boolean} subpathMapping true, for subpath mappings
364 * @param {DirectMapping|null} mappingTarget direct export
365 * @param {Set<string>} conditionNames condition names
366 * @param {(d: string, f: boolean) => void} assert asserting direct value
367 * @returns {string[]} mapping result
368 */
369function directMapping(
370 remainingRequest,
371 subpathMapping,
372 mappingTarget,
373 conditionNames,
374 assert
375) {
376 if (mappingTarget === null) return [];
377
378 if (typeof mappingTarget === "string") {
379 return [
380 targetMapping(remainingRequest, subpathMapping, mappingTarget, assert)
381 ];
382 }
383
384 const targets = [];
385
386 for (const exp of mappingTarget) {
387 if (typeof exp === "string") {
388 targets.push(
389 targetMapping(remainingRequest, subpathMapping, exp, assert)
390 );
391 continue;
392 }
393
394 const mapping = conditionalMapping(exp, conditionNames);
395 if (!mapping) continue;
396 const innerExports = directMapping(
397 remainingRequest,
398 subpathMapping,
399 mapping,
400 conditionNames,
401 assert
402 );
403 for (const innerExport of innerExports) {
404 targets.push(innerExport);
405 }
406 }
407
408 return targets;
409}
410
411/**
412 * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
413 * @param {boolean} subpathMapping true, for subpath mappings
414 * @param {string} mappingTarget direct export
415 * @param {(d: string, f: boolean) => void} assert asserting direct value
416 * @returns {string} mapping result
417 */
418function targetMapping(
419 remainingRequest,
420 subpathMapping,
421 mappingTarget,
422 assert
423) {
424 if (remainingRequest === undefined) {
425 assert(mappingTarget, false);
426 return mappingTarget;
427 }
428 if (subpathMapping) {
429 assert(mappingTarget, true);
430 return mappingTarget + remainingRequest;
431 }
432 assert(mappingTarget, false);
433 return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$"));
434}
435
436/**
437 * @param {ConditionalMapping} conditionalMapping_ conditional mapping
438 * @param {Set<string>} conditionNames condition names
439 * @returns {DirectMapping|null} direct mapping if found
440 */
441function conditionalMapping(conditionalMapping_, conditionNames) {
442 /** @type {[ConditionalMapping, string[], number][]} */
443 let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
444
445 loop: while (lookup.length > 0) {
446 const [mapping, conditions, j] = lookup[lookup.length - 1];
447 const last = conditions.length - 1;
448
449 for (let i = j; i < conditions.length; i++) {
450 const condition = conditions[i];
451
452 // assert default. Could be last only
453 if (i !== last) {
454 if (condition === "default") {
455 throw new Error("Default condition should be last one");
456 }
457 } else if (condition === "default") {
458 const innerMapping = mapping[condition];
459 // is nested
460 if (isConditionalMapping(innerMapping)) {
461 const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
462 lookup[lookup.length - 1][2] = i + 1;
463 lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
464 continue loop;
465 }
466
467 return /** @type {DirectMapping} */ (innerMapping);
468 }
469
470 if (conditionNames.has(condition)) {
471 const innerMapping = mapping[condition];
472 // is nested
473 if (isConditionalMapping(innerMapping)) {
474 const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
475 lookup[lookup.length - 1][2] = i + 1;
476 lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
477 continue loop;
478 }
479
480 return /** @type {DirectMapping} */ (innerMapping);
481 }
482 }
483
484 lookup.pop();
485 }
486
487 return null;
488}
489
490/**
491 * Internal helper to create path tree node
492 * to ensure that each node gets the same hidden class
493 * @returns {PathTreeNode} node
494 */
495function createNode() {
496 return {
497 children: null,
498 folder: null,
499 wildcards: null,
500 files: new Map()
501 };
502}
503
504/**
505 * Internal helper for building path tree
506 * @param {PathTreeNode} root root
507 * @param {string} path path
508 * @param {MappingValue} target target
509 */
510function walkPath(root, path, target) {
511 if (path.length === 0) {
512 root.folder = target;
513 return;
514 }
515
516 let node = root;
517 // Typical path tree can looks like
518 // root
519 // - files: ["a.js", "b.js"]
520 // - children:
521 // node1:
522 // - files: ["a.js", "b.js"]
523 let lastNonSlashIndex = 0;
524 let slashIndex = path.indexOf("/", 0);
525
526 while (slashIndex !== -1) {
527 const folder = path.slice(lastNonSlashIndex, slashIndex);
528 let newNode;
529
530 if (node.children === null) {
531 newNode = createNode();
532 node.children = new Map();
533 node.children.set(folder, newNode);
534 } else {
535 newNode = node.children.get(folder);
536
537 if (!newNode) {
538 newNode = createNode();
539 node.children.set(folder, newNode);
540 }
541 }
542
543 node = newNode;
544 lastNonSlashIndex = slashIndex + 1;
545 slashIndex = path.indexOf("/", lastNonSlashIndex);
546 }
547
548 if (lastNonSlashIndex >= path.length) {
549 node.folder = target;
550 } else {
551 const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path;
552 if (file.endsWith("*")) {
553 if (node.wildcards === null) node.wildcards = new Map();
554 node.wildcards.set(file.slice(0, -1), target);
555 } else {
556 node.files.set(file, target);
557 }
558 }
559}
560
561/**
562 * @param {ExportsField} field exports field
563 * @returns {PathTreeNode} tree root
564 */
565function buildExportsFieldPathTree(field) {
566 const root = createNode();
567
568 // handle syntax sugar, if exports field is direct mapping for "."
569 if (typeof field === "string") {
570 root.files.set("", field);
571
572 return root;
573 } else if (Array.isArray(field)) {
574 root.files.set("", field.slice());
575
576 return root;
577 }
578
579 const keys = Object.keys(field);
580
581 for (let i = 0; i < keys.length; i++) {
582 const key = keys[i];
583
584 if (key.charCodeAt(0) !== dotCode) {
585 // handle syntax sugar, if exports field is conditional mapping for "."
586 if (i === 0) {
587 while (i < keys.length) {
588 const charCode = keys[i].charCodeAt(0);
589 if (charCode === dotCode || charCode === slashCode) {
590 throw new Error(
591 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
592 key
593 )})`
594 );
595 }
596 i++;
597 }
598
599 root.files.set("", field);
600 return root;
601 }
602
603 throw new Error(
604 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
605 key
606 )})`
607 );
608 }
609
610 if (key.length === 1) {
611 root.files.set("", field[key]);
612 continue;
613 }
614
615 if (key.charCodeAt(1) !== slashCode) {
616 throw new Error(
617 `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
618 key
619 )})`
620 );
621 }
622
623 walkPath(root, key.slice(2), field[key]);
624 }
625
626 return root;
627}
628
629/**
630 * @param {ImportsField} field imports field
631 * @returns {PathTreeNode} root
632 */
633function buildImportsFieldPathTree(field) {
634 const root = createNode();
635
636 const keys = Object.keys(field);
637
638 for (let i = 0; i < keys.length; i++) {
639 const key = keys[i];
640
641 if (key.charCodeAt(0) !== hashCode) {
642 throw new Error(
643 `Imports field key should start with "#" (key: ${JSON.stringify(key)})`
644 );
645 }
646
647 if (key.length === 1) {
648 throw new Error(
649 `Imports field key should have at least 2 characters (key: ${JSON.stringify(
650 key
651 )})`
652 );
653 }
654
655 if (key.charCodeAt(1) === slashCode) {
656 throw new Error(
657 `Imports field key should not start with "#/" (key: ${JSON.stringify(
658 key
659 )})`
660 );
661 }
662
663 walkPath(root, key.slice(1), field[key]);
664 }
665
666 return root;
667}
Note: See TracBrowser for help on using the repository browser.