@jsenv/core 36.3.1 → 37.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 (64) hide show
  1. package/dist/js/autoreload.js +6 -5
  2. package/dist/js/import_meta_hot.js +4 -4
  3. package/dist/js/server_events_client.js +422 -304
  4. package/dist/jsenv_core.js +3819 -3256
  5. package/package.json +16 -16
  6. package/src/build/build.js +342 -658
  7. package/src/build/build_urls_generator.js +8 -8
  8. package/src/build/build_versions_manager.js +495 -0
  9. package/src/build/version_mappings_injection.js +27 -16
  10. package/src/dev/file_service.js +80 -91
  11. package/src/dev/start_dev_server.js +5 -3
  12. package/src/kitchen/errors.js +16 -16
  13. package/src/kitchen/fetched_content_compliance.js +4 -8
  14. package/src/kitchen/kitchen.js +367 -939
  15. package/src/kitchen/prepend_content.js +13 -35
  16. package/src/kitchen/url_graph/references.js +713 -0
  17. package/src/kitchen/url_graph/sort_by_dependencies.js +2 -2
  18. package/src/kitchen/url_graph/url_content.js +96 -0
  19. package/src/kitchen/url_graph/url_graph.js +439 -0
  20. package/src/kitchen/url_graph/url_graph_report.js +6 -4
  21. package/src/kitchen/url_graph/url_graph_visitor.js +14 -12
  22. package/src/kitchen/url_graph/url_info_transformations.js +180 -184
  23. package/src/plugins/autoreload/client/autoreload.js +1 -0
  24. package/src/plugins/autoreload/client/reload.js +6 -6
  25. package/src/plugins/autoreload/jsenv_plugin_autoreload.js +2 -2
  26. package/src/plugins/autoreload/jsenv_plugin_autoreload_client.js +2 -2
  27. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +84 -78
  28. package/src/plugins/autoreload/jsenv_plugin_hot_search_param.js +52 -0
  29. package/src/plugins/cache_control/jsenv_plugin_cache_control.js +1 -1
  30. package/src/plugins/commonjs_globals/jsenv_plugin_commonjs_globals.js +2 -2
  31. package/src/plugins/global_scenarios/jsenv_plugin_global_scenarios.js +3 -3
  32. package/src/plugins/import_meta_hot/client/import_meta_hot.js +4 -4
  33. package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +18 -20
  34. package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +2 -2
  35. package/src/plugins/importmap/jsenv_plugin_importmap.js +35 -37
  36. package/src/plugins/inlining/jsenv_plugin_inlining.js +1 -17
  37. package/src/plugins/inlining/jsenv_plugin_inlining_as_data_url.js +70 -50
  38. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +72 -54
  39. package/src/plugins/plugin_controller.js +92 -27
  40. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +18 -20
  41. package/src/plugins/reference_analysis/css/jsenv_plugin_css_reference_analysis.js +4 -5
  42. package/src/plugins/reference_analysis/data_urls/jsenv_plugin_data_urls_analysis.js +18 -16
  43. package/src/plugins/reference_analysis/directory/jsenv_plugin_directory_reference_analysis.js +13 -20
  44. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +55 -72
  45. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +33 -42
  46. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +16 -7
  47. package/src/plugins/reference_analysis/webmanifest/jsenv_plugin_webmanifest_reference_analysis.js +4 -3
  48. package/src/plugins/resolution_node_esm/jsenv_plugin_node_esm_resolution.js +16 -6
  49. package/src/plugins/resolution_node_esm/node_esm_resolver.js +30 -24
  50. package/src/plugins/resolution_web/jsenv_plugin_web_resolution.js +8 -5
  51. package/src/plugins/ribbon/jsenv_plugin_ribbon.js +3 -3
  52. package/src/plugins/server_events/client/server_events_client.js +460 -15
  53. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +13 -29
  54. package/src/plugins/version_search_param/jsenv_plugin_version_search_param.js +1 -1
  55. package/src/build/version_generator.js +0 -19
  56. package/src/kitchen/url_graph/url_graph_loader.js +0 -77
  57. package/src/kitchen/url_graph.js +0 -322
  58. package/src/plugins/autoreload/jsenv_plugin_hmr.js +0 -42
  59. package/src/plugins/resolution_node_esm/url_type_from_reference.js +0 -13
  60. package/src/plugins/server_events/client/connection_manager.js +0 -170
  61. package/src/plugins/server_events/client/event_source_connection.js +0 -83
  62. package/src/plugins/server_events/client/events_manager.js +0 -75
  63. package/src/plugins/server_events/client/web_socket_connection.js +0 -81
  64. /package/src/kitchen/{url_specifier_encoding.js → url_graph/url_specifier_encoding.js} +0 -0
@@ -17,12 +17,12 @@ export const createBuildUrlsGenerator = ({
17
17
  return urlToFilename(url);
18
18
  };
19
19
 
20
- const generate = memoizeByFirstArgument((url, { urlInfo, parentUrlInfo }) => {
20
+ const generate = memoizeByFirstArgument((url, { urlInfo, ownerUrlInfo }) => {
21
21
  const directoryPath = determineDirectoryPath({
22
22
  buildDirectoryUrl,
23
23
  assetsDirectory,
24
24
  urlInfo,
25
- parentUrlInfo,
25
+ ownerUrlInfo,
26
26
  });
27
27
  let names = cache[directoryPath];
28
28
  if (!names) {
@@ -77,23 +77,23 @@ const determineDirectoryPath = ({
77
77
  buildDirectoryUrl,
78
78
  assetsDirectory,
79
79
  urlInfo,
80
- parentUrlInfo,
80
+ ownerUrlInfo,
81
81
  }) => {
82
82
  if (urlInfo.type === "directory") {
83
83
  return "";
84
84
  }
85
- if (parentUrlInfo && parentUrlInfo.type === "directory") {
86
- const parentDirectoryPath = urlToRelativeUrl(
87
- parentUrlInfo.url,
85
+ if (ownerUrlInfo && ownerUrlInfo.type === "directory") {
86
+ const ownerDirectoryPath = urlToRelativeUrl(
87
+ ownerUrlInfo.url,
88
88
  buildDirectoryUrl,
89
89
  );
90
- return parentDirectoryPath;
90
+ return ownerDirectoryPath;
91
91
  }
92
92
  if (urlInfo.isInline) {
93
93
  const parentDirectoryPath = determineDirectoryPath({
94
94
  buildDirectoryUrl,
95
95
  assetsDirectory,
96
- urlInfo: parentUrlInfo,
96
+ urlInfo: ownerUrlInfo,
97
97
  });
98
98
  return parentDirectoryPath;
99
99
  }
@@ -0,0 +1,495 @@
1
+ // see https://github.com/rollup/rollup/blob/ce453507ab8457dd1ea3909d8dd7b117b2d14fab/src/utils/hashPlaceholders.ts#L1
2
+
3
+ import { createHash } from "node:crypto";
4
+ import {
5
+ injectQueryParamIntoSpecifierWithoutEncoding,
6
+ renderUrlOrRelativeUrlFilename,
7
+ } from "@jsenv/urls";
8
+ import { parseHtmlString, stringifyHtmlAst } from "@jsenv/ast";
9
+ import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
10
+ import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
11
+ import { GRAPH_VISITOR } from "../kitchen/url_graph/url_graph_visitor.js";
12
+ import { isWebWorkerUrlInfo } from "../kitchen/web_workers.js";
13
+ import {
14
+ injectVersionMappingsAsGlobal,
15
+ injectVersionMappingsAsImportmap,
16
+ } from "./version_mappings_injection.js";
17
+
18
+ const placeholderLeft = "!~{";
19
+ const placeholderRight = "}~";
20
+ const placeholderOverhead = placeholderLeft.length + placeholderRight.length;
21
+
22
+ export const createBuildVersionsManager = ({
23
+ finalKitchen,
24
+ versioningMethod,
25
+ versionLength = 8,
26
+ canUseImportmap,
27
+ getBuildUrlFromBuildSpecifier,
28
+ }) => {
29
+ const workerReferenceSet = new Set();
30
+ const isReferencedByWorker = (reference) => {
31
+ if (workerReferenceSet.has(reference)) {
32
+ return true;
33
+ }
34
+ const referencedUrlInfo = reference.urlInfo;
35
+ const dependentWorker = GRAPH_VISITOR.findDependent(
36
+ referencedUrlInfo,
37
+ (dependentUrlInfo) => {
38
+ return isWebWorkerUrlInfo(dependentUrlInfo);
39
+ },
40
+ );
41
+ if (dependentWorker) {
42
+ workerReferenceSet.add(reference);
43
+ return true;
44
+ }
45
+ return Boolean(dependentWorker);
46
+ };
47
+
48
+ const chars =
49
+ "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
50
+ const base = 64;
51
+ const toBase64 = (value) => {
52
+ let outString = "";
53
+ do {
54
+ const currentDigit = value % base;
55
+ value = (value / base) | 0;
56
+ outString = chars[currentDigit] + outString;
57
+ } while (value !== 0);
58
+ return outString;
59
+ };
60
+
61
+ let nextIndex = 0;
62
+ const generatePlaceholder = () => {
63
+ nextIndex++;
64
+ const id = toBase64(nextIndex);
65
+ let placeholder = placeholderLeft;
66
+ placeholder += id.padStart(versionLength - placeholderOverhead, "0");
67
+ placeholder += placeholderRight;
68
+ return placeholder;
69
+ };
70
+
71
+ const replaceFirstPlaceholder = (code, value) => {
72
+ let replaced = false;
73
+ return code.replace(REPLACER_REGEX, (match) => {
74
+ if (replaced) return match;
75
+ replaced = true;
76
+ return value;
77
+ });
78
+ };
79
+
80
+ const defaultPlaceholder = `${placeholderLeft}${"0".repeat(
81
+ versionLength - placeholderOverhead,
82
+ )}${placeholderRight}`;
83
+ const replaceWithDefaultAndPopulateContainedPlaceholders = (
84
+ code,
85
+ containedPlaceholders,
86
+ ) => {
87
+ const transformedCode = code.replace(REPLACER_REGEX, (placeholder) => {
88
+ containedPlaceholders.add(placeholder);
89
+ return defaultPlaceholder;
90
+ });
91
+ return transformedCode;
92
+ };
93
+
94
+ const REPLACER_REGEX = new RegExp(
95
+ `${escapeRegexpSpecialChars(placeholderLeft)}[0-9a-zA-Z_$]{1,${
96
+ versionLength - placeholderOverhead
97
+ }}${escapeRegexpSpecialChars(placeholderRight)}`,
98
+ "g",
99
+ );
100
+
101
+ const placeholderToReferenceMap = new Map();
102
+ const buildSpecifierToPlaceholderMap = new Map();
103
+
104
+ const referenceVersionedByCodeMap = new Map();
105
+ const referenceVersionedByImportmapMap = new Map();
106
+ const specifierVersionedInlineSet = new Set();
107
+ const specifierVersionedByCodeSet = new Set();
108
+ const specifierVersionedByImportmapSet = new Set();
109
+ const versionMap = new Map();
110
+
111
+ const buildSpecifierToBuildSpecifierVersionedMap = new Map();
112
+ // - will be used by global and importmap registry
113
+ // - will be used by build during "inject_urls_in_service_workers" and
114
+ // "resync_resource_hints"
115
+ const getBuildSpecifierVersioned = (buildSpecifier) => {
116
+ const fromCache =
117
+ buildSpecifierToBuildSpecifierVersionedMap.get(buildSpecifier);
118
+ if (fromCache) {
119
+ return fromCache;
120
+ }
121
+ const buildSpecifierPlaceholder =
122
+ buildSpecifierToPlaceholderMap.get(buildSpecifier);
123
+ if (!buildSpecifierPlaceholder) {
124
+ return null;
125
+ }
126
+ const buildUrl = getBuildUrlFromBuildSpecifier(buildSpecifier);
127
+ const urlInfo = finalKitchen.graph.getUrlInfo(buildUrl);
128
+ const version = versionMap.get(urlInfo);
129
+ const buildSpecifierVersioned = replaceFirstPlaceholder(
130
+ buildSpecifierPlaceholder,
131
+ version,
132
+ );
133
+ buildSpecifierToBuildSpecifierVersionedMap.set(
134
+ buildSpecifier,
135
+ buildSpecifierVersioned,
136
+ );
137
+ return buildSpecifierVersioned;
138
+ };
139
+
140
+ return {
141
+ generateBuildSpecifierPlaceholder: (reference, buildSpecifier) => {
142
+ const placeholder = generatePlaceholder();
143
+ const buildSpecifierWithVersionPlaceholder =
144
+ injectVersionPlaceholderIntoBuildSpecifier({
145
+ buildSpecifier,
146
+ versionPlaceholder: placeholder,
147
+ versioningMethod,
148
+ });
149
+ buildSpecifierToPlaceholderMap.set(
150
+ buildSpecifier,
151
+ buildSpecifierWithVersionPlaceholder,
152
+ );
153
+ placeholderToReferenceMap.set(placeholder, reference);
154
+
155
+ const asPlaceholderForVersionedSpecifier = () => {
156
+ specifierVersionedInlineSet.add(buildSpecifier);
157
+ return buildSpecifierWithVersionPlaceholder;
158
+ };
159
+
160
+ const asPlaceholderForCodeVersionedByGlobalRegistry = (codeToInject) => {
161
+ // here we use placeholder as specifier, so something like
162
+ // "/other/file.png" becomes "!~{0001}~" and finally "__v__("/other/file.png")"
163
+ // this is to support cases like CSS inlined in JS
164
+ // CSS minifier must see valid CSS specifiers like background-image: url("!~{0001}~");
165
+ // that is finally replaced by invalid css background-image: url("__v__("/other/file.png")")
166
+ specifierVersionedByCodeSet.add(buildSpecifier);
167
+ referenceVersionedByCodeMap.set(reference, codeToInject);
168
+ return placeholder;
169
+ };
170
+
171
+ const asPlaceholderForSpecifierVersionedByImportmap = (
172
+ specifierToUse,
173
+ ) => {
174
+ specifierVersionedByImportmapSet.add(buildSpecifier);
175
+ referenceVersionedByImportmapMap.set(reference, specifierToUse);
176
+ return buildSpecifier;
177
+ };
178
+
179
+ const ownerUrlInfo = reference.ownerUrlInfo;
180
+ if (ownerUrlInfo.jsQuote) {
181
+ return asPlaceholderForCodeVersionedByGlobalRegistry(
182
+ `${ownerUrlInfo.jsQuote}+__v__(${JSON.stringify(buildSpecifier)})+${
183
+ ownerUrlInfo.jsQuote
184
+ }`,
185
+ );
186
+ }
187
+ if (reference.type === "js_url") {
188
+ return asPlaceholderForCodeVersionedByGlobalRegistry(
189
+ `__v__(${JSON.stringify(buildSpecifier)})`,
190
+ );
191
+ }
192
+ if (reference.type === "js_import") {
193
+ if (reference.subtype === "import_dynamic") {
194
+ return asPlaceholderForCodeVersionedByGlobalRegistry(
195
+ `__v__(${JSON.stringify(buildSpecifier)})`,
196
+ );
197
+ }
198
+ if (reference.subtype === "import_meta_resolve") {
199
+ return asPlaceholderForCodeVersionedByGlobalRegistry(
200
+ `__v__(${JSON.stringify(buildSpecifier)})`,
201
+ );
202
+ }
203
+ if (canUseImportmap && !isReferencedByWorker(reference)) {
204
+ return asPlaceholderForSpecifierVersionedByImportmap(
205
+ JSON.stringify(buildSpecifier),
206
+ );
207
+ }
208
+ }
209
+ return asPlaceholderForVersionedSpecifier();
210
+ },
211
+ applyVersioning: async (finalKitchen) => {
212
+ workerReferenceSet.clear();
213
+
214
+ const contentOnlyVersionMap = new Map();
215
+ generate_content_only_versions: {
216
+ GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
217
+ // ignore:
218
+ // - inline files and data files:
219
+ // they are already taken into account in the file where they appear
220
+ // - ignored files:
221
+ // we don't know their content
222
+ // - unused files without reference
223
+ // File updated such as style.css -> style.css.js or file.js->file.nomodule.js
224
+ // Are used at some point just to be discarded later because they need to be converted
225
+ // There is no need to version them and we could not because the file have been ignored
226
+ // so their content is unknown
227
+ if (urlInfo.isRoot) {
228
+ return;
229
+ }
230
+ if (urlInfo.type === "sourcemap") {
231
+ return;
232
+ }
233
+ if (urlInfo.isInline) {
234
+ return;
235
+ }
236
+ if (urlInfo.url.startsWith("data:")) {
237
+ // urlInfo became inline and is not referenced by something else
238
+ return;
239
+ }
240
+ if (urlInfo.url.startsWith("ignore:")) {
241
+ return;
242
+ }
243
+ if (!urlInfo.isUsed()) {
244
+ return;
245
+ }
246
+ let content = urlInfo.content;
247
+ if (urlInfo.type === "html") {
248
+ content = stringifyHtmlAst(
249
+ parseHtmlString(urlInfo.content, {
250
+ storeOriginalPositions: false,
251
+ }),
252
+ {
253
+ cleanupJsenvAttributes: true,
254
+ cleanupPositionAttributes: true,
255
+ },
256
+ );
257
+ }
258
+ if (
259
+ CONTENT_TYPE.isTextual(urlInfo.contentType) &&
260
+ urlInfo.referenceToOthersSet.size > 0
261
+ ) {
262
+ const containedPlaceholders = new Set();
263
+ const contentWithPredictibleVersionPlaceholders =
264
+ replaceWithDefaultAndPopulateContainedPlaceholders(
265
+ content,
266
+ containedPlaceholders,
267
+ );
268
+ content = contentWithPredictibleVersionPlaceholders;
269
+ }
270
+ const contentVersion = generateVersion([content], versionLength);
271
+ contentOnlyVersionMap.set(urlInfo, contentVersion);
272
+ });
273
+ }
274
+
275
+ generate_versions: {
276
+ const getSetOfUrlInfoInfluencingVersion = (urlInfo) => {
277
+ // there is no need to take into account dependencies
278
+ // when the reference can reference them without version
279
+ // (importmap or window.__v__)
280
+ const setOfUrlInfluencingVersion = new Set();
281
+ const visitDependencies = (urlInfo) => {
282
+ urlInfo.referenceToOthersSet.forEach((referenceToOther) => {
283
+ if (
284
+ referenceVersionedByCodeMap.has(referenceToOther) ||
285
+ referenceVersionedByImportmapMap.has(referenceToOther)
286
+ ) {
287
+ // when versioning is dynamic no need to take into account
288
+ // happens for:
289
+ // - specifier mapped by window.__v__()
290
+ // - specifier mapped by importmap
291
+ return;
292
+ }
293
+ const referencedUrlInfo = referenceToOther.urlInfo;
294
+ const referencedContentVersion =
295
+ contentOnlyVersionMap.get(referencedUrlInfo);
296
+ if (!referencedContentVersion) {
297
+ // ignored while traversing graph (not used anymore, inline, ...)
298
+ return;
299
+ }
300
+ if (setOfUrlInfluencingVersion.has(referencedUrlInfo)) {
301
+ // handle circular deps
302
+ return;
303
+ }
304
+ setOfUrlInfluencingVersion.add(referencedUrlInfo);
305
+ visitDependencies(referencedUrlInfo);
306
+ });
307
+ };
308
+ visitDependencies(urlInfo);
309
+ return setOfUrlInfluencingVersion;
310
+ };
311
+
312
+ contentOnlyVersionMap.forEach((contentOnlyVersion, urlInfo) => {
313
+ const setOfUrlInfoInfluencingVersion =
314
+ getSetOfUrlInfoInfluencingVersion(urlInfo);
315
+ const versionPartSet = new Set();
316
+ versionPartSet.add(contentOnlyVersion);
317
+ setOfUrlInfoInfluencingVersion.forEach(
318
+ (urlInfoInfluencingVersion) => {
319
+ const otherUrlInfoContentVersion = contentOnlyVersionMap.get(
320
+ urlInfoInfluencingVersion,
321
+ );
322
+ if (!otherUrlInfoContentVersion) {
323
+ throw new Error(
324
+ `cannot find content version for ${urlInfoInfluencingVersion.url} (used by ${urlInfo.url})`,
325
+ );
326
+ }
327
+ versionPartSet.add(otherUrlInfoContentVersion);
328
+ },
329
+ );
330
+ const version = generateVersion(versionPartSet, versionLength);
331
+ versionMap.set(urlInfo, version);
332
+ });
333
+ }
334
+
335
+ replace_version_placeholders: {
336
+ // now replace all placeholders in urlInfos with the real versions
337
+ versionMap.forEach((version, urlInfo) => {
338
+ if (!CONTENT_TYPE.isTextual(urlInfo.contentType)) return;
339
+ if (urlInfo.referenceToOthersSet.size === 0) return;
340
+
341
+ let replacements = [];
342
+ let content = urlInfo.content;
343
+ content.replace(REPLACER_REGEX, (placeholder, index) => {
344
+ const replacement = {
345
+ start: index,
346
+ placeholder,
347
+ value: null,
348
+ valueType: "",
349
+ };
350
+ replacements.push(replacement);
351
+ const reference = placeholderToReferenceMap.get(placeholder);
352
+
353
+ const codeToInject = referenceVersionedByCodeMap.get(reference);
354
+ if (codeToInject) {
355
+ replacement.value = codeToInject;
356
+ replacement.valueType = "code";
357
+ return;
358
+ }
359
+ const specifierToUse =
360
+ referenceVersionedByImportmapMap.get(reference);
361
+ if (specifierToUse) {
362
+ replacement.value = specifierToUse;
363
+ replacement.valueType = "specifier";
364
+ return;
365
+ }
366
+ const version = versionMap.get(reference.urlInfo);
367
+ replacement.value = version;
368
+ replacement.valueType = "specifier";
369
+ });
370
+
371
+ let diff = 0;
372
+ replacements.forEach((replacement) => {
373
+ const placeholder = replacement.placeholder;
374
+ const value = replacement.value;
375
+ let start = replacement.start + diff;
376
+ let end = start + placeholder.length;
377
+ if (
378
+ replacement.valueType === "code" &&
379
+ // when specifier is wrapper by quotes
380
+ // we remove the quotes to transform the string
381
+ // into code that will be executed
382
+ isWrappedByQuote(content, start, end)
383
+ ) {
384
+ start = start - 1;
385
+ end = end + 1;
386
+ diff = diff - 2;
387
+ }
388
+ const before = content.slice(0, start);
389
+ const after = content.slice(end);
390
+ content = before + value + after;
391
+ const charAdded = value.length - placeholder.length;
392
+ diff += charAdded;
393
+ });
394
+ urlInfo.content = content;
395
+ });
396
+ }
397
+
398
+ inject_global_registry_and_importmap: {
399
+ const actions = [];
400
+ const visitors = [];
401
+ if (specifierVersionedByCodeSet.size) {
402
+ const versionMappingsNeeded = {};
403
+ specifierVersionedByCodeSet.forEach((buildSpecifier) => {
404
+ versionMappingsNeeded[buildSpecifier] =
405
+ getBuildSpecifierVersioned(buildSpecifier);
406
+ });
407
+ visitors.push((urlInfo) => {
408
+ if (urlInfo.isEntryPoint) {
409
+ actions.push(async () => {
410
+ await injectVersionMappingsAsGlobal(
411
+ urlInfo,
412
+ versionMappingsNeeded,
413
+ );
414
+ });
415
+ }
416
+ });
417
+ }
418
+ if (specifierVersionedByImportmapSet.size) {
419
+ const versionMappingsNeeded = {};
420
+ specifierVersionedByImportmapSet.forEach((buildSpecifier) => {
421
+ versionMappingsNeeded[buildSpecifier] =
422
+ getBuildSpecifierVersioned(buildSpecifier);
423
+ });
424
+ visitors.push((urlInfo) => {
425
+ if (urlInfo.type === "html" && urlInfo.isEntryPoint) {
426
+ actions.push(async () => {
427
+ await injectVersionMappingsAsImportmap(
428
+ urlInfo,
429
+ versionMappingsNeeded,
430
+ );
431
+ });
432
+ }
433
+ });
434
+ }
435
+
436
+ if (visitors.length) {
437
+ GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
438
+ if (urlInfo.isRoot) return;
439
+ visitors.forEach((visitor) => visitor(urlInfo));
440
+ });
441
+ if (actions.length) {
442
+ await Promise.all(actions.map((action) => action()));
443
+ }
444
+ }
445
+ }
446
+ },
447
+ getVersion: (urlInfo) => versionMap.get(urlInfo),
448
+ getBuildSpecifierVersioned,
449
+ };
450
+ };
451
+
452
+ const isWrappedByQuote = (content, start, end) => {
453
+ const previousChar = content[start - 1];
454
+ const nextChar = content[end];
455
+ if (previousChar === `'` && nextChar === `'`) {
456
+ return true;
457
+ }
458
+ if (previousChar === `"` && nextChar === `"`) {
459
+ return true;
460
+ }
461
+ if (previousChar === "`" && nextChar === "`") {
462
+ return true;
463
+ }
464
+ return false;
465
+ };
466
+
467
+ // https://github.com/rollup/rollup/blob/19e50af3099c2f627451a45a84e2fa90d20246d5/src/utils/FileEmitter.ts#L47
468
+ // https://github.com/rollup/rollup/blob/5a5391971d695c808eed0c5d7d2c6ccb594fc689/src/Chunk.ts#L870
469
+ export const generateVersion = (parts, length) => {
470
+ const hash = createHash("sha256");
471
+ parts.forEach((part) => {
472
+ hash.update(part);
473
+ });
474
+ return hash.digest("hex").slice(0, length);
475
+ };
476
+
477
+ const injectVersionPlaceholderIntoBuildSpecifier = ({
478
+ buildSpecifier,
479
+ versionPlaceholder,
480
+ versioningMethod,
481
+ }) => {
482
+ if (versioningMethod === "search_param") {
483
+ return injectQueryParamIntoSpecifierWithoutEncoding(
484
+ buildSpecifier,
485
+ "v",
486
+ versionPlaceholder,
487
+ );
488
+ }
489
+ return renderUrlOrRelativeUrlFilename(
490
+ buildSpecifier,
491
+ ({ basename, extension }) => {
492
+ return `${basename}-${versionPlaceholder}${extension}`;
493
+ },
494
+ );
495
+ };
@@ -10,30 +10,30 @@ import {
10
10
  import { isWebWorkerUrlInfo } from "@jsenv/core/src/kitchen/web_workers.js";
11
11
  import { prependContent } from "../kitchen/prepend_content.js";
12
12
 
13
- export const injectVersionMappingsAsGlobal = async ({
14
- kitchen,
13
+ export const injectVersionMappingsAsGlobal = async (
15
14
  urlInfo,
16
15
  versionMappings,
17
- }) => {
16
+ ) => {
18
17
  if (urlInfo.type === "html") {
19
- return prependContent(kitchen.urlInfoTransformer, urlInfo, {
18
+ await prependContent(urlInfo, {
20
19
  type: "js_classic",
21
20
  content: generateClientCodeForVersionMappings(versionMappings, {
22
21
  globalName: "window",
23
- minification: kitchen.kitchenContext.minification,
22
+ minification: urlInfo.context.minification,
24
23
  }),
25
24
  });
25
+ return;
26
26
  }
27
27
  if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
28
- return prependContent(kitchen.urlInfoTransformer, urlInfo, {
28
+ await prependContent(urlInfo, {
29
29
  type: "js_classic",
30
30
  content: generateClientCodeForVersionMappings(versionMappings, {
31
31
  globalName: isWebWorkerUrlInfo(urlInfo) ? "self" : "window",
32
- minification: kitchen.kitchenContext.minification,
32
+ minification: urlInfo.context.minification,
33
33
  }),
34
34
  });
35
+ return;
35
36
  }
36
- return null;
37
37
  };
38
38
 
39
39
  const generateClientCodeForVersionMappings = (
@@ -46,18 +46,16 @@ const generateClientCodeForVersionMappings = (
46
46
  )}; ${globalName}.__v__ = function (s) { return m[s] || s }; })();`;
47
47
  }
48
48
  return `;(function() {
49
- var __versionMappings__ = ${JSON.stringify(versionMappings, null, " ")};
49
+ var __versionMappings__ = {
50
+ ${stringifyParams(versionMappings, " ")}
51
+ };
50
52
  ${globalName}.__v__ = function (specifier) {
51
53
  return __versionMappings__[specifier] || specifier
52
54
  };
53
55
  })();`;
54
56
  };
55
57
 
56
- export const injectVersionMappingsAsImportmap = async ({
57
- kitchen,
58
- urlInfo,
59
- versionMappings,
60
- }) => {
58
+ export const injectVersionMappingsAsImportmap = (urlInfo, versionMappings) => {
61
59
  const htmlAst = parseHtmlString(urlInfo.content, {
62
60
  storeOriginalPositions: false,
63
61
  });
@@ -69,13 +67,26 @@ export const injectVersionMappingsAsImportmap = async ({
69
67
  createHtmlNode({
70
68
  tagName: "script",
71
69
  type: "importmap",
72
- textContent: kitchen.kitchenContext.minification
70
+ textContent: urlInfo.context.minification
73
71
  ? JSON.stringify({ imports: versionMappings })
74
72
  : JSON.stringify({ imports: versionMappings }, null, " "),
75
73
  }),
76
74
  "jsenv:versioning",
77
75
  );
78
- kitchen.urlInfoTransformer.applyTransformations(urlInfo, {
76
+ urlInfo.mutateContent({
79
77
  content: stringifyHtmlAst(htmlAst),
80
78
  });
81
79
  };
80
+
81
+ const stringifyParams = (params, prefix = "") => {
82
+ const source = JSON.stringify(params, null, prefix);
83
+ if (prefix.length) {
84
+ // remove leading "{\n"
85
+ // remove leading prefix
86
+ // remove trailing "\n}"
87
+ return source.slice(2 + prefix.length, -2);
88
+ }
89
+ // remove leading "{"
90
+ // remove trailing "}"
91
+ return source.slice(1, -1);
92
+ };