@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.
package/dist/build/build.js
CHANGED
|
@@ -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
package/src/build/build.js
CHANGED
|
@@ -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
|
+
};
|
package/src/plugins/plugins.js
CHANGED
|
@@ -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),
|