@jsenv/test 3.5.45 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,602 @@
1
+ // duplicated from @jsenv/log to avoid the dependency
2
+ const createDetailedMessage = (message, details = {}) => {
3
+ let string = `${message}`;
4
+
5
+ Object.keys(details).forEach((key) => {
6
+ const value = details[key];
7
+ string += `
8
+ --- ${key} ---
9
+ ${
10
+ Array.isArray(value)
11
+ ? value.join(`
12
+ `)
13
+ : value
14
+ }`;
15
+ });
16
+
17
+ return string
18
+ };
19
+
20
+ const assertImportMap = (value) => {
21
+ if (value === null) {
22
+ throw new TypeError(`an importMap must be an object, got null`)
23
+ }
24
+
25
+ const type = typeof value;
26
+ if (type !== "object") {
27
+ throw new TypeError(`an importMap must be an object, received ${value}`)
28
+ }
29
+
30
+ if (Array.isArray(value)) {
31
+ throw new TypeError(
32
+ `an importMap must be an object, received array ${value}`,
33
+ )
34
+ }
35
+ };
36
+
37
+ const hasScheme = (string) => {
38
+ return /^[a-zA-Z]{2,}:/.test(string)
39
+ };
40
+
41
+ const urlToScheme = (urlString) => {
42
+ const colonIndex = urlString.indexOf(":");
43
+ if (colonIndex === -1) return ""
44
+ return urlString.slice(0, colonIndex)
45
+ };
46
+
47
+ const urlToPathname = (urlString) => {
48
+ return ressourceToPathname(urlToRessource(urlString))
49
+ };
50
+
51
+ const urlToRessource = (urlString) => {
52
+ const scheme = urlToScheme(urlString);
53
+
54
+ if (scheme === "file") {
55
+ return urlString.slice("file://".length)
56
+ }
57
+
58
+ if (scheme === "https" || scheme === "http") {
59
+ // remove origin
60
+ const afterProtocol = urlString.slice(scheme.length + "://".length);
61
+ const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
62
+ return afterProtocol.slice(pathnameSlashIndex)
63
+ }
64
+
65
+ return urlString.slice(scheme.length + 1)
66
+ };
67
+
68
+ const ressourceToPathname = (ressource) => {
69
+ const searchSeparatorIndex = ressource.indexOf("?");
70
+ return searchSeparatorIndex === -1
71
+ ? ressource
72
+ : ressource.slice(0, searchSeparatorIndex)
73
+ };
74
+
75
+ const urlToOrigin = (urlString) => {
76
+ const scheme = urlToScheme(urlString);
77
+
78
+ if (scheme === "file") {
79
+ return "file://"
80
+ }
81
+
82
+ if (scheme === "http" || scheme === "https") {
83
+ const secondProtocolSlashIndex = scheme.length + "://".length;
84
+ const pathnameSlashIndex = urlString.indexOf("/", secondProtocolSlashIndex);
85
+
86
+ if (pathnameSlashIndex === -1) return urlString
87
+ return urlString.slice(0, pathnameSlashIndex)
88
+ }
89
+
90
+ return urlString.slice(0, scheme.length + 1)
91
+ };
92
+
93
+ const pathnameToParentPathname = (pathname) => {
94
+ const slashLastIndex = pathname.lastIndexOf("/");
95
+ if (slashLastIndex === -1) {
96
+ return "/"
97
+ }
98
+
99
+ return pathname.slice(0, slashLastIndex + 1)
100
+ };
101
+
102
+ // could be useful: https://url.spec.whatwg.org/#url-miscellaneous
103
+
104
+
105
+ const resolveUrl = (specifier, baseUrl) => {
106
+ if (baseUrl) {
107
+ if (typeof baseUrl !== "string") {
108
+ throw new TypeError(writeBaseUrlMustBeAString({ baseUrl, specifier }))
109
+ }
110
+ if (!hasScheme(baseUrl)) {
111
+ throw new Error(writeBaseUrlMustBeAbsolute({ baseUrl, specifier }))
112
+ }
113
+ }
114
+
115
+ if (hasScheme(specifier)) {
116
+ return specifier
117
+ }
118
+
119
+ if (!baseUrl) {
120
+ throw new Error(writeBaseUrlRequired({ baseUrl, specifier }))
121
+ }
122
+
123
+ // scheme relative
124
+ if (specifier.slice(0, 2) === "//") {
125
+ return `${urlToScheme(baseUrl)}:${specifier}`
126
+ }
127
+
128
+ // origin relative
129
+ if (specifier[0] === "/") {
130
+ return `${urlToOrigin(baseUrl)}${specifier}`
131
+ }
132
+
133
+ const baseOrigin = urlToOrigin(baseUrl);
134
+ const basePathname = urlToPathname(baseUrl);
135
+
136
+ if (specifier === ".") {
137
+ const baseDirectoryPathname = pathnameToParentPathname(basePathname);
138
+ return `${baseOrigin}${baseDirectoryPathname}`
139
+ }
140
+
141
+ // pathname relative inside
142
+ if (specifier.slice(0, 2) === "./") {
143
+ const baseDirectoryPathname = pathnameToParentPathname(basePathname);
144
+ return `${baseOrigin}${baseDirectoryPathname}${specifier.slice(2)}`
145
+ }
146
+
147
+ // pathname relative outside
148
+ if (specifier.slice(0, 3) === "../") {
149
+ let unresolvedPathname = specifier;
150
+ const importerFolders = basePathname.split("/");
151
+ importerFolders.pop();
152
+
153
+ while (unresolvedPathname.slice(0, 3) === "../") {
154
+ unresolvedPathname = unresolvedPathname.slice(3);
155
+ // when there is no folder left to resolved
156
+ // we just ignore '../'
157
+ if (importerFolders.length) {
158
+ importerFolders.pop();
159
+ }
160
+ }
161
+
162
+ const resolvedPathname = `${importerFolders.join(
163
+ "/",
164
+ )}/${unresolvedPathname}`;
165
+ return `${baseOrigin}${resolvedPathname}`
166
+ }
167
+
168
+ // bare
169
+ if (basePathname === "") {
170
+ return `${baseOrigin}/${specifier}`
171
+ }
172
+ if (basePathname[basePathname.length] === "/") {
173
+ return `${baseOrigin}${basePathname}${specifier}`
174
+ }
175
+ return `${baseOrigin}${pathnameToParentPathname(basePathname)}${specifier}`
176
+ };
177
+
178
+ const writeBaseUrlMustBeAString = ({
179
+ baseUrl,
180
+ specifier,
181
+ }) => `baseUrl must be a string.
182
+ --- base url ---
183
+ ${baseUrl}
184
+ --- specifier ---
185
+ ${specifier}`;
186
+
187
+ const writeBaseUrlMustBeAbsolute = ({
188
+ baseUrl,
189
+ specifier,
190
+ }) => `baseUrl must be absolute.
191
+ --- base url ---
192
+ ${baseUrl}
193
+ --- specifier ---
194
+ ${specifier}`;
195
+
196
+ const writeBaseUrlRequired = ({
197
+ baseUrl,
198
+ specifier,
199
+ }) => `baseUrl required to resolve relative specifier.
200
+ --- base url ---
201
+ ${baseUrl}
202
+ --- specifier ---
203
+ ${specifier}`;
204
+
205
+ const tryUrlResolution = (string, url) => {
206
+ const result = resolveUrl(string, url);
207
+ return hasScheme(result) ? result : null
208
+ };
209
+
210
+ const resolveSpecifier = (specifier, importer) => {
211
+ if (
212
+ specifier === "." ||
213
+ specifier[0] === "/" ||
214
+ specifier.startsWith("./") ||
215
+ specifier.startsWith("../")
216
+ ) {
217
+ return resolveUrl(specifier, importer)
218
+ }
219
+
220
+ if (hasScheme(specifier)) {
221
+ return specifier
222
+ }
223
+
224
+ return null
225
+ };
226
+
227
+ const applyImportMap = ({
228
+ importMap,
229
+ specifier,
230
+ importer,
231
+ createBareSpecifierError = ({ specifier, importer }) => {
232
+ return new Error(
233
+ createDetailedMessage(`Unmapped bare specifier.`, {
234
+ specifier,
235
+ importer,
236
+ }),
237
+ )
238
+ },
239
+ onImportMapping = () => {},
240
+ }) => {
241
+ assertImportMap(importMap);
242
+ if (typeof specifier !== "string") {
243
+ throw new TypeError(
244
+ createDetailedMessage("specifier must be a string.", {
245
+ specifier,
246
+ importer,
247
+ }),
248
+ )
249
+ }
250
+ if (importer) {
251
+ if (typeof importer !== "string") {
252
+ throw new TypeError(
253
+ createDetailedMessage("importer must be a string.", {
254
+ importer,
255
+ specifier,
256
+ }),
257
+ )
258
+ }
259
+ if (!hasScheme(importer)) {
260
+ throw new Error(
261
+ createDetailedMessage(`importer must be an absolute url.`, {
262
+ importer,
263
+ specifier,
264
+ }),
265
+ )
266
+ }
267
+ }
268
+
269
+ const specifierUrl = resolveSpecifier(specifier, importer);
270
+ const specifierNormalized = specifierUrl || specifier;
271
+
272
+ const { scopes } = importMap;
273
+ if (scopes && importer) {
274
+ const scopeSpecifierMatching = Object.keys(scopes).find(
275
+ (scopeSpecifier) => {
276
+ return (
277
+ scopeSpecifier === importer ||
278
+ specifierIsPrefixOf(scopeSpecifier, importer)
279
+ )
280
+ },
281
+ );
282
+ if (scopeSpecifierMatching) {
283
+ const scopeMappings = scopes[scopeSpecifierMatching];
284
+ const mappingFromScopes = applyMappings(
285
+ scopeMappings,
286
+ specifierNormalized,
287
+ scopeSpecifierMatching,
288
+ onImportMapping,
289
+ );
290
+ if (mappingFromScopes !== null) {
291
+ return mappingFromScopes
292
+ }
293
+ }
294
+ }
295
+
296
+ const { imports } = importMap;
297
+ if (imports) {
298
+ const mappingFromImports = applyMappings(
299
+ imports,
300
+ specifierNormalized,
301
+ undefined,
302
+ onImportMapping,
303
+ );
304
+ if (mappingFromImports !== null) {
305
+ return mappingFromImports
306
+ }
307
+ }
308
+
309
+ if (specifierUrl) {
310
+ return specifierUrl
311
+ }
312
+
313
+ throw createBareSpecifierError({ specifier, importer })
314
+ };
315
+
316
+ const applyMappings = (
317
+ mappings,
318
+ specifierNormalized,
319
+ scope,
320
+ onImportMapping,
321
+ ) => {
322
+ const specifierCandidates = Object.keys(mappings);
323
+
324
+ let i = 0;
325
+ while (i < specifierCandidates.length) {
326
+ const specifierCandidate = specifierCandidates[i];
327
+ i++;
328
+ if (specifierCandidate === specifierNormalized) {
329
+ const address = mappings[specifierCandidate];
330
+ onImportMapping({
331
+ scope,
332
+ from: specifierCandidate,
333
+ to: address,
334
+ before: specifierNormalized,
335
+ after: address,
336
+ });
337
+ return address
338
+ }
339
+ if (specifierIsPrefixOf(specifierCandidate, specifierNormalized)) {
340
+ const address = mappings[specifierCandidate];
341
+ const afterSpecifier = specifierNormalized.slice(
342
+ specifierCandidate.length,
343
+ );
344
+ const addressFinal = tryUrlResolution(afterSpecifier, address);
345
+ onImportMapping({
346
+ scope,
347
+ from: specifierCandidate,
348
+ to: address,
349
+ before: specifierNormalized,
350
+ after: addressFinal,
351
+ });
352
+ return addressFinal
353
+ }
354
+ }
355
+
356
+ return null
357
+ };
358
+
359
+ const specifierIsPrefixOf = (specifierHref, href) => {
360
+ return (
361
+ specifierHref[specifierHref.length - 1] === "/" &&
362
+ href.startsWith(specifierHref)
363
+ )
364
+ };
365
+
366
+ const sortImports = (imports) => {
367
+ const mappingsSorted = {};
368
+
369
+ Object.keys(imports)
370
+ .sort(compareLengthOrLocaleCompare)
371
+ .forEach((name) => {
372
+ mappingsSorted[name] = imports[name];
373
+ });
374
+
375
+ return mappingsSorted
376
+ };
377
+
378
+ const sortScopes = (scopes) => {
379
+ const scopesSorted = {};
380
+
381
+ Object.keys(scopes)
382
+ .sort(compareLengthOrLocaleCompare)
383
+ .forEach((scopeSpecifier) => {
384
+ scopesSorted[scopeSpecifier] = sortImports(scopes[scopeSpecifier]);
385
+ });
386
+
387
+ return scopesSorted
388
+ };
389
+
390
+ const compareLengthOrLocaleCompare = (a, b) => {
391
+ return b.length - a.length || a.localeCompare(b)
392
+ };
393
+
394
+ const normalizeImportMap = (importMap, baseUrl) => {
395
+ assertImportMap(importMap);
396
+
397
+ if (!isStringOrUrl(baseUrl)) {
398
+ throw new TypeError(formulateBaseUrlMustBeStringOrUrl({ baseUrl }))
399
+ }
400
+
401
+ const { imports, scopes } = importMap;
402
+
403
+ return {
404
+ imports: imports ? normalizeMappings(imports, baseUrl) : undefined,
405
+ scopes: scopes ? normalizeScopes(scopes, baseUrl) : undefined,
406
+ }
407
+ };
408
+
409
+ const isStringOrUrl = (value) => {
410
+ if (typeof value === "string") {
411
+ return true
412
+ }
413
+
414
+ if (typeof URL === "function" && value instanceof URL) {
415
+ return true
416
+ }
417
+
418
+ return false
419
+ };
420
+
421
+ const normalizeMappings = (mappings, baseUrl) => {
422
+ const mappingsNormalized = {};
423
+
424
+ Object.keys(mappings).forEach((specifier) => {
425
+ const address = mappings[specifier];
426
+
427
+ if (typeof address !== "string") {
428
+ console.warn(
429
+ formulateAddressMustBeAString({
430
+ address,
431
+ specifier,
432
+ }),
433
+ );
434
+ return
435
+ }
436
+
437
+ const specifierResolved = resolveSpecifier(specifier, baseUrl) || specifier;
438
+
439
+ const addressUrl = tryUrlResolution(address, baseUrl);
440
+ if (addressUrl === null) {
441
+ console.warn(
442
+ formulateAdressResolutionFailed({
443
+ address,
444
+ baseUrl,
445
+ specifier,
446
+ }),
447
+ );
448
+ return
449
+ }
450
+
451
+ if (specifier.endsWith("/") && !addressUrl.endsWith("/")) {
452
+ console.warn(
453
+ formulateAddressUrlRequiresTrailingSlash({
454
+ addressUrl,
455
+ address,
456
+ specifier,
457
+ }),
458
+ );
459
+ return
460
+ }
461
+ mappingsNormalized[specifierResolved] = addressUrl;
462
+ });
463
+
464
+ return sortImports(mappingsNormalized)
465
+ };
466
+
467
+ const normalizeScopes = (scopes, baseUrl) => {
468
+ const scopesNormalized = {};
469
+
470
+ Object.keys(scopes).forEach((scopeSpecifier) => {
471
+ const scopeMappings = scopes[scopeSpecifier];
472
+ const scopeUrl = tryUrlResolution(scopeSpecifier, baseUrl);
473
+ if (scopeUrl === null) {
474
+ console.warn(
475
+ formulateScopeResolutionFailed({
476
+ scope: scopeSpecifier,
477
+ baseUrl,
478
+ }),
479
+ );
480
+ return
481
+ }
482
+ const scopeValueNormalized = normalizeMappings(scopeMappings, baseUrl);
483
+ scopesNormalized[scopeUrl] = scopeValueNormalized;
484
+ });
485
+
486
+ return sortScopes(scopesNormalized)
487
+ };
488
+
489
+ const formulateBaseUrlMustBeStringOrUrl = ({
490
+ baseUrl,
491
+ }) => `baseUrl must be a string or an url.
492
+ --- base url ---
493
+ ${baseUrl}`;
494
+
495
+ const formulateAddressMustBeAString = ({
496
+ specifier,
497
+ address,
498
+ }) => `Address must be a string.
499
+ --- address ---
500
+ ${address}
501
+ --- specifier ---
502
+ ${specifier}`;
503
+
504
+ const formulateAdressResolutionFailed = ({
505
+ address,
506
+ baseUrl,
507
+ specifier,
508
+ }) => `Address url resolution failed.
509
+ --- address ---
510
+ ${address}
511
+ --- base url ---
512
+ ${baseUrl}
513
+ --- specifier ---
514
+ ${specifier}`;
515
+
516
+ const formulateAddressUrlRequiresTrailingSlash = ({
517
+ addressURL,
518
+ address,
519
+ specifier,
520
+ }) => `Address must end with /.
521
+ --- address url ---
522
+ ${addressURL}
523
+ --- address ---
524
+ ${address}
525
+ --- specifier ---
526
+ ${specifier}`;
527
+
528
+ const formulateScopeResolutionFailed = ({
529
+ scope,
530
+ baseUrl,
531
+ }) => `Scope url resolution failed.
532
+ --- scope ---
533
+ ${scope}
534
+ --- base url ---
535
+ ${baseUrl}`;
536
+
537
+ const pathnameToExtension = (pathname) => {
538
+ const slashLastIndex = pathname.lastIndexOf("/");
539
+ if (slashLastIndex !== -1) {
540
+ pathname = pathname.slice(slashLastIndex + 1);
541
+ }
542
+
543
+ const dotLastIndex = pathname.lastIndexOf(".");
544
+ if (dotLastIndex === -1) return ""
545
+ // if (dotLastIndex === pathname.length - 1) return ""
546
+ return pathname.slice(dotLastIndex)
547
+ };
548
+
549
+ const resolveImport = ({
550
+ specifier,
551
+ importer,
552
+ importMap,
553
+ defaultExtension = false,
554
+ createBareSpecifierError,
555
+ onImportMapping = () => {},
556
+ }) => {
557
+ let url;
558
+ if (importMap) {
559
+ url = applyImportMap({
560
+ importMap,
561
+ specifier,
562
+ importer,
563
+ createBareSpecifierError,
564
+ onImportMapping,
565
+ });
566
+ } else {
567
+ url = resolveUrl(specifier, importer);
568
+ }
569
+
570
+ if (defaultExtension) {
571
+ url = applyDefaultExtension({ url, importer, defaultExtension });
572
+ }
573
+
574
+ return url
575
+ };
576
+
577
+ const applyDefaultExtension = ({ url, importer, defaultExtension }) => {
578
+ if (urlToPathname(url).endsWith("/")) {
579
+ return url
580
+ }
581
+
582
+ if (typeof defaultExtension === "string") {
583
+ const extension = pathnameToExtension(url);
584
+ if (extension === "") {
585
+ return `${url}${defaultExtension}`
586
+ }
587
+ return url
588
+ }
589
+
590
+ if (defaultExtension === true) {
591
+ const extension = pathnameToExtension(url);
592
+ if (extension === "" && importer) {
593
+ const importerPathname = urlToPathname(importer);
594
+ const importerExtension = pathnameToExtension(importerPathname);
595
+ return `${url}${importerExtension}`
596
+ }
597
+ }
598
+
599
+ return url
600
+ };
601
+
602
+ export { normalizeImportMap, resolveImport };