@jsenv/core 41.5.20 → 41.5.21

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.
@@ -8184,6 +8184,140 @@ const asInheritedInjections = (injections) => {
8184
8184
  return inheritedInjections;
8185
8185
  };
8186
8186
 
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
+
8187
8321
  /*
8188
8322
  * Some code uses globals specific to Node.js in code meant to run in browsers...
8189
8323
  * This plugin will replace some node globals to things compatible with web:
@@ -10320,6 +10454,7 @@ const getCorePlugins = ({
10320
10454
  directoryListing = true,
10321
10455
  directoryReferenceEffect,
10322
10456
  supervisor,
10457
+ patches,
10323
10458
  injections,
10324
10459
  transpilation = true,
10325
10460
  inlining = true,
@@ -10361,6 +10496,9 @@ const getCorePlugins = ({
10361
10496
  ...(packageBundle
10362
10497
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
10363
10498
  : []),
10499
+ // before everything else: what the other plugins read must be the
10500
+ // patched file
10501
+ ...jsenvPluginPatches(patches),
10364
10502
  // before reference analysis: an url written by an injection must hold its
10365
10503
  // final value when references are analyzed
10366
10504
  jsenvPluginInjections(injections),
@@ -12645,6 +12783,17 @@ const jsenvPluginMappings = (mappings) => {
12645
12783
  * }
12646
12784
  *
12647
12785
  * See startDevServer "ribbon" param for the full list of options.
12786
+ * @param {object} [entryPoint.patches]
12787
+ * Text patches applied to files as they are built, as { file: [{ from, to }] }.
12788
+ * A key is a url pattern relative to sourceDirectoryUrl ("./main.js") or a path
12789
+ * inside a package ("preact/dist/preact.mjs", found in node_modules the way node does):
12790
+ *
12791
+ * patches: {
12792
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
12793
+ * }
12794
+ *
12795
+ * Each `from` must occur exactly once in the file, otherwise the build fails
12796
+ * and says which patch did not apply.
12648
12797
  * @param {object} [entryPoint.injections]
12649
12798
  * Values to inject into files, as { urlPattern: getInjections }.
12650
12799
  * Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
@@ -13510,6 +13659,7 @@ const entryPointDefaultParams = {
13510
13659
  directoryReferenceEffect: undefined,
13511
13660
  scenarioPlaceholders: undefined,
13512
13661
  ribbon: false,
13662
+ patches: undefined,
13513
13663
  injections: undefined,
13514
13664
  transpilation: {},
13515
13665
  preserveComments: undefined,
@@ -13566,6 +13716,7 @@ const prepareEntryPointBuild = async (
13566
13716
  directoryReferenceEffect,
13567
13717
  scenarioPlaceholders,
13568
13718
  ribbon,
13719
+ patches,
13569
13720
  injections,
13570
13721
  transpilation,
13571
13722
  preserveComments,
@@ -13734,6 +13885,7 @@ const prepareEntryPointBuild = async (
13734
13885
  magicExtensions,
13735
13886
  magicDirectoryIndex,
13736
13887
  directoryReferenceEffect,
13888
+ patches,
13737
13889
  injections,
13738
13890
  transpilation: {
13739
13891
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -5652,6 +5652,140 @@ const asInheritedInjections = (injections) => {
5652
5652
  return inheritedInjections;
5653
5653
  };
5654
5654
 
5655
+ /*
5656
+ * Text patches applied to files as they are served and built, keyed by file:
5657
+ *
5658
+ * patches: {
5659
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
5660
+ * }
5661
+ *
5662
+ * A key is a url pattern relative to the root directory ("./main.js",
5663
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
5664
+ * walking up from the root directory into node_modules, the way node does,
5665
+ * so the key holds wherever the package manager hoists the package.
5666
+ *
5667
+ * Every `from` must occur exactly once in the file, otherwise the file fails
5668
+ * to cook and says which patch did not apply: a dependency update that moved
5669
+ * the patched code must be looked at, never silently unpatched.
5670
+ */
5671
+
5672
+
5673
+ const jsenvPluginPatches = (rawPatches) => {
5674
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
5675
+ return [];
5676
+ }
5677
+ let findPatches;
5678
+ const patchesPlugin = {
5679
+ name: "jsenv:patches",
5680
+ appliesDuring: "*",
5681
+ init: (context) => {
5682
+ const { rootDirectoryUrl } = context;
5683
+ const patchesByPattern = {};
5684
+ for (const key of Object.keys(rawPatches)) {
5685
+ const patches = rawPatches[key];
5686
+ assertPatches(patches, key);
5687
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
5688
+ }
5689
+ const associations = URL_META.resolveAssociations(
5690
+ { patches: patchesByPattern },
5691
+ rootDirectoryUrl,
5692
+ );
5693
+ findPatches = (url) => {
5694
+ const { patches } = URL_META.applyAssociations({
5695
+ url: asUrlWithoutSearch(url),
5696
+ associations,
5697
+ });
5698
+ return patches;
5699
+ };
5700
+ },
5701
+ transformUrlContent: (urlInfo) => {
5702
+ const patches = findPatches(urlInfo.url);
5703
+ if (!patches) {
5704
+ return null;
5705
+ }
5706
+ const { content } = urlInfo;
5707
+ const magicSource = createMagicSource(content);
5708
+ for (const { from, to } of patches) {
5709
+ const start = content.indexOf(from);
5710
+ const occurrenceCount =
5711
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
5712
+ if (occurrenceCount !== 1) {
5713
+ const fileRelativeUrl = urlToRelativeUrl(
5714
+ urlInfo.url,
5715
+ urlInfo.context.rootDirectoryUrl,
5716
+ );
5717
+ throw new Error(
5718
+ `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.`,
5719
+ );
5720
+ }
5721
+ magicSource.replace({
5722
+ start,
5723
+ end: start + from.length,
5724
+ replacement: to,
5725
+ });
5726
+ }
5727
+ return magicSource.toContentAndSourcemap();
5728
+ },
5729
+ };
5730
+ return [patchesPlugin];
5731
+ };
5732
+
5733
+ const assertPatches = (patches, key) => {
5734
+ if (!Array.isArray(patches)) {
5735
+ throw new TypeError(
5736
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
5737
+ );
5738
+ }
5739
+ for (const patch of patches) {
5740
+ if (
5741
+ !patch ||
5742
+ typeof patch.from !== "string" ||
5743
+ patch.from === "" ||
5744
+ typeof patch.to !== "string"
5745
+ ) {
5746
+ throw new TypeError(
5747
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
5748
+ );
5749
+ }
5750
+ }
5751
+ };
5752
+
5753
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
5754
+ // else names a path inside a package, looked up in node_modules
5755
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
5756
+ if (
5757
+ key.startsWith("./") ||
5758
+ key.startsWith("../") ||
5759
+ key.startsWith("/") ||
5760
+ key.startsWith("file:") ||
5761
+ key.startsWith("*")
5762
+ ) {
5763
+ return key;
5764
+ }
5765
+ const segments = key.split("/");
5766
+ const packageName = key.startsWith("@")
5767
+ ? `${segments[0]}/${segments[1]}`
5768
+ : segments[0];
5769
+ const pathInsidePackage = key.slice(packageName.length);
5770
+ let directoryUrl = new URL(rootDirectoryUrl);
5771
+ while (true) {
5772
+ const packageDirectoryUrl = new URL(
5773
+ `./node_modules/${packageName}/`,
5774
+ directoryUrl,
5775
+ );
5776
+ if (existsSync(packageDirectoryUrl)) {
5777
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
5778
+ }
5779
+ const parentDirectoryUrl = new URL("../", directoryUrl);
5780
+ if (parentDirectoryUrl.href === directoryUrl.href) {
5781
+ throw new Error(
5782
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
5783
+ );
5784
+ }
5785
+ directoryUrl = parentDirectoryUrl;
5786
+ }
5787
+ };
5788
+
5655
5789
  const jsenvPluginInliningAsDataUrl = () => {
5656
5790
  return {
5657
5791
  name: "jsenv:inlining_as_data_url",
@@ -8101,6 +8235,7 @@ const getCorePlugins = ({
8101
8235
  directoryListing = true,
8102
8236
  directoryReferenceEffect,
8103
8237
  supervisor,
8238
+ patches,
8104
8239
  injections,
8105
8240
  transpilation = true,
8106
8241
  inlining = true,
@@ -8142,6 +8277,9 @@ const getCorePlugins = ({
8142
8277
  ...(packageBundle
8143
8278
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
8144
8279
  : []),
8280
+ // before everything else: what the other plugins read must be the
8281
+ // patched file
8282
+ ...jsenvPluginPatches(patches),
8145
8283
  // before reference analysis: an url written by an injection must hold its
8146
8284
  // final value when references are analyzed
8147
8285
  jsenvPluginInjections(injections),
@@ -12278,6 +12416,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
12278
12416
  * @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
12279
12417
  * @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
12280
12418
  * @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
12419
+ * @param {object} [params.patches] - Text patches applied to files as they are served, as `{ file: [{ from, to }] }`. A key is a url pattern relative to sourceDirectoryUrl (`"./main.js"`) or a path inside a package (`"preact/dist/preact.mjs"`, found in node_modules the way node does). Each `from` must occur exactly once in the file, otherwise the file fails to load and says which patch did not apply.
12281
12420
  * @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
12282
12421
  * @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
12283
12422
  * @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
@@ -12339,6 +12478,7 @@ const startDevServer = async ({
12339
12478
  magicExtensions,
12340
12479
  magicDirectoryIndex,
12341
12480
  directoryListing,
12481
+ patches,
12342
12482
  injections,
12343
12483
  transpilation,
12344
12484
  cacheControl = true,
@@ -12506,6 +12646,7 @@ const startDevServer = async ({
12506
12646
  magicDirectoryIndex,
12507
12647
  directoryListing,
12508
12648
  supervisor,
12649
+ patches,
12509
12650
  injections,
12510
12651
  transpilation,
12511
12652
  spa,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.5.20",
3
+ "version": "41.5.21",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -149,6 +149,17 @@ import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
149
149
  * }
150
150
  *
151
151
  * See startDevServer "ribbon" param for the full list of options.
152
+ * @param {object} [entryPoint.patches]
153
+ * Text patches applied to files as they are built, as { file: [{ from, to }] }.
154
+ * A key is a url pattern relative to sourceDirectoryUrl ("./main.js") or a path
155
+ * inside a package ("preact/dist/preact.mjs", found in node_modules the way node does):
156
+ *
157
+ * patches: {
158
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
159
+ * }
160
+ *
161
+ * Each `from` must occur exactly once in the file, otherwise the build fails
162
+ * and says which patch did not apply.
152
163
  * @param {object} [entryPoint.injections]
153
164
  * Values to inject into files, as { urlPattern: getInjections }.
154
165
  * Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
@@ -1016,6 +1027,7 @@ const entryPointDefaultParams = {
1016
1027
  directoryReferenceEffect: undefined,
1017
1028
  scenarioPlaceholders: undefined,
1018
1029
  ribbon: false,
1030
+ patches: undefined,
1019
1031
  injections: undefined,
1020
1032
  transpilation: {},
1021
1033
  preserveComments: undefined,
@@ -1072,6 +1084,7 @@ const prepareEntryPointBuild = async (
1072
1084
  directoryReferenceEffect,
1073
1085
  scenarioPlaceholders,
1074
1086
  ribbon,
1087
+ patches,
1075
1088
  injections,
1076
1089
  transpilation,
1077
1090
  preserveComments,
@@ -1240,6 +1253,7 @@ const prepareEntryPointBuild = async (
1240
1253
  magicExtensions,
1241
1254
  magicDirectoryIndex,
1242
1255
  directoryReferenceEffect,
1256
+ patches,
1243
1257
  injections,
1244
1258
  transpilation: {
1245
1259
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -60,6 +60,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
60
60
  * @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
61
61
  * @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
62
62
  * @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
63
+ * @param {object} [params.patches] - Text patches applied to files as they are served, as `{ file: [{ from, to }] }`. A key is a url pattern relative to sourceDirectoryUrl (`"./main.js"`) or a path inside a package (`"preact/dist/preact.mjs"`, found in node_modules the way node does). Each `from` must occur exactly once in the file, otherwise the file fails to load and says which patch did not apply.
63
64
  * @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
64
65
  * @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
65
66
  * @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
@@ -121,6 +122,7 @@ export const startDevServer = async ({
121
122
  magicExtensions,
122
123
  magicDirectoryIndex,
123
124
  directoryListing,
125
+ patches,
124
126
  injections,
125
127
  transpilation,
126
128
  cacheControl = true,
@@ -289,6 +291,7 @@ export const startDevServer = async ({
289
291
  magicDirectoryIndex,
290
292
  directoryListing,
291
293
  supervisor,
294
+ patches,
292
295
  injections,
293
296
  transpilation,
294
297
  spa,
@@ -0,0 +1,137 @@
1
+ /*
2
+ * Text patches applied to files as they are served and built, keyed by file:
3
+ *
4
+ * patches: {
5
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
6
+ * }
7
+ *
8
+ * A key is a url pattern relative to the root directory ("./main.js",
9
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
10
+ * walking up from the root directory into node_modules, the way node does,
11
+ * so the key holds wherever the package manager hoists the package.
12
+ *
13
+ * Every `from` must occur exactly once in the file, otherwise the file fails
14
+ * to cook and says which patch did not apply: a dependency update that moved
15
+ * the patched code must be looked at, never silently unpatched.
16
+ */
17
+
18
+ import { createMagicSource } from "@jsenv/sourcemap";
19
+ import { URL_META } from "@jsenv/url-meta";
20
+ import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
21
+ import { existsSync } from "node:fs";
22
+
23
+ export const jsenvPluginPatches = (rawPatches) => {
24
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
25
+ return [];
26
+ }
27
+ let findPatches;
28
+ const patchesPlugin = {
29
+ name: "jsenv:patches",
30
+ appliesDuring: "*",
31
+ init: (context) => {
32
+ const { rootDirectoryUrl } = context;
33
+ const patchesByPattern = {};
34
+ for (const key of Object.keys(rawPatches)) {
35
+ const patches = rawPatches[key];
36
+ assertPatches(patches, key);
37
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
38
+ }
39
+ const associations = URL_META.resolveAssociations(
40
+ { patches: patchesByPattern },
41
+ rootDirectoryUrl,
42
+ );
43
+ findPatches = (url) => {
44
+ const { patches } = URL_META.applyAssociations({
45
+ url: asUrlWithoutSearch(url),
46
+ associations,
47
+ });
48
+ return patches;
49
+ };
50
+ },
51
+ transformUrlContent: (urlInfo) => {
52
+ const patches = findPatches(urlInfo.url);
53
+ if (!patches) {
54
+ return null;
55
+ }
56
+ const { content } = urlInfo;
57
+ const magicSource = createMagicSource(content);
58
+ for (const { from, to } of patches) {
59
+ const start = content.indexOf(from);
60
+ const occurrenceCount =
61
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
62
+ if (occurrenceCount !== 1) {
63
+ const fileRelativeUrl = urlToRelativeUrl(
64
+ urlInfo.url,
65
+ urlInfo.context.rootDirectoryUrl,
66
+ );
67
+ throw new Error(
68
+ `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.`,
69
+ );
70
+ }
71
+ magicSource.replace({
72
+ start,
73
+ end: start + from.length,
74
+ replacement: to,
75
+ });
76
+ }
77
+ return magicSource.toContentAndSourcemap();
78
+ },
79
+ };
80
+ return [patchesPlugin];
81
+ };
82
+
83
+ const assertPatches = (patches, key) => {
84
+ if (!Array.isArray(patches)) {
85
+ throw new TypeError(
86
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
87
+ );
88
+ }
89
+ for (const patch of patches) {
90
+ if (
91
+ !patch ||
92
+ typeof patch.from !== "string" ||
93
+ patch.from === "" ||
94
+ typeof patch.to !== "string"
95
+ ) {
96
+ throw new TypeError(
97
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
98
+ );
99
+ }
100
+ }
101
+ };
102
+
103
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
104
+ // else names a path inside a package, looked up in node_modules
105
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
106
+ if (
107
+ key.startsWith("./") ||
108
+ key.startsWith("../") ||
109
+ key.startsWith("/") ||
110
+ key.startsWith("file:") ||
111
+ key.startsWith("*")
112
+ ) {
113
+ return key;
114
+ }
115
+ const segments = key.split("/");
116
+ const packageName = key.startsWith("@")
117
+ ? `${segments[0]}/${segments[1]}`
118
+ : segments[0];
119
+ const pathInsidePackage = key.slice(packageName.length);
120
+ let directoryUrl = new URL(rootDirectoryUrl);
121
+ while (true) {
122
+ const packageDirectoryUrl = new URL(
123
+ `./node_modules/${packageName}/`,
124
+ directoryUrl,
125
+ );
126
+ if (existsSync(packageDirectoryUrl)) {
127
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
128
+ }
129
+ const parentDirectoryUrl = new URL("../", directoryUrl);
130
+ if (parentDirectoryUrl.href === directoryUrl.href) {
131
+ throw new Error(
132
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
133
+ );
134
+ }
135
+ directoryUrl = parentDirectoryUrl;
136
+ }
137
+ };
@@ -11,6 +11,7 @@ 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";
14
15
  import { jsenvPluginInlining } from "./inlining/jsenv_plugin_inlining.js";
15
16
  import { jsenvPluginCommonJsGlobals } from "./commonjs_globals/jsenv_plugin_commonjs_globals.js";
16
17
  import { jsenvPluginImportMetaScenarios } from "./import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js";
@@ -51,6 +52,7 @@ export const getCorePlugins = ({
51
52
  directoryListing = true,
52
53
  directoryReferenceEffect,
53
54
  supervisor,
55
+ patches,
54
56
  injections,
55
57
  transpilation = true,
56
58
  inlining = true,
@@ -92,6 +94,9 @@ export const getCorePlugins = ({
92
94
  ...(packageBundle
93
95
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
94
96
  : []),
97
+ // before everything else: what the other plugins read must be the
98
+ // patched file
99
+ ...jsenvPluginPatches(patches),
95
100
  // before reference analysis: an url written by an injection must hold its
96
101
  // final value when references are analyzed
97
102
  jsenvPluginInjections(injections),