source: imaps-frontend/node_modules/enhanced-resolve/lib/util/entrypoints.js@ 79a0317

main
Last change on this file since 79a0317 was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 3 days ago

F4 Finalna Verzija

  • Property mode set to 100644
File size: 14.1 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 * Processing exports/imports field
16 * @callback FieldProcessor
17 * @param {string} request request
18 * @param {Set<string>} conditionNames condition names
19 * @returns {[string[], string | null]} resolved paths with used field
20 */
21
22/*
23Example exports field:
24{
25 ".": "./main.js",
26 "./feature": {
27 "browser": "./feature-browser.js",
28 "default": "./feature.js"
29 }
30}
31Terminology:
32
33Enhanced-resolve name keys ("." and "./feature") as exports field keys.
34
35If value is string or string[], mapping is called as a direct mapping
36and value called as a direct export.
37
38If value is key-value object, mapping is called as a conditional mapping
39and value called as a conditional export.
40
41Key in conditional mapping is called condition name.
42
43Conditional mapping nested in another conditional mapping is called nested mapping.
44
45----------
46
47Example imports field:
48{
49 "#a": "./main.js",
50 "#moment": {
51 "browser": "./moment/index.js",
52 "default": "moment"
53 },
54 "#moment/": {
55 "browser": "./moment/",
56 "default": "moment/"
57 }
58}
59Terminology:
60
61Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
62
63If value is string or string[], mapping is called as a direct mapping
64and value called as a direct export.
65
66If value is key-value object, mapping is called as a conditional mapping
67and value called as a conditional export.
68
69Key in conditional mapping is called condition name.
70
71Conditional mapping nested in another conditional mapping is called nested mapping.
72
73*/
74
75const { parseIdentifier } = require("./identifier");
76const slashCode = "/".charCodeAt(0);
77const dotCode = ".".charCodeAt(0);
78const hashCode = "#".charCodeAt(0);
79const patternRegEx = /\*/g;
80
81/**
82 * @param {ExportsField} exportsField the exports field
83 * @returns {FieldProcessor} process callback
84 */
85module.exports.processExportsField = function processExportsField(
86 exportsField
87) {
88 return createFieldProcessor(
89 buildExportsField(exportsField),
90 request => (request.length === 0 ? "." : "./" + request),
91 assertExportsFieldRequest,
92 assertExportTarget
93 );
94};
95
96/**
97 * @param {ImportsField} importsField the exports field
98 * @returns {FieldProcessor} process callback
99 */
100module.exports.processImportsField = function processImportsField(
101 importsField
102) {
103 return createFieldProcessor(
104 importsField,
105 request => "#" + request,
106 assertImportsFieldRequest,
107 assertImportTarget
108 );
109};
110
111/**
112 * @param {ExportsField | ImportsField} field root
113 * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
114 * @param {(s: string) => string} assertRequest assertRequest
115 * @param {(s: string, f: boolean) => void} assertTarget assertTarget
116 * @returns {FieldProcessor} field processor
117 */
118function createFieldProcessor(
119 field,
120 normalizeRequest,
121 assertRequest,
122 assertTarget
123) {
124 return function fieldProcessor(request, conditionNames) {
125 request = assertRequest(request);
126
127 const match = findMatch(normalizeRequest(request), field);
128
129 if (match === null) return [[], null];
130
131 const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
132 match;
133
134 /** @type {DirectMapping|null} */
135 let direct = null;
136
137 if (isConditionalMapping(mapping)) {
138 direct = conditionalMapping(
139 /** @type {ConditionalMapping} */ (mapping),
140 conditionNames
141 );
142
143 // matching not found
144 if (direct === null) return [[], null];
145 } else {
146 direct = /** @type {DirectMapping} */ (mapping);
147 }
148
149 return [
150 directMapping(
151 remainingRequest,
152 isPattern,
153 isSubpathMapping,
154 direct,
155 conditionNames,
156 assertTarget
157 ),
158 usedField
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 const parsedIdentifier = parseIdentifier(exp);
209
210 if (!parsedIdentifier) {
211 return;
212 }
213
214 const [relativePath] = parsedIdentifier;
215 const isFolder =
216 relativePath.charCodeAt(relativePath.length - 1) === slashCode;
217
218 if (isFolder !== expectFolder) {
219 throw new Error(
220 expectFolder
221 ? `Expecting folder to folder mapping. ${JSON.stringify(
222 exp
223 )} should end with "/"`
224 : `Expecting file to file mapping. ${JSON.stringify(
225 exp
226 )} should not end with "/"`
227 );
228 }
229}
230
231/**
232 * @param {string} imp import target
233 * @param {boolean} expectFolder is folder expected
234 */
235function assertImportTarget(imp, expectFolder) {
236 const parsedIdentifier = parseIdentifier(imp);
237
238 if (!parsedIdentifier) {
239 return;
240 }
241
242 const [relativePath] = parsedIdentifier;
243 const isFolder =
244 relativePath.charCodeAt(relativePath.length - 1) === slashCode;
245
246 if (isFolder !== expectFolder) {
247 throw new Error(
248 expectFolder
249 ? `Expecting folder to folder mapping. ${JSON.stringify(
250 imp
251 )} should end with "/"`
252 : `Expecting file to file mapping. ${JSON.stringify(
253 imp
254 )} should not end with "/"`
255 );
256 }
257}
258
259/**
260 * @param {string} a first string
261 * @param {string} b second string
262 * @returns {number} compare result
263 */
264function patternKeyCompare(a, b) {
265 const aPatternIndex = a.indexOf("*");
266 const bPatternIndex = b.indexOf("*");
267 const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
268 const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
269
270 if (baseLenA > baseLenB) return -1;
271 if (baseLenB > baseLenA) return 1;
272 if (aPatternIndex === -1) return 1;
273 if (bPatternIndex === -1) return -1;
274 if (a.length > b.length) return -1;
275 if (b.length > a.length) return 1;
276
277 return 0;
278}
279
280/**
281 * Trying to match request to field
282 * @param {string} request request
283 * @param {ExportsField | ImportsField} field exports or import field
284 * @returns {[MappingValue, string, boolean, boolean, string]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
285 */
286function findMatch(request, field) {
287 if (
288 Object.prototype.hasOwnProperty.call(field, request) &&
289 !request.includes("*") &&
290 !request.endsWith("/")
291 ) {
292 const target = /** @type {{[k: string]: MappingValue}} */ (field)[request];
293
294 return [target, "", false, false, request];
295 }
296
297 /** @type {string} */
298 let bestMatch = "";
299 /** @type {string|undefined} */
300 let bestMatchSubpath;
301
302 const keys = Object.getOwnPropertyNames(field);
303
304 for (let i = 0; i < keys.length; i++) {
305 const key = keys[i];
306 const patternIndex = key.indexOf("*");
307
308 if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) {
309 const patternTrailer = key.slice(patternIndex + 1);
310
311 if (
312 request.length >= key.length &&
313 request.endsWith(patternTrailer) &&
314 patternKeyCompare(bestMatch, key) === 1 &&
315 key.lastIndexOf("*") === patternIndex
316 ) {
317 bestMatch = key;
318 bestMatchSubpath = request.slice(
319 patternIndex,
320 request.length - patternTrailer.length
321 );
322 }
323 }
324 // For legacy `./foo/`
325 else if (
326 key[key.length - 1] === "/" &&
327 request.startsWith(key) &&
328 patternKeyCompare(bestMatch, key) === 1
329 ) {
330 bestMatch = key;
331 bestMatchSubpath = request.slice(key.length);
332 }
333 }
334
335 if (bestMatch === "") return null;
336
337 const target = /** @type {{[k: string]: MappingValue}} */ (field)[bestMatch];
338 const isSubpathMapping = bestMatch.endsWith("/");
339 const isPattern = bestMatch.includes("*");
340
341 return [
342 target,
343 /** @type {string} */ (bestMatchSubpath),
344 isSubpathMapping,
345 isPattern,
346 bestMatch
347 ];
348}
349
350/**
351 * @param {ConditionalMapping|DirectMapping|null} mapping mapping
352 * @returns {boolean} is conditional mapping
353 */
354function isConditionalMapping(mapping) {
355 return (
356 mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
357 );
358}
359
360/**
361 * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
362 * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
363 * @param {boolean} isSubpathMapping 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 isPattern,
372 isSubpathMapping,
373 mappingTarget,
374 conditionNames,
375 assert
376) {
377 if (mappingTarget === null) return [];
378
379 if (typeof mappingTarget === "string") {
380 return [
381 targetMapping(
382 remainingRequest,
383 isPattern,
384 isSubpathMapping,
385 mappingTarget,
386 assert
387 )
388 ];
389 }
390
391 /** @type {string[]} */
392 const targets = [];
393
394 for (const exp of mappingTarget) {
395 if (typeof exp === "string") {
396 targets.push(
397 targetMapping(
398 remainingRequest,
399 isPattern,
400 isSubpathMapping,
401 exp,
402 assert
403 )
404 );
405 continue;
406 }
407
408 const mapping = conditionalMapping(exp, conditionNames);
409 if (!mapping) continue;
410 const innerExports = directMapping(
411 remainingRequest,
412 isPattern,
413 isSubpathMapping,
414 mapping,
415 conditionNames,
416 assert
417 );
418 for (const innerExport of innerExports) {
419 targets.push(innerExport);
420 }
421 }
422
423 return targets;
424}
425
426/**
427 * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
428 * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
429 * @param {boolean} isSubpathMapping true, for subpath mappings
430 * @param {string} mappingTarget direct export
431 * @param {(d: string, f: boolean) => void} assert asserting direct value
432 * @returns {string} mapping result
433 */
434function targetMapping(
435 remainingRequest,
436 isPattern,
437 isSubpathMapping,
438 mappingTarget,
439 assert
440) {
441 if (remainingRequest === undefined) {
442 assert(mappingTarget, false);
443
444 return mappingTarget;
445 }
446
447 if (isSubpathMapping) {
448 assert(mappingTarget, true);
449
450 return mappingTarget + remainingRequest;
451 }
452
453 assert(mappingTarget, false);
454
455 let result = mappingTarget;
456
457 if (isPattern) {
458 result = result.replace(
459 patternRegEx,
460 remainingRequest.replace(/\$/g, "$$")
461 );
462 }
463
464 return result;
465}
466
467/**
468 * @param {ConditionalMapping} conditionalMapping_ conditional mapping
469 * @param {Set<string>} conditionNames condition names
470 * @returns {DirectMapping|null} direct mapping if found
471 */
472function conditionalMapping(conditionalMapping_, conditionNames) {
473 /** @type {[ConditionalMapping, string[], number][]} */
474 let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
475
476 loop: while (lookup.length > 0) {
477 const [mapping, conditions, j] = lookup[lookup.length - 1];
478
479 for (let i = j; i < conditions.length; i++) {
480 const condition = conditions[i];
481
482 if (condition === "default") {
483 const innerMapping = mapping[condition];
484 // is nested
485 if (isConditionalMapping(innerMapping)) {
486 const conditionalMapping = /** @type {ConditionalMapping} */ (
487 innerMapping
488 );
489 lookup[lookup.length - 1][2] = i + 1;
490 lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
491 continue loop;
492 }
493
494 return /** @type {DirectMapping} */ (innerMapping);
495 }
496
497 if (conditionNames.has(condition)) {
498 const innerMapping = mapping[condition];
499 // is nested
500 if (isConditionalMapping(innerMapping)) {
501 const conditionalMapping = /** @type {ConditionalMapping} */ (
502 innerMapping
503 );
504 lookup[lookup.length - 1][2] = i + 1;
505 lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
506 continue loop;
507 }
508
509 return /** @type {DirectMapping} */ (innerMapping);
510 }
511 }
512
513 lookup.pop();
514 }
515
516 return null;
517}
518
519/**
520 * @param {ExportsField} field exports field
521 * @returns {ExportsField} normalized exports field
522 */
523function buildExportsField(field) {
524 // handle syntax sugar, if exports field is direct mapping for "."
525 if (typeof field === "string" || Array.isArray(field)) {
526 return { ".": field };
527 }
528
529 const keys = Object.keys(field);
530
531 for (let i = 0; i < keys.length; i++) {
532 const key = keys[i];
533
534 if (key.charCodeAt(0) !== dotCode) {
535 // handle syntax sugar, if exports field is conditional mapping for "."
536 if (i === 0) {
537 while (i < keys.length) {
538 const charCode = keys[i].charCodeAt(0);
539 if (charCode === dotCode || charCode === slashCode) {
540 throw new Error(
541 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
542 key
543 )})`
544 );
545 }
546 i++;
547 }
548
549 return { ".": field };
550 }
551
552 throw new Error(
553 `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
554 key
555 )})`
556 );
557 }
558
559 if (key.length === 1) {
560 continue;
561 }
562
563 if (key.charCodeAt(1) !== slashCode) {
564 throw new Error(
565 `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
566 key
567 )})`
568 );
569 }
570 }
571
572 return field;
573}
Note: See TracBrowser for help on using the repository browser.