@jsenv/core 37.1.4 → 38.0.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.
Files changed (29) hide show
  1. package/dist/js/autoreload.js +2 -2
  2. package/dist/jsenv_core.js +3221 -2483
  3. package/package.json +16 -15
  4. package/src/build/build.js +246 -1028
  5. package/src/build/build_specifier_manager.js +1200 -0
  6. package/src/build/build_urls_generator.js +40 -18
  7. package/src/build/version_mappings_injection.js +14 -16
  8. package/src/dev/file_service.js +0 -10
  9. package/src/dev/start_dev_server.js +0 -2
  10. package/src/kitchen/kitchen.js +54 -37
  11. package/src/kitchen/url_graph/references.js +84 -93
  12. package/src/kitchen/url_graph/url_graph.js +16 -6
  13. package/src/kitchen/url_graph/url_info_transformations.js +124 -55
  14. package/src/plugins/autoreload/client/autoreload.js +6 -2
  15. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +20 -16
  16. package/src/plugins/autoreload/jsenv_plugin_hot_search_param.js +1 -1
  17. package/src/plugins/cache_control/jsenv_plugin_cache_control.js +2 -2
  18. package/src/plugins/clean_html/jsenv_plugin_clean_html.js +16 -0
  19. package/src/plugins/importmap/jsenv_plugin_importmap.js +11 -23
  20. package/src/plugins/inlining/jsenv_plugin_inlining_as_data_url.js +16 -1
  21. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +14 -24
  22. package/src/plugins/plugin_controller.js +37 -25
  23. package/src/plugins/plugins.js +2 -0
  24. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +31 -16
  25. package/src/plugins/reference_analysis/directory/jsenv_plugin_directory_reference_analysis.js +12 -6
  26. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +33 -54
  27. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +2 -9
  28. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +15 -8
  29. package/src/build/build_versions_manager.js +0 -492
@@ -0,0 +1,1200 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createDetailedMessage } from "@jsenv/log";
3
+ import { comparePathnames } from "@jsenv/filesystem";
4
+ import { createMagicSource, generateSourcemapFileUrl } from "@jsenv/sourcemap";
5
+ import {
6
+ ensurePathnameTrailingSlash,
7
+ urlToRelativeUrl,
8
+ injectQueryParamIntoSpecifierWithoutEncoding,
9
+ renderUrlOrRelativeUrlFilename,
10
+ } from "@jsenv/urls";
11
+ import {
12
+ parseHtmlString,
13
+ stringifyHtmlAst,
14
+ visitHtmlNodes,
15
+ getHtmlNodeAttribute,
16
+ setHtmlNodeAttributes,
17
+ removeHtmlNode,
18
+ createHtmlNode,
19
+ insertHtmlNodeAfter,
20
+ findHtmlNode,
21
+ } from "@jsenv/ast";
22
+ import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
23
+
24
+ import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
25
+ import { GRAPH_VISITOR } from "../kitchen/url_graph/url_graph_visitor.js";
26
+ import { isWebWorkerUrlInfo } from "../kitchen/web_workers.js";
27
+ import { prependContent } from "../kitchen/prepend_content.js";
28
+ import { createBuildUrlsGenerator } from "./build_urls_generator.js";
29
+ import {
30
+ injectVersionMappingsAsGlobal,
31
+ injectVersionMappingsAsImportmap,
32
+ } from "./version_mappings_injection.js";
33
+
34
+ export const createBuildSpecifierManager = ({
35
+ rawKitchen,
36
+ finalKitchen,
37
+ logger,
38
+ sourceDirectoryUrl,
39
+ buildDirectoryUrl,
40
+ base,
41
+ assetsDirectory,
42
+ length = 8,
43
+
44
+ versioning,
45
+ versioningMethod,
46
+ versionLength,
47
+ canUseImportmap,
48
+ }) => {
49
+ const buildUrlsGenerator = createBuildUrlsGenerator({
50
+ logger,
51
+ sourceDirectoryUrl,
52
+ buildDirectoryUrl,
53
+ assetsDirectory,
54
+ });
55
+ const placeholderAPI = createPlaceholderAPI({
56
+ length,
57
+ });
58
+ const placeholderToReferenceMap = new Map();
59
+ const urlInfoToBuildUrlMap = new Map();
60
+ const buildUrlToUrlInfoMap = new Map();
61
+ const buildUrlToBuildSpecifierMap = new Map();
62
+
63
+ const generateReplacement = (reference) => {
64
+ let buildUrl;
65
+ if (reference.type === "sourcemap_comment") {
66
+ const parentBuildUrl = urlInfoToBuildUrlMap.get(reference.ownerUrlInfo);
67
+ buildUrl = generateSourcemapFileUrl(parentBuildUrl);
68
+ reference.generatedSpecifier = buildUrl;
69
+ } else {
70
+ const url = reference.generatedUrl;
71
+ let urlInfo;
72
+ const rawUrlInfo = rawKitchen.graph.getUrlInfo(reference.url);
73
+ if (rawUrlInfo) {
74
+ urlInfo = rawUrlInfo;
75
+ } else {
76
+ const buildUrlInfo = reference.urlInfo;
77
+ buildUrlInfo.type = reference.expectedType || "asset";
78
+ buildUrlInfo.subtype = reference.expectedSubtype;
79
+ urlInfo = buildUrlInfo;
80
+ }
81
+ buildUrl = buildUrlsGenerator.generate(url, {
82
+ urlInfo,
83
+ ownerUrlInfo: reference.ownerUrlInfo,
84
+ });
85
+ }
86
+
87
+ let buildSpecifier;
88
+ if (base === "./") {
89
+ const parentBuildUrl = reference.ownerUrlInfo.isRoot
90
+ ? buildDirectoryUrl
91
+ : urlInfoToBuildUrlMap.get(reference.ownerUrlInfo);
92
+ const urlRelativeToParent = urlToRelativeUrl(buildUrl, parentBuildUrl);
93
+ if (urlRelativeToParent[0] === ".") {
94
+ buildSpecifier = urlRelativeToParent;
95
+ } else {
96
+ // ensure "./" on relative url (otherwise it could be a "bare specifier")
97
+ buildSpecifier = `./${urlRelativeToParent}`;
98
+ }
99
+ } else {
100
+ const urlRelativeToBuildDirectory = urlToRelativeUrl(
101
+ buildUrl,
102
+ buildDirectoryUrl,
103
+ );
104
+ buildSpecifier = `${base}${urlRelativeToBuildDirectory}`;
105
+ }
106
+
107
+ urlInfoToBuildUrlMap.set(reference.urlInfo, buildUrl);
108
+ buildUrlToUrlInfoMap.set(buildUrl, reference.urlInfo);
109
+ buildUrlToBuildSpecifierMap.set(buildUrl, buildSpecifier);
110
+ const buildGeneratedSpecifier = applyVersioningOnBuildSpecifier(
111
+ buildSpecifier,
112
+ reference,
113
+ );
114
+ return buildGeneratedSpecifier;
115
+ };
116
+ const internalRedirections = new Map();
117
+ const bundleInfoMap = new Map();
118
+
119
+ const applyBundling = async ({ bundler, urlInfosToBundle }) => {
120
+ const urlInfosBundled = await rawKitchen.pluginController.callAsyncHook(
121
+ {
122
+ plugin: bundler.plugin,
123
+ hookName: "bundle",
124
+ value: bundler.bundleFunction,
125
+ },
126
+ urlInfosToBundle,
127
+ );
128
+ Object.keys(urlInfosBundled).forEach((url) => {
129
+ const urlInfoBundled = urlInfosBundled[url];
130
+ if (urlInfoBundled.sourceUrls) {
131
+ urlInfoBundled.sourceUrls.forEach((sourceUrl) => {
132
+ const sourceRawUrlInfo = rawKitchen.graph.getUrlInfo(sourceUrl);
133
+ if (sourceRawUrlInfo) {
134
+ sourceRawUrlInfo.data.bundled = true;
135
+ }
136
+ });
137
+ }
138
+ bundleInfoMap.set(url, urlInfoBundled);
139
+ });
140
+ };
141
+
142
+ const jsenvPluginMoveToBuildDirectory = {
143
+ name: "jsenv:move_to_build_directory",
144
+ appliesDuring: "build",
145
+ // reference resolution is split in 2
146
+ // the redirection to build directory is done in a second phase (redirectReference)
147
+ // to let opportunity to others plugins (js_module_fallback)
148
+ // to mutate reference (inject ?js_module_fallback)
149
+ // before it gets redirected to build directory
150
+ resolveReference: (reference) => {
151
+ const { ownerUrlInfo } = reference;
152
+ if (ownerUrlInfo.remapReference && !reference.isInline) {
153
+ const newSpecifier = ownerUrlInfo.remapReference(reference);
154
+ reference.specifier = newSpecifier;
155
+ }
156
+ const referenceFromPlaceholder = placeholderToReferenceMap.get(
157
+ reference.specifier,
158
+ );
159
+ if (referenceFromPlaceholder) {
160
+ return referenceFromPlaceholder.url;
161
+ }
162
+ if (reference.type === "filesystem") {
163
+ const ownerRawUrl = ensurePathnameTrailingSlash(ownerUrlInfo.url);
164
+ const url = new URL(reference.specifier, ownerRawUrl).href;
165
+ return url;
166
+ }
167
+ if (reference.specifier[0] === "/") {
168
+ const url = new URL(reference.specifier.slice(1), sourceDirectoryUrl)
169
+ .href;
170
+ return url;
171
+ }
172
+ if (reference.injected) {
173
+ // js_module_fallback
174
+ const url = new URL(
175
+ reference.specifier,
176
+ reference.baseUrl || ownerUrlInfo.url,
177
+ ).href;
178
+ return url;
179
+ }
180
+ const parentUrl = reference.baseUrl || ownerUrlInfo.url;
181
+ const url = new URL(reference.specifier, parentUrl).href;
182
+ return url;
183
+ },
184
+ transformReferenceSearchParams: () => {
185
+ // those search params are reflected into the build file name
186
+ // moreover it create cleaner output
187
+ // otherwise output is full of ?js_module_fallback search param
188
+ return {
189
+ js_module_fallback: undefined,
190
+ as_json_module: undefined,
191
+ as_css_module: undefined,
192
+ as_text_module: undefined,
193
+ as_js_module: undefined,
194
+ as_js_classic: undefined,
195
+ cjs_as_js_module: undefined,
196
+ js_classic: undefined, // TODO: add comment to explain who is using this
197
+ entry_point: undefined,
198
+ dynamic_import: undefined,
199
+ };
200
+ },
201
+ formatReference: (reference) => {
202
+ const generatedUrl = reference.generatedUrl;
203
+ if (!generatedUrl.startsWith("file:")) {
204
+ return null;
205
+ }
206
+ if (reference.isWeak) {
207
+ return null;
208
+ }
209
+ if (reference.type === "sourcemap_comment") {
210
+ return null;
211
+ }
212
+ const placeholder = placeholderAPI.generate();
213
+ if (generatedUrl !== reference.url) {
214
+ internalRedirections.set(generatedUrl, reference.url);
215
+ }
216
+ placeholderToReferenceMap.set(placeholder, reference);
217
+ return placeholder;
218
+ },
219
+ fetchUrlContent: async (finalUrlInfo) => {
220
+ let { firstReference } = finalUrlInfo;
221
+ if (
222
+ firstReference.isInline &&
223
+ firstReference.prev &&
224
+ !firstReference.prev.isInline
225
+ ) {
226
+ firstReference = firstReference.prev;
227
+ }
228
+ const rawUrl = firstReference.url;
229
+ const rawUrlInfo = rawKitchen.graph.getUrlInfo(rawUrl);
230
+ const bundleInfo = bundleInfoMap.get(rawUrl);
231
+ if (bundleInfo) {
232
+ if (rawUrlInfo && !finalUrlInfo.filenameHint) {
233
+ finalUrlInfo.filenameHint = rawUrlInfo.filenameHint;
234
+ }
235
+ finalUrlInfo.remapReference = bundleInfo.remapReference;
236
+ return {
237
+ // url: bundleInfo.url,
238
+ originalUrl: bundleInfo.originalUrl,
239
+ type: bundleInfo.type,
240
+ content: bundleInfo.content,
241
+ contentType: bundleInfo.contentType,
242
+ sourcemap: bundleInfo.sourcemap,
243
+ data: bundleInfo.data,
244
+ };
245
+ }
246
+ if (rawUrlInfo) {
247
+ if (rawUrlInfo && !finalUrlInfo.filenameHint) {
248
+ finalUrlInfo.filenameHint = rawUrlInfo.filenameHint;
249
+ }
250
+ return rawUrlInfo;
251
+ }
252
+ // reference injected during "shape":
253
+ // - "js_module_fallback" using getWithoutSearchParam to obtain source
254
+ // url info that will be converted to systemjs/UMD
255
+ // - "js_module_fallback" injecting "s.js"
256
+ if (firstReference.injected) {
257
+ const reference = firstReference.original || firstReference;
258
+ const rawReference = rawKitchen.graph.rootUrlInfo.dependencies.inject({
259
+ type: reference.type,
260
+ expectedType: reference.expectedType,
261
+ specifier: reference.specifier,
262
+ specifierLine: reference.specifierLine,
263
+ specifierColumn: reference.specifierColumn,
264
+ specifierStart: reference.specifierStart,
265
+ specifierEnd: reference.specifierEnd,
266
+ isInline: reference.isInline,
267
+ filenameHint: reference.filenameHint,
268
+ content: reference.content,
269
+ contentType: reference.contentType,
270
+ });
271
+ if (!finalUrlInfo.filenameHint) {
272
+ finalUrlInfo.filenameHint = reference.filenameHint;
273
+ }
274
+ const rawUrlInfo = rawReference.urlInfo;
275
+ await rawUrlInfo.cook();
276
+ return {
277
+ type: rawUrlInfo.type,
278
+ content: rawUrlInfo.content,
279
+ contentType: rawUrlInfo.contentType,
280
+ originalContent: rawUrlInfo.originalContent,
281
+ originalUrl: rawUrlInfo.originalUrl,
282
+ sourcemap: rawUrlInfo.sourcemap,
283
+ };
284
+ }
285
+ if (firstReference.isInline) {
286
+ if (
287
+ firstReference.ownerUrlInfo.url ===
288
+ firstReference.ownerUrlInfo.originalUrl
289
+ ) {
290
+ const rawUrlInfo = GRAPH_VISITOR.find(
291
+ rawKitchen.graph,
292
+ (rawUrlInfoCandidate) => {
293
+ const { inlineUrlSite } = rawUrlInfoCandidate;
294
+ if (!inlineUrlSite) {
295
+ return false;
296
+ }
297
+ if (
298
+ inlineUrlSite.url === firstReference.ownerUrlInfo.url &&
299
+ inlineUrlSite.line === firstReference.specifierLine &&
300
+ inlineUrlSite.column === firstReference.specifierColumn
301
+ ) {
302
+ return true;
303
+ }
304
+ if (rawUrlInfoCandidate.content === firstReference.content) {
305
+ return true;
306
+ }
307
+ if (
308
+ rawUrlInfoCandidate.originalContent === firstReference.content
309
+ ) {
310
+ return true;
311
+ }
312
+ return false;
313
+ },
314
+ );
315
+ if (rawUrlInfo) {
316
+ if (!finalUrlInfo.filenameHint) {
317
+ finalUrlInfo.filenameHint = rawUrlInfo.filenameHint;
318
+ }
319
+ return rawUrlInfo;
320
+ }
321
+ }
322
+ if (!finalUrlInfo.filenameHint) {
323
+ finalUrlInfo.filenameHint = firstReference.filenameHint;
324
+ }
325
+ return {
326
+ originalContent: finalUrlInfo.originalContent,
327
+ content: firstReference.content,
328
+ contentType: firstReference.contentType,
329
+ };
330
+ }
331
+ throw new Error(createDetailedMessage(`Cannot fetch ${rawUrl}`));
332
+ },
333
+ };
334
+
335
+ const buildSpecifierToBuildSpecifierVersionedMap = new Map();
336
+
337
+ const versionMap = new Map();
338
+
339
+ const workerReferenceSet = new Set();
340
+ const referenceVersioningInfoMap = new Map();
341
+ const _getReferenceVersioningInfo = (reference) => {
342
+ if (!shouldApplyVersioningOnReference(reference)) {
343
+ return {
344
+ type: "not_versioned",
345
+ };
346
+ }
347
+ const ownerUrlInfo = reference.ownerUrlInfo;
348
+ if (ownerUrlInfo.jsQuote) {
349
+ // here we use placeholder as specifier, so something like
350
+ // "/other/file.png" becomes "!~{0001}~" and finally "__v__("/other/file.png")"
351
+ // this is to support cases like CSS inlined in JS
352
+ // CSS minifier must see valid CSS specifiers like background-image: url("!~{0001}~");
353
+ // that is finally replaced by invalid css background-image: url("__v__("/other/file.png")")
354
+ return {
355
+ type: "global",
356
+ render: (buildSpecifier) => {
357
+ return placeholderAPI.markAsCode(
358
+ `${ownerUrlInfo.jsQuote}+__v__(${JSON.stringify(buildSpecifier)})+${
359
+ ownerUrlInfo.jsQuote
360
+ }`,
361
+ );
362
+ },
363
+ };
364
+ }
365
+ if (reference.type === "js_url") {
366
+ return {
367
+ type: "global",
368
+ render: (buildSpecifier) => {
369
+ return placeholderAPI.markAsCode(
370
+ `__v__(${JSON.stringify(buildSpecifier)})`,
371
+ );
372
+ },
373
+ };
374
+ }
375
+ if (reference.type === "js_import") {
376
+ if (reference.subtype === "import_dynamic") {
377
+ return {
378
+ type: "global",
379
+ render: (buildSpecifier) => {
380
+ return placeholderAPI.markAsCode(
381
+ `__v__(${JSON.stringify(buildSpecifier)})`,
382
+ );
383
+ },
384
+ };
385
+ }
386
+ if (reference.subtype === "import_meta_resolve") {
387
+ return {
388
+ type: "global",
389
+ render: (buildSpecifier) => {
390
+ return placeholderAPI.markAsCode(
391
+ `__v__(${JSON.stringify(buildSpecifier)})`,
392
+ );
393
+ },
394
+ };
395
+ }
396
+ if (canUseImportmap && !isInsideWorker(reference)) {
397
+ return {
398
+ type: "importmap",
399
+ render: (buildSpecifier) => {
400
+ return buildSpecifier;
401
+ },
402
+ };
403
+ }
404
+ }
405
+ return {
406
+ type: "inline",
407
+ render: (buildSpecifier) => {
408
+ const buildSpecifierVersioned =
409
+ buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier);
410
+ return buildSpecifierVersioned;
411
+ },
412
+ };
413
+ };
414
+ const getReferenceVersioningInfo = (reference) => {
415
+ const infoFromCache = referenceVersioningInfoMap.get(reference);
416
+ if (infoFromCache) {
417
+ return infoFromCache;
418
+ }
419
+ const info = _getReferenceVersioningInfo(reference);
420
+ referenceVersioningInfoMap.set(reference, info);
421
+ return info;
422
+ };
423
+ const isInsideWorker = (reference) => {
424
+ if (workerReferenceSet.has(reference)) {
425
+ return true;
426
+ }
427
+ const referenceOwnerUrllInfo = reference.ownerUrlInfo;
428
+ let is = false;
429
+ if (isWebWorkerUrlInfo(referenceOwnerUrllInfo)) {
430
+ is = true;
431
+ } else {
432
+ GRAPH_VISITOR.findDependent(
433
+ referenceOwnerUrllInfo,
434
+ (dependentUrlInfo) => {
435
+ if (isWebWorkerUrlInfo(dependentUrlInfo)) {
436
+ is = true;
437
+ return true;
438
+ }
439
+ return false;
440
+ },
441
+ );
442
+ }
443
+ if (is) {
444
+ workerReferenceSet.add(reference);
445
+ return true;
446
+ }
447
+ return false;
448
+ };
449
+ const canUseVersionedUrl = (urlInfo) => {
450
+ if (urlInfo.isRoot) {
451
+ return false;
452
+ }
453
+ if (urlInfo.isEntryPoint) {
454
+ // if (urlInfo.subtype === "worker") {
455
+ // return true;
456
+ // }
457
+ return false;
458
+ }
459
+ return urlInfo.type !== "webmanifest";
460
+ };
461
+ const shouldApplyVersioningOnReference = (reference) => {
462
+ if (reference.isInline) {
463
+ return false;
464
+ }
465
+ if (reference.next && reference.next.isInline) {
466
+ return false;
467
+ }
468
+ if (reference.type === "sourcemap_comment") {
469
+ return false;
470
+ }
471
+ // specifier comes from "normalize" hook done a bit earlier in this file
472
+ // we want to get back their build url to access their infos
473
+ const referencedUrlInfo = reference.urlInfo;
474
+ if (!canUseVersionedUrl(referencedUrlInfo)) {
475
+ return false;
476
+ }
477
+ return true;
478
+ };
479
+
480
+ const prepareVersioning = () => {
481
+ const contentOnlyVersionMap = new Map();
482
+ const urlInfoToContainedPlaceholderSetMap = new Map();
483
+ generate_content_only_versions: {
484
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
485
+ finalKitchen.graph.rootUrlInfo,
486
+ (urlInfo) => {
487
+ // ignore:
488
+ // - inline files and data files:
489
+ // they are already taken into account in the file where they appear
490
+ // - ignored files:
491
+ // we don't know their content
492
+ // - unused files without reference
493
+ // File updated such as style.css -> style.css.js or file.js->file.nomodule.js
494
+ // Are used at some point just to be discarded later because they need to be converted
495
+ // There is no need to version them and we could not because the file have been ignored
496
+ // so their content is unknown
497
+ if (urlInfo.type === "sourcemap") {
498
+ return;
499
+ }
500
+ if (urlInfo.isInline) {
501
+ return;
502
+ }
503
+ if (urlInfo.url.startsWith("data:")) {
504
+ // urlInfo became inline and is not referenced by something else
505
+ return;
506
+ }
507
+ if (urlInfo.url.startsWith("ignore:")) {
508
+ return;
509
+ }
510
+ let content = urlInfo.content;
511
+ if (urlInfo.type === "html") {
512
+ content = stringifyHtmlAst(
513
+ parseHtmlString(urlInfo.content, {
514
+ storeOriginalPositions: false,
515
+ }),
516
+ {
517
+ cleanupJsenvAttributes: true,
518
+ cleanupPositionAttributes: true,
519
+ },
520
+ );
521
+ }
522
+ const containedPlaceholderSet = new Set();
523
+ if (mayUsePlaceholder(urlInfo)) {
524
+ const contentWithPredictibleVersionPlaceholders =
525
+ placeholderAPI.replaceWithDefault(content, (placeholder) => {
526
+ containedPlaceholderSet.add(placeholder);
527
+ });
528
+ content = contentWithPredictibleVersionPlaceholders;
529
+ }
530
+ urlInfoToContainedPlaceholderSetMap.set(
531
+ urlInfo,
532
+ containedPlaceholderSet,
533
+ );
534
+ const contentVersion = generateVersion([content], versionLength);
535
+ contentOnlyVersionMap.set(urlInfo, contentVersion);
536
+ },
537
+ );
538
+ }
539
+
540
+ generate_versions: {
541
+ const getSetOfUrlInfoInfluencingVersion = (urlInfo) => {
542
+ const placeholderInfluencingVersionSet = new Set();
543
+ const visitContainedPlaceholders = (urlInfo) => {
544
+ const referencedContentVersion = contentOnlyVersionMap.get(urlInfo);
545
+ if (!referencedContentVersion) {
546
+ // ignored while traversing graph (not used anymore, inline, ...)
547
+ return;
548
+ }
549
+ const containedPlaceholderSet =
550
+ urlInfoToContainedPlaceholderSetMap.get(urlInfo);
551
+ for (const containedPlaceholder of containedPlaceholderSet) {
552
+ if (placeholderInfluencingVersionSet.has(containedPlaceholder)) {
553
+ continue;
554
+ }
555
+ const reference =
556
+ placeholderToReferenceMap.get(containedPlaceholder);
557
+ const referenceVersioningInfo =
558
+ getReferenceVersioningInfo(reference);
559
+ if (
560
+ referenceVersioningInfo.type === "global" ||
561
+ referenceVersioningInfo.type === "importmap"
562
+ ) {
563
+ // when versioning is dynamic no need to take into account
564
+ continue;
565
+ }
566
+ placeholderInfluencingVersionSet.add(containedPlaceholder);
567
+ const referencedUrlInfo = reference.urlInfo;
568
+ visitContainedPlaceholders(referencedUrlInfo);
569
+ }
570
+ };
571
+ visitContainedPlaceholders(urlInfo);
572
+
573
+ const setOfUrlInfluencingVersion = new Set();
574
+ for (const placeholderInfluencingVersion of placeholderInfluencingVersionSet) {
575
+ const reference = placeholderToReferenceMap.get(
576
+ placeholderInfluencingVersion,
577
+ );
578
+ const referencedUrlInfo = reference.urlInfo;
579
+ setOfUrlInfluencingVersion.add(referencedUrlInfo);
580
+ }
581
+ return setOfUrlInfluencingVersion;
582
+ };
583
+ for (const [urlInfo, contentOnlyVersion] of contentOnlyVersionMap) {
584
+ const setOfUrlInfoInfluencingVersion =
585
+ getSetOfUrlInfoInfluencingVersion(urlInfo);
586
+ const versionPartSet = new Set();
587
+ versionPartSet.add(contentOnlyVersion);
588
+ for (const urlInfoInfluencingVersion of setOfUrlInfoInfluencingVersion) {
589
+ const otherUrlInfoContentVersion = contentOnlyVersionMap.get(
590
+ urlInfoInfluencingVersion,
591
+ );
592
+ if (!otherUrlInfoContentVersion) {
593
+ throw new Error(
594
+ `cannot find content version for ${urlInfoInfluencingVersion.url} (used by ${urlInfo.url})`,
595
+ );
596
+ }
597
+ versionPartSet.add(otherUrlInfoContentVersion);
598
+ }
599
+ const version = generateVersion(versionPartSet, versionLength);
600
+ versionMap.set(urlInfo, version);
601
+ }
602
+ }
603
+ };
604
+
605
+ const applyVersioningOnBuildSpecifier = (buildSpecifier, reference) => {
606
+ if (!versioning) {
607
+ return buildSpecifier;
608
+ }
609
+ const referenceVersioningInfo = getReferenceVersioningInfo(reference);
610
+ if (referenceVersioningInfo.type === "not_versioned") {
611
+ return buildSpecifier;
612
+ }
613
+ const version = versionMap.get(reference.urlInfo);
614
+ const buildSpecifierVersioned = injectVersionIntoBuildSpecifier({
615
+ buildSpecifier,
616
+ versioningMethod,
617
+ version,
618
+ });
619
+ buildSpecifierToBuildSpecifierVersionedMap.set(
620
+ buildSpecifier,
621
+ buildSpecifierVersioned,
622
+ );
623
+ return referenceVersioningInfo.render(buildSpecifier);
624
+ };
625
+ const finishVersioning = async () => {
626
+ inject_global_registry_and_importmap: {
627
+ const actions = [];
628
+ const visitors = [];
629
+ const globalMappings = {};
630
+ const importmapMappings = {};
631
+ for (const [reference, versioningInfo] of referenceVersioningInfoMap) {
632
+ if (versioningInfo.type === "global") {
633
+ const urlInfo = reference.urlInfo;
634
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
635
+ const buildSpecifier = buildUrlToBuildSpecifierMap.get(buildUrl);
636
+ const buildSpecifierVersioned =
637
+ buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier);
638
+ globalMappings[buildSpecifier] = buildSpecifierVersioned;
639
+ }
640
+ if (versioningInfo.type === "importmap") {
641
+ const urlInfo = reference.urlInfo;
642
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
643
+ const buildSpecifier = buildUrlToBuildSpecifierMap.get(buildUrl);
644
+ const buildSpecifierVersioned =
645
+ buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier);
646
+ importmapMappings[buildSpecifier] = buildSpecifierVersioned;
647
+ }
648
+ }
649
+ if (Object.keys(globalMappings).length > 0) {
650
+ visitors.push((urlInfo) => {
651
+ if (urlInfo.isEntryPoint) {
652
+ actions.push(async () => {
653
+ await injectVersionMappingsAsGlobal(urlInfo, globalMappings);
654
+ });
655
+ }
656
+ });
657
+ }
658
+ if (Object.keys(importmapMappings).length > 0) {
659
+ visitors.push((urlInfo) => {
660
+ if (urlInfo.type === "html" && urlInfo.isEntryPoint) {
661
+ actions.push(async () => {
662
+ await injectVersionMappingsAsImportmap(
663
+ urlInfo,
664
+ importmapMappings,
665
+ );
666
+ });
667
+ }
668
+ });
669
+ }
670
+ if (visitors.length) {
671
+ GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
672
+ if (urlInfo.isRoot) return;
673
+ visitors.forEach((visitor) => visitor(urlInfo));
674
+ });
675
+ if (actions.length) {
676
+ await Promise.all(actions.map((action) => action()));
677
+ }
678
+ }
679
+ }
680
+ };
681
+
682
+ const getBuildGeneratedSpecifier = (urlInfo) => {
683
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
684
+ const buildSpecifier = buildUrlToBuildSpecifierMap.get(buildUrl);
685
+ const buildGeneratedSpecifier =
686
+ buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier) ||
687
+ buildSpecifier;
688
+ return buildGeneratedSpecifier;
689
+ };
690
+
691
+ return {
692
+ jsenvPluginMoveToBuildDirectory,
693
+ applyBundling,
694
+
695
+ remapPlaceholder: (specifier) => {
696
+ const reference = placeholderToReferenceMap.get(specifier);
697
+ if (reference) {
698
+ return reference.specifier;
699
+ }
700
+ return specifier;
701
+ },
702
+
703
+ replacePlaceholders: async () => {
704
+ if (versioning) {
705
+ prepareVersioning();
706
+ }
707
+
708
+ const urlInfoSet = new Set();
709
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
710
+ finalKitchen.graph.rootUrlInfo,
711
+ (urlInfo) => {
712
+ urlInfoSet.add(urlInfo);
713
+ if (urlInfo.isEntryPoint) {
714
+ generateReplacement(urlInfo.firstReference);
715
+ }
716
+ if (urlInfo.isInline) {
717
+ generateReplacement(urlInfo.firstReference);
718
+ }
719
+ if (urlInfo.type === "sourcemap") {
720
+ generateReplacement(urlInfo.firstReference);
721
+ }
722
+ if (urlInfo.firstReference.type === "side_effect_file") {
723
+ generateReplacement(urlInfo.firstReference);
724
+ }
725
+ // side effect stuff must be generated too
726
+ if (mayUsePlaceholder(urlInfo)) {
727
+ const contentBeforeReplace = urlInfo.content;
728
+ const { content, sourcemap } = placeholderAPI.replaceAll(
729
+ contentBeforeReplace,
730
+ (placeholder) => {
731
+ const reference = placeholderToReferenceMap.get(placeholder);
732
+ return generateReplacement(reference);
733
+ },
734
+ );
735
+ urlInfo.mutateContent({ content, sourcemap });
736
+ }
737
+ },
738
+ );
739
+
740
+ workerReferenceSet.clear();
741
+ if (versioning) {
742
+ await finishVersioning();
743
+ }
744
+
745
+ for (const urlInfo of urlInfoSet) {
746
+ urlInfo.kitchen.urlInfoTransformer.applySourcemapOnContent(
747
+ urlInfo,
748
+ (source) => {
749
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
750
+ if (buildUrl) {
751
+ return urlToRelativeUrl(source, buildUrl);
752
+ }
753
+ return source;
754
+ },
755
+ );
756
+ }
757
+ urlInfoSet.clear();
758
+ },
759
+
760
+ prepareResyncResourceHints: () => {
761
+ const actions = [];
762
+ GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
763
+ if (urlInfo.type !== "html") {
764
+ return;
765
+ }
766
+ const htmlAst = parseHtmlString(urlInfo.content, {
767
+ storeOriginalPositions: false,
768
+ });
769
+ const mutations = [];
770
+ const hintsToInject = [];
771
+ visitHtmlNodes(htmlAst, {
772
+ link: (node) => {
773
+ const href = getHtmlNodeAttribute(node, "href");
774
+ if (href === undefined || href.startsWith("data:")) {
775
+ return;
776
+ }
777
+ const rel = getHtmlNodeAttribute(node, "rel");
778
+ const isResourceHint = [
779
+ "preconnect",
780
+ "dns-prefetch",
781
+ "prefetch",
782
+ "preload",
783
+ "modulepreload",
784
+ ].includes(rel);
785
+ if (!isResourceHint) {
786
+ return;
787
+ }
788
+ const rawUrl = href;
789
+ const finalUrl = internalRedirections.get(rawUrl) || rawUrl;
790
+ const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
791
+ if (!urlInfo) {
792
+ logger.warn(
793
+ `remove resource hint because cannot find "${href}" in the graph`,
794
+ );
795
+ mutations.push(() => {
796
+ removeHtmlNode(node);
797
+ });
798
+ return;
799
+ }
800
+ if (!urlInfo.isUsed()) {
801
+ const rawUrlInfo = rawKitchen.graph.getUrlInfo(rawUrl);
802
+ if (rawUrlInfo && rawUrlInfo.data.bundled) {
803
+ logger.warn(
804
+ `remove resource hint on "${href}" because it was bundled`,
805
+ );
806
+ mutations.push(() => {
807
+ removeHtmlNode(node);
808
+ });
809
+ return;
810
+ }
811
+ logger.warn(
812
+ `remove resource hint on "${href}" because it is not used anymore`,
813
+ );
814
+ mutations.push(() => {
815
+ removeHtmlNode(node);
816
+ });
817
+ return;
818
+ }
819
+ const buildGeneratedSpecifier = getBuildGeneratedSpecifier(urlInfo);
820
+ mutations.push(() => {
821
+ setHtmlNodeAttributes(node, {
822
+ href: buildGeneratedSpecifier,
823
+ ...(urlInfo.type === "js_classic"
824
+ ? { crossorigin: undefined }
825
+ : {}),
826
+ });
827
+ });
828
+ for (const referenceToOther of urlInfo.referenceToOthersSet) {
829
+ if (referenceToOther.isWeak) {
830
+ continue;
831
+ }
832
+ const referencedUrlInfo = referenceToOther.urlInfo;
833
+ if (referencedUrlInfo.data.generatedToShareCode) {
834
+ hintsToInject.push({ urlInfo, node });
835
+ }
836
+ }
837
+ },
838
+ });
839
+ hintsToInject.forEach(({ urlInfo, node }) => {
840
+ const buildGeneratedSpecifier = getBuildGeneratedSpecifier(urlInfo);
841
+ const found = findHtmlNode(htmlAst, (htmlNode) => {
842
+ return (
843
+ htmlNode.nodeName === "link" &&
844
+ getHtmlNodeAttribute(htmlNode, "href") === buildGeneratedSpecifier
845
+ );
846
+ });
847
+ if (!found) {
848
+ mutations.push(() => {
849
+ const nodeToInsert = createHtmlNode({
850
+ tagName: "link",
851
+ href: buildGeneratedSpecifier,
852
+ rel: getHtmlNodeAttribute(node, "rel"),
853
+ as: getHtmlNodeAttribute(node, "as"),
854
+ type: getHtmlNodeAttribute(node, "type"),
855
+ crossorigin: getHtmlNodeAttribute(node, "crossorigin"),
856
+ });
857
+ insertHtmlNodeAfter(nodeToInsert, node);
858
+ });
859
+ }
860
+ });
861
+ if (mutations.length > 0) {
862
+ actions.push(() => {
863
+ mutations.forEach((mutation) => mutation());
864
+ urlInfo.mutateContent({
865
+ content: stringifyHtmlAst(htmlAst),
866
+ });
867
+ });
868
+ }
869
+ });
870
+ if (actions.length === 0) {
871
+ return null;
872
+ }
873
+ return () => {
874
+ actions.map((resourceHintAction) => resourceHintAction());
875
+ };
876
+ },
877
+
878
+ prepareServiceWorkerUrlInjection: () => {
879
+ const serviceWorkerEntryUrlInfos = GRAPH_VISITOR.filter(
880
+ finalKitchen.graph,
881
+ (finalUrlInfo) => {
882
+ return (
883
+ finalUrlInfo.subtype === "service_worker" &&
884
+ finalUrlInfo.isEntryPoint &&
885
+ finalUrlInfo.isUsed()
886
+ );
887
+ },
888
+ );
889
+ if (serviceWorkerEntryUrlInfos.length === 0) {
890
+ return null;
891
+ }
892
+ return async () => {
893
+ const allResourcesFromJsenvBuild = {};
894
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
895
+ finalKitchen.graph.rootUrlInfo,
896
+ (urlInfo) => {
897
+ if (!urlInfo.url.startsWith("file:")) {
898
+ return;
899
+ }
900
+ if (urlInfo.isInline) {
901
+ return;
902
+ }
903
+
904
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
905
+ const buildSpecifier = buildUrlToBuildSpecifierMap.get(buildUrl);
906
+ if (canUseVersionedUrl(urlInfo)) {
907
+ const buildSpecifierVersioned = versioning
908
+ ? buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier)
909
+ : null;
910
+ allResourcesFromJsenvBuild[buildSpecifier] = {
911
+ version: versionMap.get(urlInfo),
912
+ versionedUrl: buildSpecifierVersioned,
913
+ };
914
+ } else {
915
+ // when url is not versioned we compute a "version" for that url anyway
916
+ // so that service worker source still changes and navigator
917
+ // detect there is a change
918
+ allResourcesFromJsenvBuild[buildSpecifier] = {
919
+ version: versionMap.get(urlInfo),
920
+ };
921
+ }
922
+ },
923
+ );
924
+ for (const serviceWorkerEntryUrlInfo of serviceWorkerEntryUrlInfos) {
925
+ const resourcesFromJsenvBuild = {
926
+ ...allResourcesFromJsenvBuild,
927
+ };
928
+ const serviceWorkerBuildUrl = urlInfoToBuildUrlMap.get(
929
+ serviceWorkerEntryUrlInfo,
930
+ );
931
+ const serviceWorkerBuildSpecifier = buildUrlToBuildSpecifierMap.get(
932
+ serviceWorkerBuildUrl,
933
+ );
934
+ delete resourcesFromJsenvBuild[serviceWorkerBuildSpecifier];
935
+ await prependContent(serviceWorkerEntryUrlInfo, {
936
+ type: "js_classic",
937
+ content: `self.resourcesFromJsenvBuild = ${JSON.stringify(
938
+ resourcesFromJsenvBuild,
939
+ null,
940
+ " ",
941
+ )};\n`,
942
+ });
943
+ }
944
+ };
945
+ },
946
+
947
+ getBuildInfo: () => {
948
+ const buildManifest = {};
949
+ const buildContents = {};
950
+ const buildInlineRelativeUrlSet = new Set();
951
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
952
+ finalKitchen.graph.rootUrlInfo,
953
+ (urlInfo) => {
954
+ if (!urlInfo.url.startsWith("file:")) {
955
+ return;
956
+ }
957
+ const buildUrl = urlInfoToBuildUrlMap.get(urlInfo);
958
+ const buildSpecifier = buildUrlToBuildSpecifierMap.get(buildUrl);
959
+ const buildSpecifierVersioned = versioning
960
+ ? buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier)
961
+ : null;
962
+ const buildRelativeUrl = urlToRelativeUrl(
963
+ buildUrl,
964
+ buildDirectoryUrl,
965
+ );
966
+ let contentKey;
967
+ // if to guard for html where versioned build specifier is not generated
968
+ if (buildSpecifierVersioned) {
969
+ const buildUrlVersioned = asBuildUrlVersioned({
970
+ buildSpecifierVersioned,
971
+ buildDirectoryUrl,
972
+ });
973
+ const buildRelativeUrlVersioned = urlToRelativeUrl(
974
+ buildUrlVersioned,
975
+ buildDirectoryUrl,
976
+ );
977
+ buildManifest[buildRelativeUrl] = buildRelativeUrlVersioned;
978
+ contentKey = buildRelativeUrlVersioned;
979
+ } else {
980
+ contentKey = buildRelativeUrl;
981
+ }
982
+ if (urlInfo.type !== "directory") {
983
+ buildContents[contentKey] = urlInfo.content;
984
+ }
985
+ if (urlInfo.isInline) {
986
+ buildInlineRelativeUrlSet.add(buildRelativeUrl);
987
+ }
988
+ },
989
+ );
990
+ const buildFileContents = {};
991
+ const buildInlineContents = {};
992
+ Object.keys(buildContents)
993
+ .sort((a, b) => comparePathnames(a, b))
994
+ .forEach((buildRelativeUrl) => {
995
+ if (buildInlineRelativeUrlSet.has(buildRelativeUrl)) {
996
+ buildInlineContents[buildRelativeUrl] =
997
+ buildContents[buildRelativeUrl];
998
+ } else {
999
+ buildFileContents[buildRelativeUrl] =
1000
+ buildContents[buildRelativeUrl];
1001
+ }
1002
+ });
1003
+
1004
+ return { buildFileContents, buildInlineContents, buildManifest };
1005
+ },
1006
+ };
1007
+ };
1008
+
1009
+ // see https://github.com/rollup/rollup/blob/ce453507ab8457dd1ea3909d8dd7b117b2d14fab/src/utils/hashPlaceholders.ts#L1
1010
+ // see also "New hashing algorithm that "fixes (nearly) everything"
1011
+ // at https://github.com/rollup/rollup/pull/4543
1012
+ const placeholderLeft = "!~{";
1013
+ const placeholderRight = "}~";
1014
+ const placeholderOverhead = placeholderLeft.length + placeholderRight.length;
1015
+
1016
+ const createPlaceholderAPI = ({ length }) => {
1017
+ const chars =
1018
+ "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
1019
+ const toBase64 = (value) => {
1020
+ let outString = "";
1021
+ do {
1022
+ const currentDigit = value % 64;
1023
+ value = (value / 64) | 0;
1024
+ outString = chars[currentDigit] + outString;
1025
+ } while (value !== 0);
1026
+ return outString;
1027
+ };
1028
+
1029
+ let nextIndex = 0;
1030
+ const generate = () => {
1031
+ nextIndex++;
1032
+ const id = toBase64(nextIndex);
1033
+ let placeholder = placeholderLeft;
1034
+ placeholder += id.padStart(length - placeholderOverhead, "0");
1035
+ placeholder += placeholderRight;
1036
+ return placeholder;
1037
+ };
1038
+
1039
+ const replaceFirst = (code, value) => {
1040
+ let replaced = false;
1041
+ return code.replace(PLACEHOLDER_REGEX, (match) => {
1042
+ if (replaced) return match;
1043
+ replaced = true;
1044
+ return value;
1045
+ });
1046
+ };
1047
+
1048
+ const extractFirst = (string) => {
1049
+ const match = string.match(PLACEHOLDER_REGEX);
1050
+ return match ? match[0] : null;
1051
+ };
1052
+
1053
+ const defaultPlaceholder = `${placeholderLeft}${"0".repeat(
1054
+ length - placeholderOverhead,
1055
+ )}${placeholderRight}`;
1056
+ const replaceWithDefault = (code, onPlaceholder) => {
1057
+ const transformedCode = code.replace(PLACEHOLDER_REGEX, (placeholder) => {
1058
+ onPlaceholder(placeholder);
1059
+ return defaultPlaceholder;
1060
+ });
1061
+ return transformedCode;
1062
+ };
1063
+
1064
+ const PLACEHOLDER_REGEX = new RegExp(
1065
+ `${escapeRegexpSpecialChars(placeholderLeft)}[0-9a-zA-Z_$]{1,${
1066
+ length - placeholderOverhead
1067
+ }}${escapeRegexpSpecialChars(placeholderRight)}`,
1068
+ "g",
1069
+ );
1070
+
1071
+ const markAsCode = (string) => {
1072
+ return {
1073
+ __isCode__: true,
1074
+ toString: () => string,
1075
+ value: string,
1076
+ };
1077
+ };
1078
+
1079
+ const replaceAll = (string, replacer) => {
1080
+ const magicSource = createMagicSource(string);
1081
+
1082
+ string.replace(PLACEHOLDER_REGEX, (placeholder, index) => {
1083
+ const replacement = replacer(placeholder, index);
1084
+ if (!replacement) {
1085
+ return;
1086
+ }
1087
+ let value;
1088
+ let isCode = false;
1089
+ if (replacement && replacement.__isCode__) {
1090
+ value = replacement.value;
1091
+ isCode = true;
1092
+ } else {
1093
+ value = replacement;
1094
+ }
1095
+
1096
+ let start = index;
1097
+ let end = start + placeholder.length;
1098
+ if (
1099
+ isCode &&
1100
+ // when specifier is wrapper by quotes
1101
+ // we remove the quotes to transform the string
1102
+ // into code that will be executed
1103
+ isWrappedByQuote(string, start, end)
1104
+ ) {
1105
+ start = start - 1;
1106
+ end = end + 1;
1107
+ }
1108
+ magicSource.replace({
1109
+ start,
1110
+ end,
1111
+ replacement: value,
1112
+ });
1113
+ });
1114
+ return magicSource.toContentAndSourcemap();
1115
+ };
1116
+
1117
+ return {
1118
+ generate,
1119
+ replaceFirst,
1120
+ replaceAll,
1121
+ extractFirst,
1122
+ markAsCode,
1123
+ replaceWithDefault,
1124
+ };
1125
+ };
1126
+
1127
+ const mayUsePlaceholder = (urlInfo) => {
1128
+ if (urlInfo.referenceToOthersSet.size === 0) {
1129
+ return false;
1130
+ }
1131
+ if (!CONTENT_TYPE.isTextual(urlInfo.contentType)) {
1132
+ return false;
1133
+ }
1134
+ return true;
1135
+ };
1136
+
1137
+ const isWrappedByQuote = (content, start, end) => {
1138
+ const previousChar = content[start - 1];
1139
+ const nextChar = content[end];
1140
+ if (previousChar === `'` && nextChar === `'`) {
1141
+ return true;
1142
+ }
1143
+ if (previousChar === `"` && nextChar === `"`) {
1144
+ return true;
1145
+ }
1146
+ if (previousChar === "`" && nextChar === "`") {
1147
+ return true;
1148
+ }
1149
+ return false;
1150
+ };
1151
+
1152
+ // https://github.com/rollup/rollup/blob/19e50af3099c2f627451a45a84e2fa90d20246d5/src/utils/FileEmitter.ts#L47
1153
+ // https://github.com/rollup/rollup/blob/5a5391971d695c808eed0c5d7d2c6ccb594fc689/src/Chunk.ts#L870
1154
+ const generateVersion = (parts, length) => {
1155
+ const hash = createHash("sha256");
1156
+ parts.forEach((part) => {
1157
+ hash.update(part);
1158
+ });
1159
+ return hash.digest("hex").slice(0, length);
1160
+ };
1161
+
1162
+ const injectVersionIntoBuildSpecifier = ({
1163
+ buildSpecifier,
1164
+ version,
1165
+ versioningMethod,
1166
+ }) => {
1167
+ if (versioningMethod === "search_param") {
1168
+ return injectQueryParamIntoSpecifierWithoutEncoding(
1169
+ buildSpecifier,
1170
+ "v",
1171
+ version,
1172
+ );
1173
+ }
1174
+ return renderUrlOrRelativeUrlFilename(
1175
+ buildSpecifier,
1176
+ ({ basename, extension }) => {
1177
+ return `${basename}-${version}${extension}`;
1178
+ },
1179
+ );
1180
+ };
1181
+
1182
+ const asBuildUrlVersioned = ({
1183
+ buildSpecifierVersioned,
1184
+ buildDirectoryUrl,
1185
+ }) => {
1186
+ if (buildSpecifierVersioned[0] === "/") {
1187
+ return new URL(buildSpecifierVersioned.slice(1), buildDirectoryUrl).href;
1188
+ }
1189
+ const buildUrl = new URL(buildSpecifierVersioned, buildDirectoryUrl).href;
1190
+ if (buildUrl.startsWith(buildDirectoryUrl)) {
1191
+ return buildUrl;
1192
+ }
1193
+ // it's likely "base" parameter was set to an url origin like "https://cdn.example.com"
1194
+ // let's move url to build directory
1195
+ const { pathname, search, hash } = new URL(buildSpecifierVersioned);
1196
+ return `${buildDirectoryUrl}${pathname}${search}${hash}`;
1197
+ };
1198
+
1199
+ // export for unit tests
1200
+ export { generateVersion };