Apply module bundling
[platform/framework/web/wrtjs.git] / node_modules / enhanced-resolve / lib / util / entrypoints.js
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 /*
31 Example exports field:
32 {
33   ".": "./main.js",
34   "./feature": {
35     "browser": "./feature-browser.js",
36     "default": "./feature.js"
37   }
38 }
39 Terminology:
40
41 Enhanced-resolve name keys ("." and "./feature") as exports field keys.
42
43 If value is string or string[], mapping is called as a direct mapping
44 and value called as a direct export.
45
46 If value is key-value object, mapping is called as a conditional mapping
47 and value called as a conditional export.
48
49 Key in conditional mapping is called condition name.
50
51 Conditional mapping nested in another conditional mapping is called nested mapping.
52
53 ----------
54
55 Example 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 }
67 Terminology:
68
69 Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
70
71 If value is string or string[], mapping is called as a direct mapping
72 and value called as a direct export.
73
74 If value is key-value object, mapping is called as a conditional mapping
75 and value called as a conditional export.
76
77 Key in conditional mapping is called condition name.
78
79 Conditional mapping nested in another conditional mapping is called nested mapping.
80
81 */
82
83 const slashCode = "/".charCodeAt(0);
84 const dotCode = ".".charCodeAt(0);
85 const hashCode = "#".charCodeAt(0);
86
87 /**
88  * @param {ExportsField} exportsField the exports field
89  * @returns {FieldProcessor} process callback
90  */
91 module.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  */
105 module.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  */
121 function 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  */
167 function 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  */
186 function 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  */
207 function 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  */
238 function 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  */
260 function 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  */
355 function 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  */
369 function 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  */
418 function 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  */
441 function 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  */
495 function 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  */
510 function 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  */
565 function 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  */
633 function 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 }