@jsenv/core 41.5.24 → 41.5.26

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.
@@ -2873,6 +2873,14 @@ var require$$1 = [
2873
2873
  security: false,
2874
2874
  v8: "13.6.233.17"
2875
2875
  },
2876
+ {
2877
+ name: "nodejs",
2878
+ version: "24.21.0",
2879
+ date: "2026-09-07",
2880
+ lts: "Krypton",
2881
+ security: false,
2882
+ v8: "13.6.233.17"
2883
+ },
2876
2884
  {
2877
2885
  name: "nodejs",
2878
2886
  version: "25.0.0",
@@ -3410,8 +3418,11 @@ function requireVersions () {
3410
3418
  "43.3": "150",
3411
3419
  "43.4": "150",
3412
3420
  "43.5": "150",
3421
+ "43.6": "150",
3413
3422
  "44.0": "152",
3414
3423
  "44.1": "152",
3424
+ "44.2": "152",
3425
+ "44.3": "152",
3415
3426
  "45.0": "155"
3416
3427
  };
3417
3428
  return versions;
@@ -2460,7 +2460,7 @@ ${urlInfo.url}`,
2460
2460
  const injectionSymbol = Symbol.for("jsenv_injection");
2461
2461
  const INJECTIONS = {
2462
2462
  /**
2463
- * Inject `Object.assign(window, { [key]: value })` at the top of the file
2463
+ * Inject `Object.assign(globalThis, { [key]: value })` at the top of the file
2464
2464
  * (into a script for html, into the module itself for js) instead of
2465
2465
  * replacing a placeholder: the value is read at runtime as a global.
2466
2466
  */
@@ -2611,7 +2611,7 @@ const injectGlobals = (content, globals, urlInfo) => {
2611
2611
  return globalInjectorOnHtml(content, globals, urlInfo);
2612
2612
  }
2613
2613
  if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
2614
- return globalsInjectorOnJs(content, globals, urlInfo);
2614
+ return globalsInjectorOnJs(content, globals);
2615
2615
  }
2616
2616
  throw new Error(
2617
2617
  createDetailedMessage(`cannot inject globals into "${urlInfo.type}"`, {
@@ -2631,9 +2631,7 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
2631
2631
  url: urlInfo.url,
2632
2632
  storeOriginalPositions: false,
2633
2633
  });
2634
- const clientCode = generateClientCodeForGlobals(globals, {
2635
- isWebWorker: false,
2636
- });
2634
+ const clientCode = generateClientCodeForGlobals(globals);
2637
2635
  injectJsenvScript(htmlAst, {
2638
2636
  content: clientCode,
2639
2637
  pluginName: "jsenv:inject_globals",
@@ -2642,24 +2640,17 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
2642
2640
  content: stringifyHtmlAst(htmlAst),
2643
2641
  };
2644
2642
  };
2645
- const globalsInjectorOnJs = (content, globals, urlInfo) => {
2646
- const clientCode = generateClientCodeForGlobals(globals, {
2647
- isWebWorker:
2648
- urlInfo.subtype === "worker" ||
2649
- urlInfo.subtype === "service_worker" ||
2650
- urlInfo.subtype === "shared_worker",
2651
- });
2643
+ const globalsInjectorOnJs = (content, globals) => {
2644
+ const clientCode = generateClientCodeForGlobals(globals);
2652
2645
  const magicSource = createMagicSource(content);
2653
2646
  magicSource.prepend(clientCode);
2654
2647
  return magicSource.toContentAndSourcemap();
2655
2648
  };
2656
- const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
2657
- const globalName = isWebWorker ? "self" : "window";
2658
- return `Object.assign(${globalName}, ${JSON.stringify(
2659
- globals,
2660
- null,
2661
- " ",
2662
- )});`;
2649
+ // "globalThis" is the global object in a window, a worker and a service worker alike;
2650
+ // naming one of "window"/"self" would require knowing the file's subtype, which is not
2651
+ // known yet when the browser fetches a service worker on its own (update check).
2652
+ const generateClientCodeForGlobals = (globals) => {
2653
+ return `Object.assign(globalThis, ${JSON.stringify(globals, null, " ")});`;
2663
2654
  };
2664
2655
 
2665
2656
  const defineGettersOnPropertiesDerivedFromOriginalContent = (
@@ -4892,6 +4883,140 @@ const isBareSpecifier = (specifier) => {
4892
4883
  }
4893
4884
  };
4894
4885
 
4886
+ /*
4887
+ * Text patches applied to files as they are served and built, keyed by file:
4888
+ *
4889
+ * patches: {
4890
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
4891
+ * }
4892
+ *
4893
+ * A key is a url pattern relative to the root directory ("./main.js",
4894
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
4895
+ * walking up from the root directory into node_modules, the way node does,
4896
+ * so the key holds wherever the package manager hoists the package.
4897
+ *
4898
+ * Every `from` must occur exactly once in the file, otherwise the file fails
4899
+ * to cook and says which patch did not apply: a dependency update that moved
4900
+ * the patched code must be looked at, never silently unpatched.
4901
+ */
4902
+
4903
+
4904
+ const jsenvPluginPatches = (rawPatches) => {
4905
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
4906
+ return [];
4907
+ }
4908
+ let findPatches;
4909
+ const patchesPlugin = {
4910
+ name: "jsenv:patches",
4911
+ appliesDuring: "*",
4912
+ init: (context) => {
4913
+ const { rootDirectoryUrl } = context;
4914
+ const patchesByPattern = {};
4915
+ for (const key of Object.keys(rawPatches)) {
4916
+ const patches = rawPatches[key];
4917
+ assertPatches(patches, key);
4918
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
4919
+ }
4920
+ const associations = URL_META.resolveAssociations(
4921
+ { patches: patchesByPattern },
4922
+ rootDirectoryUrl,
4923
+ );
4924
+ findPatches = (url) => {
4925
+ const { patches } = URL_META.applyAssociations({
4926
+ url: asUrlWithoutSearch(url),
4927
+ associations,
4928
+ });
4929
+ return patches;
4930
+ };
4931
+ },
4932
+ transformUrlContent: (urlInfo) => {
4933
+ const patches = findPatches(urlInfo.url);
4934
+ if (!patches) {
4935
+ return null;
4936
+ }
4937
+ const { content } = urlInfo;
4938
+ const magicSource = createMagicSource(content);
4939
+ for (const { from, to } of patches) {
4940
+ const start = content.indexOf(from);
4941
+ const occurrenceCount =
4942
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
4943
+ if (occurrenceCount !== 1) {
4944
+ const fileRelativeUrl = urlToRelativeUrl(
4945
+ urlInfo.url,
4946
+ urlInfo.context.rootDirectoryUrl,
4947
+ );
4948
+ throw new Error(
4949
+ `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
4950
+ );
4951
+ }
4952
+ magicSource.replace({
4953
+ start,
4954
+ end: start + from.length,
4955
+ replacement: to,
4956
+ });
4957
+ }
4958
+ return magicSource.toContentAndSourcemap();
4959
+ },
4960
+ };
4961
+ return [patchesPlugin];
4962
+ };
4963
+
4964
+ const assertPatches = (patches, key) => {
4965
+ if (!Array.isArray(patches)) {
4966
+ throw new TypeError(
4967
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
4968
+ );
4969
+ }
4970
+ for (const patch of patches) {
4971
+ if (
4972
+ !patch ||
4973
+ typeof patch.from !== "string" ||
4974
+ patch.from === "" ||
4975
+ typeof patch.to !== "string"
4976
+ ) {
4977
+ throw new TypeError(
4978
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
4979
+ );
4980
+ }
4981
+ }
4982
+ };
4983
+
4984
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
4985
+ // else names a path inside a package, looked up in node_modules
4986
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
4987
+ if (
4988
+ key.startsWith("./") ||
4989
+ key.startsWith("../") ||
4990
+ key.startsWith("/") ||
4991
+ key.startsWith("file:") ||
4992
+ key.startsWith("*")
4993
+ ) {
4994
+ return key;
4995
+ }
4996
+ const segments = key.split("/");
4997
+ const packageName = key.startsWith("@")
4998
+ ? `${segments[0]}/${segments[1]}`
4999
+ : segments[0];
5000
+ const pathInsidePackage = key.slice(packageName.length);
5001
+ let directoryUrl = new URL(rootDirectoryUrl);
5002
+ while (true) {
5003
+ const packageDirectoryUrl = new URL(
5004
+ `./node_modules/${packageName}/`,
5005
+ directoryUrl,
5006
+ );
5007
+ if (existsSync(packageDirectoryUrl)) {
5008
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
5009
+ }
5010
+ const parentDirectoryUrl = new URL("../", directoryUrl);
5011
+ if (parentDirectoryUrl.href === directoryUrl.href) {
5012
+ throw new Error(
5013
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
5014
+ );
5015
+ }
5016
+ directoryUrl = parentDirectoryUrl;
5017
+ }
5018
+ };
5019
+
4895
5020
  /*
4896
5021
  * https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/css/src/CSSTransformer.js
4897
5022
  */
@@ -8184,140 +8309,6 @@ const asInheritedInjections = (injections) => {
8184
8309
  return inheritedInjections;
8185
8310
  };
8186
8311
 
8187
- /*
8188
- * Text patches applied to files as they are served and built, keyed by file:
8189
- *
8190
- * patches: {
8191
- * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
8192
- * }
8193
- *
8194
- * A key is a url pattern relative to the root directory ("./main.js",
8195
- * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
8196
- * walking up from the root directory into node_modules, the way node does,
8197
- * so the key holds wherever the package manager hoists the package.
8198
- *
8199
- * Every `from` must occur exactly once in the file, otherwise the file fails
8200
- * to cook and says which patch did not apply: a dependency update that moved
8201
- * the patched code must be looked at, never silently unpatched.
8202
- */
8203
-
8204
-
8205
- const jsenvPluginPatches = (rawPatches) => {
8206
- if (!rawPatches || Object.keys(rawPatches).length === 0) {
8207
- return [];
8208
- }
8209
- let findPatches;
8210
- const patchesPlugin = {
8211
- name: "jsenv:patches",
8212
- appliesDuring: "*",
8213
- init: (context) => {
8214
- const { rootDirectoryUrl } = context;
8215
- const patchesByPattern = {};
8216
- for (const key of Object.keys(rawPatches)) {
8217
- const patches = rawPatches[key];
8218
- assertPatches(patches, key);
8219
- patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
8220
- }
8221
- const associations = URL_META.resolveAssociations(
8222
- { patches: patchesByPattern },
8223
- rootDirectoryUrl,
8224
- );
8225
- findPatches = (url) => {
8226
- const { patches } = URL_META.applyAssociations({
8227
- url: asUrlWithoutSearch(url),
8228
- associations,
8229
- });
8230
- return patches;
8231
- };
8232
- },
8233
- transformUrlContent: (urlInfo) => {
8234
- const patches = findPatches(urlInfo.url);
8235
- if (!patches) {
8236
- return null;
8237
- }
8238
- const { content } = urlInfo;
8239
- const magicSource = createMagicSource(content);
8240
- for (const { from, to } of patches) {
8241
- const start = content.indexOf(from);
8242
- const occurrenceCount =
8243
- start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
8244
- if (occurrenceCount !== 1) {
8245
- const fileRelativeUrl = urlToRelativeUrl(
8246
- urlInfo.url,
8247
- urlInfo.context.rootDirectoryUrl,
8248
- );
8249
- throw new Error(
8250
- `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
8251
- );
8252
- }
8253
- magicSource.replace({
8254
- start,
8255
- end: start + from.length,
8256
- replacement: to,
8257
- });
8258
- }
8259
- return magicSource.toContentAndSourcemap();
8260
- },
8261
- };
8262
- return [patchesPlugin];
8263
- };
8264
-
8265
- const assertPatches = (patches, key) => {
8266
- if (!Array.isArray(patches)) {
8267
- throw new TypeError(
8268
- `patches["${key}"] must be an array of { from, to }, got ${patches}`,
8269
- );
8270
- }
8271
- for (const patch of patches) {
8272
- if (
8273
- !patch ||
8274
- typeof patch.from !== "string" ||
8275
- patch.from === "" ||
8276
- typeof patch.to !== "string"
8277
- ) {
8278
- throw new TypeError(
8279
- `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
8280
- );
8281
- }
8282
- }
8283
- };
8284
-
8285
- // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
8286
- // else names a path inside a package, looked up in node_modules
8287
- const resolvePatchKey = (key, rootDirectoryUrl) => {
8288
- if (
8289
- key.startsWith("./") ||
8290
- key.startsWith("../") ||
8291
- key.startsWith("/") ||
8292
- key.startsWith("file:") ||
8293
- key.startsWith("*")
8294
- ) {
8295
- return key;
8296
- }
8297
- const segments = key.split("/");
8298
- const packageName = key.startsWith("@")
8299
- ? `${segments[0]}/${segments[1]}`
8300
- : segments[0];
8301
- const pathInsidePackage = key.slice(packageName.length);
8302
- let directoryUrl = new URL(rootDirectoryUrl);
8303
- while (true) {
8304
- const packageDirectoryUrl = new URL(
8305
- `./node_modules/${packageName}/`,
8306
- directoryUrl,
8307
- );
8308
- if (existsSync(packageDirectoryUrl)) {
8309
- return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
8310
- }
8311
- const parentDirectoryUrl = new URL("../", directoryUrl);
8312
- if (parentDirectoryUrl.href === directoryUrl.href) {
8313
- throw new Error(
8314
- `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
8315
- );
8316
- }
8317
- directoryUrl = parentDirectoryUrl;
8318
- }
8319
- };
8320
-
8321
8312
  /*
8322
8313
  * Some code uses globals specific to Node.js in code meant to run in browsers...
8323
8314
  * This plugin will replace some node globals to things compatible with web:
@@ -10444,7 +10435,6 @@ const getCorePlugins = ({
10444
10435
  directoryListing = true,
10445
10436
  directoryReferenceEffect,
10446
10437
  supervisor,
10447
- patches,
10448
10438
  injections,
10449
10439
  transpilation = true,
10450
10440
  inlining = true,
@@ -10486,9 +10476,6 @@ const getCorePlugins = ({
10486
10476
  ...(packageBundle
10487
10477
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
10488
10478
  : []),
10489
- // before everything else: what the other plugins read must be the
10490
- // patched file
10491
- ...jsenvPluginPatches(patches),
10492
10479
  // before reference analysis: an url written by an injection must hold its
10493
10480
  // final value when references are analyzed
10494
10481
  jsenvPluginInjections(injections),
@@ -10858,28 +10845,33 @@ const logsDefault = {
10858
10845
  // we nevery minify those because they are already very small
10859
10846
  // and would hurt the readability of something that can be critical to debug
10860
10847
  const injectGlobalMappings = async (urlInfo, mappings) => {
10861
- if (urlInfo.type === "html") {
10862
- // const minification = Boolean(
10863
- // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
10864
- // );
10865
- const content = generateClientCodeForMappings(mappings, {
10866
- globalName: "window",
10867
- minification: false,
10868
- });
10869
- await prependContent(urlInfo, { type: "js_classic", content });
10848
+ if (
10849
+ urlInfo.type !== "html" &&
10850
+ urlInfo.type !== "js_classic" &&
10851
+ urlInfo.type !== "js_module"
10852
+ ) {
10870
10853
  return;
10871
10854
  }
10872
- if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
10873
- // const minification = Boolean(
10874
- // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
10875
- // );
10876
- const content = generateClientCodeForMappings(mappings, {
10877
- globalName: isWebWorkerUrlInfo(urlInfo) ? "self" : "window",
10878
- minification: false,
10879
- });
10880
- await prependContent(urlInfo, { type: "js_classic", content });
10881
- return;
10855
+ // const minification = Boolean(
10856
+ // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
10857
+ // );
10858
+ const content = generateClientCodeForMappings(mappings, {
10859
+ globalName: getGlobalName(urlInfo),
10860
+ minification: false,
10861
+ });
10862
+ await prependContent(urlInfo, { type: "js_classic", content });
10863
+ };
10864
+
10865
+ // "globalThis" names the global object in a window and in a worker alike;
10866
+ // the window/self split is only for runtimes predating it.
10867
+ const getGlobalName = (urlInfo) => {
10868
+ if (urlInfo.context.isSupportedOnCurrentClients("global_this")) {
10869
+ return "globalThis";
10882
10870
  }
10871
+ if (isWebWorkerUrlInfo(urlInfo)) {
10872
+ return "self";
10873
+ }
10874
+ return "window";
10883
10875
  };
10884
10876
 
10885
10877
  const generateClientCodeForMappings = (
@@ -12801,7 +12793,7 @@ const jsenvPluginMappings = (mappings) => {
12801
12793
  * `<script>window.backendUrl = __BACKEND_URL__;</script>` gets the JS literal,
12802
12794
  * which is how a value is shared with every js file of the page.
12803
12795
  * Use INJECTIONS.optional(value) for a placeholder that may be absent from the file
12804
- * and INJECTIONS.global(value) to inject `Object.assign(window, { ... })` instead of
12796
+ * and INJECTIONS.global(value) to inject `Object.assign(globalThis, { ... })` instead of
12805
12797
  * replacing a placeholder.
12806
12798
  *
12807
12799
  * @return {Promise<Object>} buildReturnValue
@@ -13841,6 +13833,9 @@ const prepareEntryPointBuild = async (
13841
13833
 
13842
13834
  let _getOtherEntryBuildInfo;
13843
13835
  const rawJsenvPluginStore = await createJsenvPluginStore([
13836
+ // First, ahead of the plugins given by the caller: what every other plugin
13837
+ // reads must be the patched file (see start_dev_server.js).
13838
+ ...jsenvPluginPatches(patches),
13844
13839
  ...(mappings ? [jsenvPluginMappings(mappings)] : []),
13845
13840
  {
13846
13841
  name: "jsenv:other_entry_point_build_during_craft",
@@ -13875,7 +13870,6 @@ const prepareEntryPointBuild = async (
13875
13870
  magicExtensions,
13876
13871
  magicDirectoryIndex,
13877
13872
  directoryReferenceEffect,
13878
- patches,
13879
13873
  injections,
13880
13874
  transpilation: {
13881
13875
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -4,7 +4,7 @@ import "@jsenv/sourcemap";
4
4
  const injectionSymbol = Symbol.for("jsenv_injection");
5
5
  const INJECTIONS = {
6
6
  /**
7
- * Inject `Object.assign(window, { [key]: value })` at the top of the file
7
+ * Inject `Object.assign(globalThis, { [key]: value })` at the top of the file
8
8
  * (into a script for html, into the module itself for js) instead of
9
9
  * replacing a placeholder: the value is read at runtime as a global.
10
10
  */
@@ -1,12 +1,12 @@
1
1
  import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, fetchDirectory, composeTwoResponses, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
2
2
  import { existsSync, statSync, readFileSync, realpathSync, readdirSync, lstatSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
- import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, createDetailedMessage, UNICODE, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, urlToFilename, asUrlWithoutSearch, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, inferRuntimeCompatFromClosestPackage, createTaskLog } from "./jsenv_core_packages.js";
4
+ import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, URL_META, asUrlWithoutSearch, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, createDetailedMessage, UNICODE, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, urlToFilename, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, inferRuntimeCompatFromClosestPackage, createTaskLog } from "./jsenv_core_packages.js";
5
5
  import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
6
6
  import { parseHtml, injectJsenvScript, stringifyHtmlAst, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, hasCssOpaqueDirective, getUrlForContentInsideJs, renderCssTemplateLiteral, applyBabelPlugins, visitJsAst, getImportMetaPropertyName, visitJsAstUntil, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
7
+ import { createMagicSource, composeTwoSourcemaps, generateSourcemapFileUrl, generateSourcemapDataUrl, SOURCEMAP, applyContentEditsOnSourcemap, composeSourcemaps } from "@jsenv/sourcemap";
7
8
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
8
9
  import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
9
- import { createMagicSource, composeTwoSourcemaps, generateSourcemapFileUrl, generateSourcemapDataUrl, SOURCEMAP, applyContentEditsOnSourcemap, composeSourcemaps } from "@jsenv/sourcemap";
10
10
  import { bundleJsModules } from "@jsenv/plugin-bundling";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/plugins/filesystem/filesystem_error_to_response.js";
@@ -1574,6 +1574,140 @@ const jsenvPluginPageSwitcher = () => {
1574
1574
  };
1575
1575
  };
1576
1576
 
1577
+ /*
1578
+ * Text patches applied to files as they are served and built, keyed by file:
1579
+ *
1580
+ * patches: {
1581
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
1582
+ * }
1583
+ *
1584
+ * A key is a url pattern relative to the root directory ("./main.js",
1585
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
1586
+ * walking up from the root directory into node_modules, the way node does,
1587
+ * so the key holds wherever the package manager hoists the package.
1588
+ *
1589
+ * Every `from` must occur exactly once in the file, otherwise the file fails
1590
+ * to cook and says which patch did not apply: a dependency update that moved
1591
+ * the patched code must be looked at, never silently unpatched.
1592
+ */
1593
+
1594
+
1595
+ const jsenvPluginPatches = (rawPatches) => {
1596
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
1597
+ return [];
1598
+ }
1599
+ let findPatches;
1600
+ const patchesPlugin = {
1601
+ name: "jsenv:patches",
1602
+ appliesDuring: "*",
1603
+ init: (context) => {
1604
+ const { rootDirectoryUrl } = context;
1605
+ const patchesByPattern = {};
1606
+ for (const key of Object.keys(rawPatches)) {
1607
+ const patches = rawPatches[key];
1608
+ assertPatches(patches, key);
1609
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
1610
+ }
1611
+ const associations = URL_META.resolveAssociations(
1612
+ { patches: patchesByPattern },
1613
+ rootDirectoryUrl,
1614
+ );
1615
+ findPatches = (url) => {
1616
+ const { patches } = URL_META.applyAssociations({
1617
+ url: asUrlWithoutSearch(url),
1618
+ associations,
1619
+ });
1620
+ return patches;
1621
+ };
1622
+ },
1623
+ transformUrlContent: (urlInfo) => {
1624
+ const patches = findPatches(urlInfo.url);
1625
+ if (!patches) {
1626
+ return null;
1627
+ }
1628
+ const { content } = urlInfo;
1629
+ const magicSource = createMagicSource(content);
1630
+ for (const { from, to } of patches) {
1631
+ const start = content.indexOf(from);
1632
+ const occurrenceCount =
1633
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
1634
+ if (occurrenceCount !== 1) {
1635
+ const fileRelativeUrl = urlToRelativeUrl(
1636
+ urlInfo.url,
1637
+ urlInfo.context.rootDirectoryUrl,
1638
+ );
1639
+ throw new Error(
1640
+ `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
1641
+ );
1642
+ }
1643
+ magicSource.replace({
1644
+ start,
1645
+ end: start + from.length,
1646
+ replacement: to,
1647
+ });
1648
+ }
1649
+ return magicSource.toContentAndSourcemap();
1650
+ },
1651
+ };
1652
+ return [patchesPlugin];
1653
+ };
1654
+
1655
+ const assertPatches = (patches, key) => {
1656
+ if (!Array.isArray(patches)) {
1657
+ throw new TypeError(
1658
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
1659
+ );
1660
+ }
1661
+ for (const patch of patches) {
1662
+ if (
1663
+ !patch ||
1664
+ typeof patch.from !== "string" ||
1665
+ patch.from === "" ||
1666
+ typeof patch.to !== "string"
1667
+ ) {
1668
+ throw new TypeError(
1669
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
1670
+ );
1671
+ }
1672
+ }
1673
+ };
1674
+
1675
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
1676
+ // else names a path inside a package, looked up in node_modules
1677
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
1678
+ if (
1679
+ key.startsWith("./") ||
1680
+ key.startsWith("../") ||
1681
+ key.startsWith("/") ||
1682
+ key.startsWith("file:") ||
1683
+ key.startsWith("*")
1684
+ ) {
1685
+ return key;
1686
+ }
1687
+ const segments = key.split("/");
1688
+ const packageName = key.startsWith("@")
1689
+ ? `${segments[0]}/${segments[1]}`
1690
+ : segments[0];
1691
+ const pathInsidePackage = key.slice(packageName.length);
1692
+ let directoryUrl = new URL(rootDirectoryUrl);
1693
+ while (true) {
1694
+ const packageDirectoryUrl = new URL(
1695
+ `./node_modules/${packageName}/`,
1696
+ directoryUrl,
1697
+ );
1698
+ if (existsSync(packageDirectoryUrl)) {
1699
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
1700
+ }
1701
+ const parentDirectoryUrl = new URL("../", directoryUrl);
1702
+ if (parentDirectoryUrl.href === directoryUrl.href) {
1703
+ throw new Error(
1704
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
1705
+ );
1706
+ }
1707
+ directoryUrl = parentDirectoryUrl;
1708
+ }
1709
+ };
1710
+
1577
1711
  /*
1578
1712
  * https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/css/src/CSSTransformer.js
1579
1713
  */
@@ -5407,7 +5541,7 @@ const jsenvPluginDirectoryReferenceEffect = (
5407
5541
  const injectionSymbol = Symbol.for("jsenv_injection");
5408
5542
  const INJECTIONS = {
5409
5543
  /**
5410
- * Inject `Object.assign(window, { [key]: value })` at the top of the file
5544
+ * Inject `Object.assign(globalThis, { [key]: value })` at the top of the file
5411
5545
  * (into a script for html, into the module itself for js) instead of
5412
5546
  * replacing a placeholder: the value is read at runtime as a global.
5413
5547
  */
@@ -5558,7 +5692,7 @@ const injectGlobals = (content, globals, urlInfo) => {
5558
5692
  return globalInjectorOnHtml(content, globals, urlInfo);
5559
5693
  }
5560
5694
  if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
5561
- return globalsInjectorOnJs(content, globals, urlInfo);
5695
+ return globalsInjectorOnJs(content, globals);
5562
5696
  }
5563
5697
  throw new Error(
5564
5698
  createDetailedMessage(`cannot inject globals into "${urlInfo.type}"`, {
@@ -5578,9 +5712,7 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
5578
5712
  url: urlInfo.url,
5579
5713
  storeOriginalPositions: false,
5580
5714
  });
5581
- const clientCode = generateClientCodeForGlobals(globals, {
5582
- isWebWorker: false,
5583
- });
5715
+ const clientCode = generateClientCodeForGlobals(globals);
5584
5716
  injectJsenvScript(htmlAst, {
5585
5717
  content: clientCode,
5586
5718
  pluginName: "jsenv:inject_globals",
@@ -5589,24 +5721,17 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
5589
5721
  content: stringifyHtmlAst(htmlAst),
5590
5722
  };
5591
5723
  };
5592
- const globalsInjectorOnJs = (content, globals, urlInfo) => {
5593
- const clientCode = generateClientCodeForGlobals(globals, {
5594
- isWebWorker:
5595
- urlInfo.subtype === "worker" ||
5596
- urlInfo.subtype === "service_worker" ||
5597
- urlInfo.subtype === "shared_worker",
5598
- });
5724
+ const globalsInjectorOnJs = (content, globals) => {
5725
+ const clientCode = generateClientCodeForGlobals(globals);
5599
5726
  const magicSource = createMagicSource(content);
5600
5727
  magicSource.prepend(clientCode);
5601
5728
  return magicSource.toContentAndSourcemap();
5602
5729
  };
5603
- const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
5604
- const globalName = isWebWorker ? "self" : "window";
5605
- return `Object.assign(${globalName}, ${JSON.stringify(
5606
- globals,
5607
- null,
5608
- " ",
5609
- )});`;
5730
+ // "globalThis" is the global object in a window, a worker and a service worker alike;
5731
+ // naming one of "window"/"self" would require knowing the file's subtype, which is not
5732
+ // known yet when the browser fetches a service worker on its own (update check).
5733
+ const generateClientCodeForGlobals = (globals) => {
5734
+ return `Object.assign(globalThis, ${JSON.stringify(globals, null, " ")});`;
5610
5735
  };
5611
5736
 
5612
5737
  const jsenvPluginInjections = (rawAssociations) => {
@@ -5771,140 +5896,6 @@ const asInheritedInjections = (injections) => {
5771
5896
  return inheritedInjections;
5772
5897
  };
5773
5898
 
5774
- /*
5775
- * Text patches applied to files as they are served and built, keyed by file:
5776
- *
5777
- * patches: {
5778
- * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
5779
- * }
5780
- *
5781
- * A key is a url pattern relative to the root directory ("./main.js",
5782
- * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
5783
- * walking up from the root directory into node_modules, the way node does,
5784
- * so the key holds wherever the package manager hoists the package.
5785
- *
5786
- * Every `from` must occur exactly once in the file, otherwise the file fails
5787
- * to cook and says which patch did not apply: a dependency update that moved
5788
- * the patched code must be looked at, never silently unpatched.
5789
- */
5790
-
5791
-
5792
- const jsenvPluginPatches = (rawPatches) => {
5793
- if (!rawPatches || Object.keys(rawPatches).length === 0) {
5794
- return [];
5795
- }
5796
- let findPatches;
5797
- const patchesPlugin = {
5798
- name: "jsenv:patches",
5799
- appliesDuring: "*",
5800
- init: (context) => {
5801
- const { rootDirectoryUrl } = context;
5802
- const patchesByPattern = {};
5803
- for (const key of Object.keys(rawPatches)) {
5804
- const patches = rawPatches[key];
5805
- assertPatches(patches, key);
5806
- patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
5807
- }
5808
- const associations = URL_META.resolveAssociations(
5809
- { patches: patchesByPattern },
5810
- rootDirectoryUrl,
5811
- );
5812
- findPatches = (url) => {
5813
- const { patches } = URL_META.applyAssociations({
5814
- url: asUrlWithoutSearch(url),
5815
- associations,
5816
- });
5817
- return patches;
5818
- };
5819
- },
5820
- transformUrlContent: (urlInfo) => {
5821
- const patches = findPatches(urlInfo.url);
5822
- if (!patches) {
5823
- return null;
5824
- }
5825
- const { content } = urlInfo;
5826
- const magicSource = createMagicSource(content);
5827
- for (const { from, to } of patches) {
5828
- const start = content.indexOf(from);
5829
- const occurrenceCount =
5830
- start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
5831
- if (occurrenceCount !== 1) {
5832
- const fileRelativeUrl = urlToRelativeUrl(
5833
- urlInfo.url,
5834
- urlInfo.context.rootDirectoryUrl,
5835
- );
5836
- throw new Error(
5837
- `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
5838
- );
5839
- }
5840
- magicSource.replace({
5841
- start,
5842
- end: start + from.length,
5843
- replacement: to,
5844
- });
5845
- }
5846
- return magicSource.toContentAndSourcemap();
5847
- },
5848
- };
5849
- return [patchesPlugin];
5850
- };
5851
-
5852
- const assertPatches = (patches, key) => {
5853
- if (!Array.isArray(patches)) {
5854
- throw new TypeError(
5855
- `patches["${key}"] must be an array of { from, to }, got ${patches}`,
5856
- );
5857
- }
5858
- for (const patch of patches) {
5859
- if (
5860
- !patch ||
5861
- typeof patch.from !== "string" ||
5862
- patch.from === "" ||
5863
- typeof patch.to !== "string"
5864
- ) {
5865
- throw new TypeError(
5866
- `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
5867
- );
5868
- }
5869
- }
5870
- };
5871
-
5872
- // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
5873
- // else names a path inside a package, looked up in node_modules
5874
- const resolvePatchKey = (key, rootDirectoryUrl) => {
5875
- if (
5876
- key.startsWith("./") ||
5877
- key.startsWith("../") ||
5878
- key.startsWith("/") ||
5879
- key.startsWith("file:") ||
5880
- key.startsWith("*")
5881
- ) {
5882
- return key;
5883
- }
5884
- const segments = key.split("/");
5885
- const packageName = key.startsWith("@")
5886
- ? `${segments[0]}/${segments[1]}`
5887
- : segments[0];
5888
- const pathInsidePackage = key.slice(packageName.length);
5889
- let directoryUrl = new URL(rootDirectoryUrl);
5890
- while (true) {
5891
- const packageDirectoryUrl = new URL(
5892
- `./node_modules/${packageName}/`,
5893
- directoryUrl,
5894
- );
5895
- if (existsSync(packageDirectoryUrl)) {
5896
- return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
5897
- }
5898
- const parentDirectoryUrl = new URL("../", directoryUrl);
5899
- if (parentDirectoryUrl.href === directoryUrl.href) {
5900
- throw new Error(
5901
- `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
5902
- );
5903
- }
5904
- directoryUrl = parentDirectoryUrl;
5905
- }
5906
- };
5907
-
5908
5899
  const jsenvPluginInliningAsDataUrl = () => {
5909
5900
  return {
5910
5901
  name: "jsenv:inlining_as_data_url",
@@ -8344,7 +8335,6 @@ const getCorePlugins = ({
8344
8335
  directoryListing = true,
8345
8336
  directoryReferenceEffect,
8346
8337
  supervisor,
8347
- patches,
8348
8338
  injections,
8349
8339
  transpilation = true,
8350
8340
  inlining = true,
@@ -8386,9 +8376,6 @@ const getCorePlugins = ({
8386
8376
  ...(packageBundle
8387
8377
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
8388
8378
  : []),
8389
- // before everything else: what the other plugins read must be the
8390
- // patched file
8391
- ...jsenvPluginPatches(patches),
8392
8379
  // before reference analysis: an url written by an injection must hold its
8393
8380
  // final value when references are analyzed
8394
8381
  jsenvPluginInjections(injections),
@@ -12124,8 +12111,20 @@ const devServerPluginServeSourceFiles = ({
12124
12111
  rootDirectoryUrl: sourceDirectoryUrl,
12125
12112
  })
12126
12113
  : sourceDirectoryUrl;
12114
+ // What the graph knows this resource as: the specifier a reference
12115
+ // decodes to never carries "?hot" (the client adds it to re-import,
12116
+ // see jsenv_plugin_hot_search_param), so the request is compared
12117
+ // without it — or nothing inline ever matches its own re-import,
12118
+ // and a file that a re-cook could create is created twice.
12119
+ const requestResourceWithoutHot = WEB_URL_CONVERTER.asWebUrl(
12120
+ requestedUrl,
12121
+ {
12122
+ origin: request.origin,
12123
+ rootDirectoryUrl: sourceDirectoryUrl,
12124
+ },
12125
+ ).slice(request.origin.length);
12127
12126
  let reference = kitchen.graph.inferReference(
12128
- request.resource,
12127
+ requestResourceWithoutHot,
12129
12128
  parentUrl,
12130
12129
  );
12131
12130
  if (!reference) {
@@ -12168,7 +12167,7 @@ const devServerPluginServeSourceFiles = ({
12168
12167
  });
12169
12168
  }
12170
12169
  reference = kitchen.graph.inferReference(
12171
- request.resource,
12170
+ requestResourceWithoutHot,
12172
12171
  inlineParentUrl,
12173
12172
  );
12174
12173
  if (!reference) {
@@ -12748,6 +12747,11 @@ const startDevServer = async ({
12748
12747
  serverStopCallbackSet.add(dependencyWatcher.stop);
12749
12748
 
12750
12749
  const devServerJsenvPluginStore = await createJsenvPluginStore([
12750
+ // First, ahead of the plugins given by the caller: what every other plugin
12751
+ // reads must be the patched file, and a caller's plugin may rewrite a file
12752
+ // (plugin-preact reprints one it instruments) before a patch written
12753
+ // against its text on disk gets to see it.
12754
+ ...jsenvPluginPatches(patches),
12751
12755
  jsenvPluginServerEvents({ clientAutoreload }),
12752
12756
  // The client-monitoring dashboard is a dev-time convenience; a test-plan run
12753
12757
  // doesn't use it and shouldn't pay for the reporter being injected into
@@ -12775,7 +12779,6 @@ const startDevServer = async ({
12775
12779
  magicDirectoryIndex,
12776
12780
  directoryListing,
12777
12781
  supervisor,
12778
- patches,
12779
12782
  injections,
12780
12783
  transpilation,
12781
12784
  spa,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.5.24",
3
+ "version": "41.5.26",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -73,14 +73,14 @@
73
73
  "test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
74
74
  },
75
75
  "dependencies": {
76
- "@jsenv/ast": "6.10.5",
77
- "@jsenv/js-module-fallback": "1.6.9",
78
- "@jsenv/plugin-bundling": "2.10.28",
79
- "@jsenv/plugin-minification": "1.7.22",
80
- "@jsenv/plugin-supervisor": "1.8.24",
81
- "@jsenv/plugin-transpilation": "1.7.9",
76
+ "@jsenv/ast": "6.10.7",
77
+ "@jsenv/js-module-fallback": "1.6.11",
78
+ "@jsenv/plugin-bundling": "2.10.29",
79
+ "@jsenv/plugin-minification": "1.7.24",
80
+ "@jsenv/plugin-supervisor": "1.8.26",
81
+ "@jsenv/plugin-transpilation": "1.7.11",
82
82
  "@jsenv/server": "17.6.6",
83
- "@jsenv/sourcemap": "1.4.9",
83
+ "@jsenv/sourcemap": "1.4.10",
84
84
  "react-table": "7.8.0"
85
85
  },
86
86
  "devDependencies": {
@@ -67,6 +67,7 @@ import {
67
67
  createJsenvPluginStore,
68
68
  } from "../plugins/jsenv_plugins_controller.js";
69
69
  import { isBareSpecifier } from "../helpers/bare_specifier.js";
70
+ import { jsenvPluginPatches } from "../plugins/patches/jsenv_plugin_patches.js";
70
71
  import { getCorePlugins } from "../plugins/plugins.js";
71
72
  import { jsenvPluginReferenceAnalysis } from "../plugins/reference_analysis/jsenv_plugin_reference_analysis.js";
72
73
  import { renderBuildDoneLog } from "./build_content_report.js";
@@ -177,7 +178,7 @@ import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
177
178
  * `<script>window.backendUrl = __BACKEND_URL__;</script>` gets the JS literal,
178
179
  * which is how a value is shared with every js file of the page.
179
180
  * Use INJECTIONS.optional(value) for a placeholder that may be absent from the file
180
- * and INJECTIONS.global(value) to inject `Object.assign(window, { ... })` instead of
181
+ * and INJECTIONS.global(value) to inject `Object.assign(globalThis, { ... })` instead of
181
182
  * replacing a placeholder.
182
183
  *
183
184
  * @return {Promise<Object>} buildReturnValue
@@ -1219,6 +1220,9 @@ const prepareEntryPointBuild = async (
1219
1220
 
1220
1221
  let _getOtherEntryBuildInfo;
1221
1222
  const rawJsenvPluginStore = await createJsenvPluginStore([
1223
+ // First, ahead of the plugins given by the caller: what every other plugin
1224
+ // reads must be the patched file (see start_dev_server.js).
1225
+ ...jsenvPluginPatches(patches),
1222
1226
  ...(mappings ? [jsenvPluginMappings(mappings)] : []),
1223
1227
  {
1224
1228
  name: "jsenv:other_entry_point_build_during_craft",
@@ -1253,7 +1257,6 @@ const prepareEntryPointBuild = async (
1253
1257
  magicExtensions,
1254
1258
  magicDirectoryIndex,
1255
1259
  directoryReferenceEffect,
1256
- patches,
1257
1260
  injections,
1258
1261
  transpilation: {
1259
1262
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -18,28 +18,33 @@ import { prependContent } from "../kitchen/prepend_content.js";
18
18
  // we nevery minify those because they are already very small
19
19
  // and would hurt the readability of something that can be critical to debug
20
20
  export const injectGlobalMappings = async (urlInfo, mappings) => {
21
- if (urlInfo.type === "html") {
22
- // const minification = Boolean(
23
- // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
24
- // );
25
- const content = generateClientCodeForMappings(mappings, {
26
- globalName: "window",
27
- minification: false,
28
- });
29
- await prependContent(urlInfo, { type: "js_classic", content });
21
+ if (
22
+ urlInfo.type !== "html" &&
23
+ urlInfo.type !== "js_classic" &&
24
+ urlInfo.type !== "js_module"
25
+ ) {
30
26
  return;
31
27
  }
32
- if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
33
- // const minification = Boolean(
34
- // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
35
- // );
36
- const content = generateClientCodeForMappings(mappings, {
37
- globalName: isWebWorkerUrlInfo(urlInfo) ? "self" : "window",
38
- minification: false,
39
- });
40
- await prependContent(urlInfo, { type: "js_classic", content });
41
- return;
28
+ // const minification = Boolean(
29
+ // urlInfo.context.getPluginMeta("willMinifyJsClassic"),
30
+ // );
31
+ const content = generateClientCodeForMappings(mappings, {
32
+ globalName: getGlobalName(urlInfo),
33
+ minification: false,
34
+ });
35
+ await prependContent(urlInfo, { type: "js_classic", content });
36
+ };
37
+
38
+ // "globalThis" names the global object in a window and in a worker alike;
39
+ // the window/self split is only for runtimes predating it.
40
+ const getGlobalName = (urlInfo) => {
41
+ if (urlInfo.context.isSupportedOnCurrentClients("global_this")) {
42
+ return "globalThis";
43
+ }
44
+ if (isWebWorkerUrlInfo(urlInfo)) {
45
+ return "self";
42
46
  }
47
+ return "window";
43
48
  };
44
49
 
45
50
  const generateClientCodeForMappings = (
@@ -274,8 +274,20 @@ export const devServerPluginServeSourceFiles = ({
274
274
  rootDirectoryUrl: sourceDirectoryUrl,
275
275
  })
276
276
  : sourceDirectoryUrl;
277
+ // What the graph knows this resource as: the specifier a reference
278
+ // decodes to never carries "?hot" (the client adds it to re-import,
279
+ // see jsenv_plugin_hot_search_param), so the request is compared
280
+ // without it — or nothing inline ever matches its own re-import,
281
+ // and a file that a re-cook could create is created twice.
282
+ const requestResourceWithoutHot = WEB_URL_CONVERTER.asWebUrl(
283
+ requestedUrl,
284
+ {
285
+ origin: request.origin,
286
+ rootDirectoryUrl: sourceDirectoryUrl,
287
+ },
288
+ ).slice(request.origin.length);
277
289
  let reference = kitchen.graph.inferReference(
278
- request.resource,
290
+ requestResourceWithoutHot,
279
291
  parentUrl,
280
292
  );
281
293
  if (!reference) {
@@ -318,7 +330,7 @@ export const devServerPluginServeSourceFiles = ({
318
330
  });
319
331
  }
320
332
  reference = kitchen.graph.inferReference(
321
- request.resource,
333
+ requestResourceWithoutHot,
322
334
  inlineParentUrl,
323
335
  );
324
336
  if (!reference) {
@@ -27,6 +27,7 @@ import { createPackageDirectory } from "../kitchen/package_directory.js";
27
27
  import { createJsenvPluginStore } from "../plugins/jsenv_plugins_controller.js";
28
28
  import { jsenvPluginClientMonitoring } from "../plugins/client_monitoring/jsenv_plugin_client_monitoring.js";
29
29
  import { jsenvPluginPageSwitcher } from "../plugins/page_switcher/jsenv_plugin_page_switcher.js";
30
+ import { jsenvPluginPatches } from "../plugins/patches/jsenv_plugin_patches.js";
30
31
  import { getCorePlugins } from "../plugins/plugins.js";
31
32
  import { jsenvPluginServerEvents } from "../plugins/server_events/jsenv_plugin_server_events.js";
32
33
  import { devServerPluginChromeDevToolsJson } from "./dev_server_plugins/dev_server_plugin_chrome_devtools_json.js";
@@ -284,6 +285,11 @@ export const startDevServer = async ({
284
285
  serverStopCallbackSet.add(dependencyWatcher.stop);
285
286
 
286
287
  const devServerJsenvPluginStore = await createJsenvPluginStore([
288
+ // First, ahead of the plugins given by the caller: what every other plugin
289
+ // reads must be the patched file, and a caller's plugin may rewrite a file
290
+ // (plugin-preact reprints one it instruments) before a patch written
291
+ // against its text on disk gets to see it.
292
+ ...jsenvPluginPatches(patches),
287
293
  jsenvPluginServerEvents({ clientAutoreload }),
288
294
  // The client-monitoring dashboard is a dev-time convenience; a test-plan run
289
295
  // doesn't use it and shouldn't pay for the reporter being injected into
@@ -311,7 +317,6 @@ export const startDevServer = async ({
311
317
  magicDirectoryIndex,
312
318
  directoryListing,
313
319
  supervisor,
314
- patches,
315
320
  injections,
316
321
  transpilation,
317
322
  spa,
@@ -5,7 +5,7 @@ import { composeTwoSourcemaps, createMagicSource } from "@jsenv/sourcemap";
5
5
  const injectionSymbol = Symbol.for("jsenv_injection");
6
6
  export const INJECTIONS = {
7
7
  /**
8
- * Inject `Object.assign(window, { [key]: value })` at the top of the file
8
+ * Inject `Object.assign(globalThis, { [key]: value })` at the top of the file
9
9
  * (into a script for html, into the module itself for js) instead of
10
10
  * replacing a placeholder: the value is read at runtime as a global.
11
11
  */
@@ -156,7 +156,7 @@ export const injectGlobals = (content, globals, urlInfo) => {
156
156
  return globalInjectorOnHtml(content, globals, urlInfo);
157
157
  }
158
158
  if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
159
- return globalsInjectorOnJs(content, globals, urlInfo);
159
+ return globalsInjectorOnJs(content, globals);
160
160
  }
161
161
  throw new Error(
162
162
  createDetailedMessage(`cannot inject globals into "${urlInfo.type}"`, {
@@ -176,9 +176,7 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
176
176
  url: urlInfo.url,
177
177
  storeOriginalPositions: false,
178
178
  });
179
- const clientCode = generateClientCodeForGlobals(globals, {
180
- isWebWorker: false,
181
- });
179
+ const clientCode = generateClientCodeForGlobals(globals);
182
180
  injectJsenvScript(htmlAst, {
183
181
  content: clientCode,
184
182
  pluginName: "jsenv:inject_globals",
@@ -187,22 +185,15 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
187
185
  content: stringifyHtmlAst(htmlAst),
188
186
  };
189
187
  };
190
- const globalsInjectorOnJs = (content, globals, urlInfo) => {
191
- const clientCode = generateClientCodeForGlobals(globals, {
192
- isWebWorker:
193
- urlInfo.subtype === "worker" ||
194
- urlInfo.subtype === "service_worker" ||
195
- urlInfo.subtype === "shared_worker",
196
- });
188
+ const globalsInjectorOnJs = (content, globals) => {
189
+ const clientCode = generateClientCodeForGlobals(globals);
197
190
  const magicSource = createMagicSource(content);
198
191
  magicSource.prepend(clientCode);
199
192
  return magicSource.toContentAndSourcemap();
200
193
  };
201
- const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
202
- const globalName = isWebWorker ? "self" : "window";
203
- return `Object.assign(${globalName}, ${JSON.stringify(
204
- globals,
205
- null,
206
- " ",
207
- )});`;
194
+ // "globalThis" is the global object in a window, a worker and a service worker alike;
195
+ // naming one of "window"/"self" would require knowing the file's subtype, which is not
196
+ // known yet when the browser fetches a service worker on its own (update check).
197
+ const generateClientCodeForGlobals = (globals) => {
198
+ return `Object.assign(globalThis, ${JSON.stringify(globals, null, " ")});`;
208
199
  };
@@ -11,7 +11,6 @@ import { jsenvPluginProtocolFile } from "./protocol_file/jsenv_plugin_protocol_f
11
11
  import { jsenvPluginProtocolHttp } from "./protocol_http/jsenv_plugin_protocol_http.js";
12
12
  import { jsenvPluginDirectoryReferenceEffect } from "./directory_reference_effect/jsenv_plugin_directory_reference_effect.js";
13
13
  import { jsenvPluginInjections } from "./injections/jsenv_plugin_injections.js";
14
- import { jsenvPluginPatches } from "./patches/jsenv_plugin_patches.js";
15
14
  import { jsenvPluginInlining } from "./inlining/jsenv_plugin_inlining.js";
16
15
  import { jsenvPluginCommonJsGlobals } from "./commonjs_globals/jsenv_plugin_commonjs_globals.js";
17
16
  import { jsenvPluginImportMetaScenarios } from "./import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js";
@@ -52,7 +51,6 @@ export const getCorePlugins = ({
52
51
  directoryListing = true,
53
52
  directoryReferenceEffect,
54
53
  supervisor,
55
- patches,
56
54
  injections,
57
55
  transpilation = true,
58
56
  inlining = true,
@@ -94,9 +92,6 @@ export const getCorePlugins = ({
94
92
  ...(packageBundle
95
93
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
96
94
  : []),
97
- // before everything else: what the other plugins read must be the
98
- // patched file
99
- ...jsenvPluginPatches(patches),
100
95
  // before reference analysis: an url written by an injection must hold its
101
96
  // final value when references are analyzed
102
97
  jsenvPluginInjections(injections),