@jsenv/core 41.4.5 → 41.4.6
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 +144 -77
- package/dist/start_dev_server/start_dev_server.js +94 -50
- package/package.json +1 -1
- package/src/build/build.js +5 -18
- package/src/build/build_specifier_manager.js +42 -4
- package/src/helpers/bare_specifier.js +18 -0
- package/src/kitchen/errors.js +5 -4
- package/src/kitchen/url_graph/url_graph.js +3 -0
- package/src/kitchen/url_graph/url_info_injections.js +11 -4
- package/src/plugins/injections/jsenv_plugin_injections.js +53 -6
- package/src/plugins/plugins.js +3 -1
- package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -17
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +1 -17
package/dist/build/build.js
CHANGED
|
@@ -628,9 +628,10 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
628
628
|
return getFirstReferenceInProject(firstReference);
|
|
629
629
|
};
|
|
630
630
|
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// (the key is free-form), tell the file it comes
|
|
631
|
+
// Injections write urls in html attributes before references are analyzed, so an url
|
|
632
|
+
// that still cannot be resolved may be a placeholder no injection replaced. Rather than
|
|
633
|
+
// guessing what a placeholder looks like (the key is free-form), tell the file it comes
|
|
634
|
+
// from: injections are configured for it.
|
|
634
635
|
const detailsFromInjectionsOnOwner = (reference) => {
|
|
635
636
|
if (!reference) {
|
|
636
637
|
return {};
|
|
@@ -649,7 +650,7 @@ const detailsFromInjectionsOnOwner = (reference) => {
|
|
|
649
650
|
return {};
|
|
650
651
|
}
|
|
651
652
|
return {
|
|
652
|
-
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, add "jsenv-ignore" so jsenv leaves that url alone:
|
|
653
|
+
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, check the placeholder spelling, or add "jsenv-ignore" so jsenv leaves that url alone:
|
|
653
654
|
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
654
655
|
};
|
|
655
656
|
};
|
|
@@ -2101,6 +2102,9 @@ const createUrlInfo = (url, context) => {
|
|
|
2101
2102
|
contentFinalized: false,
|
|
2102
2103
|
contentSideEffects: [],
|
|
2103
2104
|
contentInjections: {},
|
|
2105
|
+
// placeholders already consumed somewhere else than the content (in a specifier),
|
|
2106
|
+
// so that not finding them in the content is not worth a warning
|
|
2107
|
+
contentInjectionUsedKeySet: new Set(),
|
|
2104
2108
|
|
|
2105
2109
|
sourcemap: null,
|
|
2106
2110
|
sourcemapIsWrong: false,
|
|
@@ -2413,6 +2417,13 @@ const INJECTIONS = {
|
|
|
2413
2417
|
},
|
|
2414
2418
|
};
|
|
2415
2419
|
|
|
2420
|
+
const readInjectionValue = (injection) => {
|
|
2421
|
+
if (injection && injection[injectionSymbol]) {
|
|
2422
|
+
return injection.value;
|
|
2423
|
+
}
|
|
2424
|
+
return injection;
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2416
2427
|
const isPlaceholderInjection = (value) => {
|
|
2417
2428
|
return (
|
|
2418
2429
|
!value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
|
|
@@ -2486,7 +2497,7 @@ const injectPlaceholderReplacements = (
|
|
|
2486
2497
|
for (const { key, isOptional, value } of placeholderReplacements) {
|
|
2487
2498
|
let index = content.indexOf(key);
|
|
2488
2499
|
if (index === -1) {
|
|
2489
|
-
if (!isOptional) {
|
|
2500
|
+
if (!isOptional && !urlInfo.contentInjectionUsedKeySet.has(key)) {
|
|
2490
2501
|
urlInfo.context.logger.warn(
|
|
2491
2502
|
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
2492
2503
|
--- suggestion a ---
|
|
@@ -2511,7 +2522,7 @@ return {
|
|
|
2511
2522
|
magicSource.replace({
|
|
2512
2523
|
start,
|
|
2513
2524
|
end,
|
|
2514
|
-
replacement: asReplacement(value, urlInfo),
|
|
2525
|
+
replacement: asReplacement(value, urlInfo.type),
|
|
2515
2526
|
});
|
|
2516
2527
|
index = content.indexOf(key, end);
|
|
2517
2528
|
}
|
|
@@ -2522,8 +2533,8 @@ return {
|
|
|
2522
2533
|
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
2523
2534
|
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
2524
2535
|
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
2525
|
-
const asReplacement = (value,
|
|
2526
|
-
if (
|
|
2536
|
+
const asReplacement = (value, type) => {
|
|
2537
|
+
if (type === "js_classic" || type === "js_module") {
|
|
2527
2538
|
return JSON.stringify(value, null, " ");
|
|
2528
2539
|
}
|
|
2529
2540
|
if (typeof value === "string") {
|
|
@@ -4675,6 +4686,25 @@ const testAppliesDuring = (plugin, kitchen) => {
|
|
|
4675
4686
|
);
|
|
4676
4687
|
};
|
|
4677
4688
|
|
|
4689
|
+
// A bare specifier ("preact", "@jsenv/core/x.js") is resolved by node esm resolution,
|
|
4690
|
+
// everything else ("/a.js", "./a.js", "http://example.com/a.js") by url resolution
|
|
4691
|
+
const isBareSpecifier = (specifier) => {
|
|
4692
|
+
if (
|
|
4693
|
+
specifier[0] === "/" ||
|
|
4694
|
+
specifier.startsWith("./") ||
|
|
4695
|
+
specifier.startsWith("../")
|
|
4696
|
+
) {
|
|
4697
|
+
return false;
|
|
4698
|
+
}
|
|
4699
|
+
try {
|
|
4700
|
+
// eslint-disable-next-line no-new
|
|
4701
|
+
new URL(specifier);
|
|
4702
|
+
return false;
|
|
4703
|
+
} catch {
|
|
4704
|
+
return true;
|
|
4705
|
+
}
|
|
4706
|
+
};
|
|
4707
|
+
|
|
4678
4708
|
/*
|
|
4679
4709
|
* https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/css/src/CSSTransformer.js
|
|
4680
4710
|
*/
|
|
@@ -5616,7 +5646,7 @@ const parseAndTransformJsReferences = async (
|
|
|
5616
5646
|
let filenameHint;
|
|
5617
5647
|
if (
|
|
5618
5648
|
externalReferenceInfo.subtype === "import_dynamic" &&
|
|
5619
|
-
isBareSpecifier
|
|
5649
|
+
isBareSpecifier(externalReferenceInfo.specifier)
|
|
5620
5650
|
) {
|
|
5621
5651
|
filenameHint = `${externalReferenceInfo.specifier}.js`;
|
|
5622
5652
|
}
|
|
@@ -5706,23 +5736,6 @@ const parseAndTransformJsReferences = async (
|
|
|
5706
5736
|
return { content, sourcemap };
|
|
5707
5737
|
};
|
|
5708
5738
|
|
|
5709
|
-
const isBareSpecifier$2 = (specifier) => {
|
|
5710
|
-
if (
|
|
5711
|
-
specifier[0] === "/" ||
|
|
5712
|
-
specifier.startsWith("./") ||
|
|
5713
|
-
specifier.startsWith("../")
|
|
5714
|
-
) {
|
|
5715
|
-
return false;
|
|
5716
|
-
}
|
|
5717
|
-
try {
|
|
5718
|
-
// eslint-disable-next-line no-new
|
|
5719
|
-
new URL(specifier);
|
|
5720
|
-
return false;
|
|
5721
|
-
} catch {
|
|
5722
|
-
return true;
|
|
5723
|
-
}
|
|
5724
|
-
};
|
|
5725
|
-
|
|
5726
5739
|
const jsenvPluginReferenceExpectedTypes = () => {
|
|
5727
5740
|
const redirectJsReference = (reference) => {
|
|
5728
5741
|
const urlObject = new URL(reference.url);
|
|
@@ -6113,7 +6126,7 @@ const createBuildPackageConditions = (
|
|
|
6113
6126
|
for (const key of keys) {
|
|
6114
6127
|
const associatedValue = packageConditionsConfig[key];
|
|
6115
6128
|
|
|
6116
|
-
if (!isBareSpecifier
|
|
6129
|
+
if (!isBareSpecifier(key)) {
|
|
6117
6130
|
const url = new URL(key, rootDirectoryUrl);
|
|
6118
6131
|
associationsRaw[url] = associatedValue;
|
|
6119
6132
|
continue;
|
|
@@ -6148,7 +6161,7 @@ const createBuildPackageConditions = (
|
|
|
6148
6161
|
);
|
|
6149
6162
|
resolveConditionsFromSpecifier = (specifier, importer, { resolver }) => {
|
|
6150
6163
|
let associatedValue;
|
|
6151
|
-
if (isBareSpecifier
|
|
6164
|
+
if (isBareSpecifier(specifier)) {
|
|
6152
6165
|
const { url } = resolver({
|
|
6153
6166
|
specifier,
|
|
6154
6167
|
parentUrl: importer,
|
|
@@ -6173,7 +6186,7 @@ const createBuildPackageConditions = (
|
|
|
6173
6186
|
const nodeRuntimeEnabled = Object.keys(runtimeCompat).includes("node");
|
|
6174
6187
|
// https://nodejs.org/api/esm.html#resolver-algorithm-specification
|
|
6175
6188
|
const devResolver = (specifier, importer, { resolver }) => {
|
|
6176
|
-
if (isBareSpecifier
|
|
6189
|
+
if (isBareSpecifier(specifier)) {
|
|
6177
6190
|
const { url } = resolver({
|
|
6178
6191
|
specifier,
|
|
6179
6192
|
parentUrl: importer,
|
|
@@ -6237,7 +6250,7 @@ const createBuildPackageConditions = (
|
|
|
6237
6250
|
const associations = URL_META.resolveAssociations(
|
|
6238
6251
|
{ applies: value },
|
|
6239
6252
|
(pattern) => {
|
|
6240
|
-
if (isBareSpecifier
|
|
6253
|
+
if (isBareSpecifier(pattern)) {
|
|
6241
6254
|
try {
|
|
6242
6255
|
if (pattern.endsWith("/")) {
|
|
6243
6256
|
// avoid package path not exported
|
|
@@ -6260,7 +6273,7 @@ const createBuildPackageConditions = (
|
|
|
6260
6273
|
},
|
|
6261
6274
|
);
|
|
6262
6275
|
customResolver = (specifier, importer, { resolver }) => {
|
|
6263
|
-
if (isBareSpecifier
|
|
6276
|
+
if (isBareSpecifier(specifier)) {
|
|
6264
6277
|
const { url } = resolver({
|
|
6265
6278
|
specifier,
|
|
6266
6279
|
parentUrl: importer,
|
|
@@ -6392,23 +6405,6 @@ const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
|
6392
6405
|
};
|
|
6393
6406
|
};
|
|
6394
6407
|
|
|
6395
|
-
const isBareSpecifier$1 = (specifier) => {
|
|
6396
|
-
if (
|
|
6397
|
-
specifier[0] === "/" ||
|
|
6398
|
-
specifier.startsWith("./") ||
|
|
6399
|
-
specifier.startsWith("../")
|
|
6400
|
-
) {
|
|
6401
|
-
return false;
|
|
6402
|
-
}
|
|
6403
|
-
try {
|
|
6404
|
-
// eslint-disable-next-line no-new
|
|
6405
|
-
new URL(specifier);
|
|
6406
|
-
return false;
|
|
6407
|
-
} catch {
|
|
6408
|
-
return true;
|
|
6409
|
-
}
|
|
6410
|
-
};
|
|
6411
|
-
|
|
6412
6408
|
const jsenvPluginNodeEsmResolution = ({
|
|
6413
6409
|
packageDirectory,
|
|
6414
6410
|
resolutionConfig = {},
|
|
@@ -7676,6 +7672,7 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
7676
7672
|
}
|
|
7677
7673
|
return null;
|
|
7678
7674
|
};
|
|
7675
|
+
const injectionsForUrlInfoMap = new WeakMap();
|
|
7679
7676
|
let getInjections = null;
|
|
7680
7677
|
|
|
7681
7678
|
return {
|
|
@@ -7694,8 +7691,8 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
7694
7691
|
});
|
|
7695
7692
|
return injectionsGetter;
|
|
7696
7693
|
};
|
|
7697
|
-
//
|
|
7698
|
-
//
|
|
7694
|
+
// errors and the build read this to know an unresolved url may come from
|
|
7695
|
+
// an injection, and word what they report accordingly
|
|
7699
7696
|
context.hasInjections = (url) => {
|
|
7700
7697
|
return Boolean(findInjectionsGetterForUrl(url));
|
|
7701
7698
|
};
|
|
@@ -7749,16 +7746,61 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
7749
7746
|
contentInjections: defaultInjections,
|
|
7750
7747
|
};
|
|
7751
7748
|
}
|
|
7749
|
+
injectionsForUrlInfoMap.set(urlInfo, injections);
|
|
7752
7750
|
return {
|
|
7753
|
-
contentInjections: {
|
|
7754
|
-
...defaultInjections,
|
|
7755
|
-
...injections,
|
|
7756
|
-
},
|
|
7751
|
+
contentInjections: { ...defaultInjections, ...injections },
|
|
7757
7752
|
};
|
|
7758
7753
|
},
|
|
7754
|
+
// The content still holds the placeholders when references are analyzed: they are
|
|
7755
|
+
// replaced at the very end, once the type of every inline content is known.
|
|
7756
|
+
// A specifier must not wait for that, it would resolve "__BACKEND_URL__/users/me"
|
|
7757
|
+
// as a file sitting next to the document. Only the placeholder is resolved here,
|
|
7758
|
+
// then the specifier goes to whoever resolves this kind of url (node esm, web, ...)
|
|
7759
|
+
resolveReference: (reference) => {
|
|
7760
|
+
const { ownerUrlInfo } = reference;
|
|
7761
|
+
const injections = injectionsForUrlInfoMap.get(ownerUrlInfo);
|
|
7762
|
+
if (!injections) {
|
|
7763
|
+
return null;
|
|
7764
|
+
}
|
|
7765
|
+
const { specifier, keySet } = injectIntoSpecifier(
|
|
7766
|
+
reference.specifier,
|
|
7767
|
+
injections,
|
|
7768
|
+
);
|
|
7769
|
+
if (keySet.size === 0) {
|
|
7770
|
+
return null;
|
|
7771
|
+
}
|
|
7772
|
+
reference.specifier = specifier;
|
|
7773
|
+
for (const key of keySet) {
|
|
7774
|
+
// rewriting the specifier takes the placeholder out of the content,
|
|
7775
|
+
// so the injection step must not expect to find it there
|
|
7776
|
+
ownerUrlInfo.contentInjectionUsedKeySet.add(key);
|
|
7777
|
+
}
|
|
7778
|
+
return null;
|
|
7779
|
+
},
|
|
7759
7780
|
};
|
|
7760
7781
|
};
|
|
7761
7782
|
|
|
7783
|
+
const injectIntoSpecifier = (specifier, injections) => {
|
|
7784
|
+
let specifierInjected = specifier;
|
|
7785
|
+
const keySet = new Set();
|
|
7786
|
+
for (const key of Object.keys(injections)) {
|
|
7787
|
+
if (!specifierInjected.includes(key)) {
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
const injection = injections[key];
|
|
7791
|
+
if (!isPlaceholderInjection(injection)) {
|
|
7792
|
+
continue;
|
|
7793
|
+
}
|
|
7794
|
+
const value = readInjectionValue(injection);
|
|
7795
|
+
if (typeof value !== "string") {
|
|
7796
|
+
continue;
|
|
7797
|
+
}
|
|
7798
|
+
specifierInjected = specifierInjected.replaceAll(key, value);
|
|
7799
|
+
keySet.add(key);
|
|
7800
|
+
}
|
|
7801
|
+
return { specifier: specifierInjected, keySet };
|
|
7802
|
+
};
|
|
7803
|
+
|
|
7762
7804
|
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
7763
7805
|
// and inherits its injections, with two adjustments:
|
|
7764
7806
|
// - a global belongs to the file itself, injecting it into each inline content would
|
|
@@ -9833,8 +9875,10 @@ const getCorePlugins = ({
|
|
|
9833
9875
|
...(packageBundle
|
|
9834
9876
|
? [jsenvPluginWorkspaceBundle({ packageDirectory })]
|
|
9835
9877
|
: []),
|
|
9836
|
-
|
|
9878
|
+
// before reference analysis: an url written by an injection must hold its
|
|
9879
|
+
// final value when references are analyzed
|
|
9837
9880
|
jsenvPluginInjections(injections),
|
|
9881
|
+
jsenvPluginReferenceAnalysis(referenceAnalysis),
|
|
9838
9882
|
jsenvPluginTranspilation(transpilation),
|
|
9839
9883
|
// "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
|
|
9840
9884
|
...(inlining ? [jsenvPluginInlining()] : []),
|
|
@@ -11177,7 +11221,7 @@ const createBuildSpecifierManager = ({
|
|
|
11177
11221
|
|
|
11178
11222
|
prepareResyncResourceHints: ({ registerHtmlRefine }) => {
|
|
11179
11223
|
const hintToInjectMap = new Map();
|
|
11180
|
-
registerHtmlRefine((htmlAst, { registerHtmlMutation }) => {
|
|
11224
|
+
registerHtmlRefine((htmlAst, { registerHtmlMutation, htmlUrlInfo }) => {
|
|
11181
11225
|
visitHtmlNodes(htmlAst, {
|
|
11182
11226
|
link: (node) => {
|
|
11183
11227
|
if (getHtmlNodeAttribute(node, "jsenv-ignore") !== undefined) {
|
|
@@ -11201,12 +11245,39 @@ const createBuildSpecifierManager = ({
|
|
|
11201
11245
|
return;
|
|
11202
11246
|
}
|
|
11203
11247
|
const rawUrl = href;
|
|
11248
|
+
if (targetsAFileThatDoesNotExist(rawUrl)) {
|
|
11249
|
+
// this hint never designated anything, so nothing can explain its
|
|
11250
|
+
// removal; taking away markup written by hand on a guess is worse
|
|
11251
|
+
// than leaving it there
|
|
11252
|
+
logger.warn(
|
|
11253
|
+
createDetailedMessage(
|
|
11254
|
+
`${UNICODE.WARNING} resource hint kept as is: "${href}" cannot be resolved`,
|
|
11255
|
+
{
|
|
11256
|
+
"html file": htmlUrlInfo.url,
|
|
11257
|
+
...(rawKitchen.context.hasInjections
|
|
11258
|
+
? {
|
|
11259
|
+
suggestion: `when that url is written by an injection, jsenv resolves it in html attributes before analyzing references; check the placeholder spelling`,
|
|
11260
|
+
}
|
|
11261
|
+
: {}),
|
|
11262
|
+
},
|
|
11263
|
+
),
|
|
11264
|
+
);
|
|
11265
|
+
// the build url means nothing for something jsenv could not resolve,
|
|
11266
|
+
// and it would leak a local path into the build
|
|
11267
|
+
registerHtmlMutation(() => {
|
|
11268
|
+
setHtmlNodeAttributes(node, {
|
|
11269
|
+
href: urlToRelativeUrl(rawUrl, htmlUrlInfo.url),
|
|
11270
|
+
});
|
|
11271
|
+
});
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11204
11274
|
const finalUrl = internalRedirections.get(rawUrl) || rawUrl;
|
|
11205
11275
|
const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
|
|
11206
11276
|
if (!urlInfo) {
|
|
11207
|
-
if (
|
|
11208
|
-
//
|
|
11209
|
-
//
|
|
11277
|
+
if (!href.startsWith("file:")) {
|
|
11278
|
+
// the hint designates a resource jsenv does not own: an origin for
|
|
11279
|
+
// preconnect/dns-prefetch, a remote file for the others. The author
|
|
11280
|
+
// knows what the page will request at runtime, keep it as-is.
|
|
11210
11281
|
return;
|
|
11211
11282
|
}
|
|
11212
11283
|
logger.warn(
|
|
@@ -11651,6 +11722,16 @@ const asBuildUrlVersioned = ({
|
|
|
11651
11722
|
return `${buildDirectoryUrl}${pathname}${search}${hash}`;
|
|
11652
11723
|
};
|
|
11653
11724
|
|
|
11725
|
+
const targetsAFileThatDoesNotExist = (url) => {
|
|
11726
|
+
if (!url.startsWith("file:")) {
|
|
11727
|
+
return false;
|
|
11728
|
+
}
|
|
11729
|
+
const urlObject = new URL(url);
|
|
11730
|
+
urlObject.search = "";
|
|
11731
|
+
urlObject.hash = "";
|
|
11732
|
+
return !existsSync(urlObject);
|
|
11733
|
+
};
|
|
11734
|
+
|
|
11654
11735
|
// import { ANSI } from "@jsenv/humanize";
|
|
11655
11736
|
|
|
11656
11737
|
const createBuildUrlsGenerator = ({
|
|
@@ -13374,7 +13455,10 @@ const prepareEntryPointBuild = async (
|
|
|
13374
13455
|
const registerHtmlMutation = (callback) => {
|
|
13375
13456
|
htmlMutationCallbackSet.add(callback);
|
|
13376
13457
|
};
|
|
13377
|
-
htmlRefine(htmlAst, {
|
|
13458
|
+
htmlRefine(htmlAst, {
|
|
13459
|
+
registerHtmlMutation,
|
|
13460
|
+
htmlUrlInfo: urlInfo,
|
|
13461
|
+
});
|
|
13378
13462
|
for (const htmlMutationCallback of htmlMutationCallbackSet) {
|
|
13379
13463
|
htmlMutationCallback();
|
|
13380
13464
|
}
|
|
@@ -13459,21 +13543,4 @@ const prepareEntryPointBuild = async (
|
|
|
13459
13543
|
};
|
|
13460
13544
|
};
|
|
13461
13545
|
|
|
13462
|
-
const isBareSpecifier = (specifier) => {
|
|
13463
|
-
if (
|
|
13464
|
-
specifier[0] === "/" ||
|
|
13465
|
-
specifier.startsWith("./") ||
|
|
13466
|
-
specifier.startsWith("../")
|
|
13467
|
-
) {
|
|
13468
|
-
return false;
|
|
13469
|
-
}
|
|
13470
|
-
try {
|
|
13471
|
-
// eslint-disable-next-line no-new
|
|
13472
|
-
new URL(specifier);
|
|
13473
|
-
return false;
|
|
13474
|
-
} catch {
|
|
13475
|
-
return true;
|
|
13476
|
-
}
|
|
13477
|
-
};
|
|
13478
|
-
|
|
13479
13546
|
export { build };
|
|
@@ -2208,6 +2208,25 @@ const isWebWorkerUrlInfo = (urlInfo) => {
|
|
|
2208
2208
|
// return false
|
|
2209
2209
|
// }
|
|
2210
2210
|
|
|
2211
|
+
// A bare specifier ("preact", "@jsenv/core/x.js") is resolved by node esm resolution,
|
|
2212
|
+
// everything else ("/a.js", "./a.js", "http://example.com/a.js") by url resolution
|
|
2213
|
+
const isBareSpecifier = (specifier) => {
|
|
2214
|
+
if (
|
|
2215
|
+
specifier[0] === "/" ||
|
|
2216
|
+
specifier.startsWith("./") ||
|
|
2217
|
+
specifier.startsWith("../")
|
|
2218
|
+
) {
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
try {
|
|
2222
|
+
// eslint-disable-next-line no-new
|
|
2223
|
+
new URL(specifier);
|
|
2224
|
+
return false;
|
|
2225
|
+
} catch {
|
|
2226
|
+
return true;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2211
2230
|
const jsenvPluginJsReferenceAnalysis = ({ inlineContent }) => {
|
|
2212
2231
|
return [
|
|
2213
2232
|
{
|
|
@@ -2304,7 +2323,7 @@ const parseAndTransformJsReferences = async (
|
|
|
2304
2323
|
let filenameHint;
|
|
2305
2324
|
if (
|
|
2306
2325
|
externalReferenceInfo.subtype === "import_dynamic" &&
|
|
2307
|
-
isBareSpecifier
|
|
2326
|
+
isBareSpecifier(externalReferenceInfo.specifier)
|
|
2308
2327
|
) {
|
|
2309
2328
|
filenameHint = `${externalReferenceInfo.specifier}.js`;
|
|
2310
2329
|
}
|
|
@@ -2394,23 +2413,6 @@ const parseAndTransformJsReferences = async (
|
|
|
2394
2413
|
return { content, sourcemap };
|
|
2395
2414
|
};
|
|
2396
2415
|
|
|
2397
|
-
const isBareSpecifier$1 = (specifier) => {
|
|
2398
|
-
if (
|
|
2399
|
-
specifier[0] === "/" ||
|
|
2400
|
-
specifier.startsWith("./") ||
|
|
2401
|
-
specifier.startsWith("../")
|
|
2402
|
-
) {
|
|
2403
|
-
return false;
|
|
2404
|
-
}
|
|
2405
|
-
try {
|
|
2406
|
-
// eslint-disable-next-line no-new
|
|
2407
|
-
new URL(specifier);
|
|
2408
|
-
return false;
|
|
2409
|
-
} catch {
|
|
2410
|
-
return true;
|
|
2411
|
-
}
|
|
2412
|
-
};
|
|
2413
|
-
|
|
2414
2416
|
const jsenvPluginReferenceExpectedTypes = () => {
|
|
2415
2417
|
const redirectJsReference = (reference) => {
|
|
2416
2418
|
const urlObject = new URL(reference.url);
|
|
@@ -3080,23 +3082,6 @@ const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
|
3080
3082
|
};
|
|
3081
3083
|
};
|
|
3082
3084
|
|
|
3083
|
-
const isBareSpecifier = (specifier) => {
|
|
3084
|
-
if (
|
|
3085
|
-
specifier[0] === "/" ||
|
|
3086
|
-
specifier.startsWith("./") ||
|
|
3087
|
-
specifier.startsWith("../")
|
|
3088
|
-
) {
|
|
3089
|
-
return false;
|
|
3090
|
-
}
|
|
3091
|
-
try {
|
|
3092
|
-
// eslint-disable-next-line no-new
|
|
3093
|
-
new URL(specifier);
|
|
3094
|
-
return false;
|
|
3095
|
-
} catch {
|
|
3096
|
-
return true;
|
|
3097
|
-
}
|
|
3098
|
-
};
|
|
3099
|
-
|
|
3100
3085
|
const jsenvPluginNodeEsmResolution = ({
|
|
3101
3086
|
packageDirectory,
|
|
3102
3087
|
resolutionConfig = {},
|
|
@@ -4858,9 +4843,10 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
4858
4843
|
return getFirstReferenceInProject(firstReference);
|
|
4859
4844
|
};
|
|
4860
4845
|
|
|
4861
|
-
//
|
|
4862
|
-
//
|
|
4863
|
-
// (the key is free-form), tell the file it comes
|
|
4846
|
+
// Injections write urls in html attributes before references are analyzed, so an url
|
|
4847
|
+
// that still cannot be resolved may be a placeholder no injection replaced. Rather than
|
|
4848
|
+
// guessing what a placeholder looks like (the key is free-form), tell the file it comes
|
|
4849
|
+
// from: injections are configured for it.
|
|
4864
4850
|
const detailsFromInjectionsOnOwner = (reference) => {
|
|
4865
4851
|
if (!reference) {
|
|
4866
4852
|
return {};
|
|
@@ -4879,7 +4865,7 @@ const detailsFromInjectionsOnOwner = (reference) => {
|
|
|
4879
4865
|
return {};
|
|
4880
4866
|
}
|
|
4881
4867
|
return {
|
|
4882
|
-
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, add "jsenv-ignore" so jsenv leaves that url alone:
|
|
4868
|
+
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, check the placeholder spelling, or add "jsenv-ignore" so jsenv leaves that url alone:
|
|
4883
4869
|
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
4884
4870
|
};
|
|
4885
4871
|
};
|
|
@@ -5047,6 +5033,13 @@ const INJECTIONS = {
|
|
|
5047
5033
|
},
|
|
5048
5034
|
};
|
|
5049
5035
|
|
|
5036
|
+
const readInjectionValue = (injection) => {
|
|
5037
|
+
if (injection && injection[injectionSymbol]) {
|
|
5038
|
+
return injection.value;
|
|
5039
|
+
}
|
|
5040
|
+
return injection;
|
|
5041
|
+
};
|
|
5042
|
+
|
|
5050
5043
|
const isPlaceholderInjection = (value) => {
|
|
5051
5044
|
return (
|
|
5052
5045
|
!value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
|
|
@@ -5120,7 +5113,7 @@ const injectPlaceholderReplacements = (
|
|
|
5120
5113
|
for (const { key, isOptional, value } of placeholderReplacements) {
|
|
5121
5114
|
let index = content.indexOf(key);
|
|
5122
5115
|
if (index === -1) {
|
|
5123
|
-
if (!isOptional) {
|
|
5116
|
+
if (!isOptional && !urlInfo.contentInjectionUsedKeySet.has(key)) {
|
|
5124
5117
|
urlInfo.context.logger.warn(
|
|
5125
5118
|
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
5126
5119
|
--- suggestion a ---
|
|
@@ -5145,7 +5138,7 @@ return {
|
|
|
5145
5138
|
magicSource.replace({
|
|
5146
5139
|
start,
|
|
5147
5140
|
end,
|
|
5148
|
-
replacement: asReplacement(value, urlInfo),
|
|
5141
|
+
replacement: asReplacement(value, urlInfo.type),
|
|
5149
5142
|
});
|
|
5150
5143
|
index = content.indexOf(key, end);
|
|
5151
5144
|
}
|
|
@@ -5156,8 +5149,8 @@ return {
|
|
|
5156
5149
|
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
5157
5150
|
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
5158
5151
|
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
5159
|
-
const asReplacement = (value,
|
|
5160
|
-
if (
|
|
5152
|
+
const asReplacement = (value, type) => {
|
|
5153
|
+
if (type === "js_classic" || type === "js_module") {
|
|
5161
5154
|
return JSON.stringify(value, null, " ");
|
|
5162
5155
|
}
|
|
5163
5156
|
if (typeof value === "string") {
|
|
@@ -5235,6 +5228,7 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5235
5228
|
}
|
|
5236
5229
|
return null;
|
|
5237
5230
|
};
|
|
5231
|
+
const injectionsForUrlInfoMap = new WeakMap();
|
|
5238
5232
|
let getInjections = null;
|
|
5239
5233
|
|
|
5240
5234
|
return {
|
|
@@ -5253,8 +5247,8 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5253
5247
|
});
|
|
5254
5248
|
return injectionsGetter;
|
|
5255
5249
|
};
|
|
5256
|
-
//
|
|
5257
|
-
//
|
|
5250
|
+
// errors and the build read this to know an unresolved url may come from
|
|
5251
|
+
// an injection, and word what they report accordingly
|
|
5258
5252
|
context.hasInjections = (url) => {
|
|
5259
5253
|
return Boolean(findInjectionsGetterForUrl(url));
|
|
5260
5254
|
};
|
|
@@ -5308,16 +5302,61 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5308
5302
|
contentInjections: defaultInjections,
|
|
5309
5303
|
};
|
|
5310
5304
|
}
|
|
5305
|
+
injectionsForUrlInfoMap.set(urlInfo, injections);
|
|
5311
5306
|
return {
|
|
5312
|
-
contentInjections: {
|
|
5313
|
-
...defaultInjections,
|
|
5314
|
-
...injections,
|
|
5315
|
-
},
|
|
5307
|
+
contentInjections: { ...defaultInjections, ...injections },
|
|
5316
5308
|
};
|
|
5317
5309
|
},
|
|
5310
|
+
// The content still holds the placeholders when references are analyzed: they are
|
|
5311
|
+
// replaced at the very end, once the type of every inline content is known.
|
|
5312
|
+
// A specifier must not wait for that, it would resolve "__BACKEND_URL__/users/me"
|
|
5313
|
+
// as a file sitting next to the document. Only the placeholder is resolved here,
|
|
5314
|
+
// then the specifier goes to whoever resolves this kind of url (node esm, web, ...)
|
|
5315
|
+
resolveReference: (reference) => {
|
|
5316
|
+
const { ownerUrlInfo } = reference;
|
|
5317
|
+
const injections = injectionsForUrlInfoMap.get(ownerUrlInfo);
|
|
5318
|
+
if (!injections) {
|
|
5319
|
+
return null;
|
|
5320
|
+
}
|
|
5321
|
+
const { specifier, keySet } = injectIntoSpecifier(
|
|
5322
|
+
reference.specifier,
|
|
5323
|
+
injections,
|
|
5324
|
+
);
|
|
5325
|
+
if (keySet.size === 0) {
|
|
5326
|
+
return null;
|
|
5327
|
+
}
|
|
5328
|
+
reference.specifier = specifier;
|
|
5329
|
+
for (const key of keySet) {
|
|
5330
|
+
// rewriting the specifier takes the placeholder out of the content,
|
|
5331
|
+
// so the injection step must not expect to find it there
|
|
5332
|
+
ownerUrlInfo.contentInjectionUsedKeySet.add(key);
|
|
5333
|
+
}
|
|
5334
|
+
return null;
|
|
5335
|
+
},
|
|
5318
5336
|
};
|
|
5319
5337
|
};
|
|
5320
5338
|
|
|
5339
|
+
const injectIntoSpecifier = (specifier, injections) => {
|
|
5340
|
+
let specifierInjected = specifier;
|
|
5341
|
+
const keySet = new Set();
|
|
5342
|
+
for (const key of Object.keys(injections)) {
|
|
5343
|
+
if (!specifierInjected.includes(key)) {
|
|
5344
|
+
continue;
|
|
5345
|
+
}
|
|
5346
|
+
const injection = injections[key];
|
|
5347
|
+
if (!isPlaceholderInjection(injection)) {
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
const value = readInjectionValue(injection);
|
|
5351
|
+
if (typeof value !== "string") {
|
|
5352
|
+
continue;
|
|
5353
|
+
}
|
|
5354
|
+
specifierInjected = specifierInjected.replaceAll(key, value);
|
|
5355
|
+
keySet.add(key);
|
|
5356
|
+
}
|
|
5357
|
+
return { specifier: specifierInjected, keySet };
|
|
5358
|
+
};
|
|
5359
|
+
|
|
5321
5360
|
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
5322
5361
|
// and inherits its injections, with two adjustments:
|
|
5323
5362
|
// - a global belongs to the file itself, injecting it into each inline content would
|
|
@@ -7645,8 +7684,10 @@ const getCorePlugins = ({
|
|
|
7645
7684
|
...(packageBundle
|
|
7646
7685
|
? [jsenvPluginWorkspaceBundle({ packageDirectory })]
|
|
7647
7686
|
: []),
|
|
7648
|
-
|
|
7687
|
+
// before reference analysis: an url written by an injection must hold its
|
|
7688
|
+
// final value when references are analyzed
|
|
7649
7689
|
jsenvPluginInjections(injections),
|
|
7690
|
+
jsenvPluginReferenceAnalysis(referenceAnalysis),
|
|
7650
7691
|
jsenvPluginTranspilation(transpilation),
|
|
7651
7692
|
// "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
|
|
7652
7693
|
...(inlining ? [jsenvPluginInlining()] : []),
|
|
@@ -9309,6 +9350,9 @@ const createUrlInfo = (url, context) => {
|
|
|
9309
9350
|
contentFinalized: false,
|
|
9310
9351
|
contentSideEffects: [],
|
|
9311
9352
|
contentInjections: {},
|
|
9353
|
+
// placeholders already consumed somewhere else than the content (in a specifier),
|
|
9354
|
+
// so that not finding them in the content is not worth a warning
|
|
9355
|
+
contentInjectionUsedKeySet: new Set(),
|
|
9312
9356
|
|
|
9313
9357
|
sourcemap: null,
|
|
9314
9358
|
sourcemapIsWrong: false,
|
package/package.json
CHANGED
package/src/build/build.js
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
createJsenvPluginsController,
|
|
70
70
|
createJsenvPluginStore,
|
|
71
71
|
} from "../plugins/jsenv_plugins_controller.js";
|
|
72
|
+
import { isBareSpecifier } from "../helpers/bare_specifier.js";
|
|
72
73
|
import { getCorePlugins } from "../plugins/plugins.js";
|
|
73
74
|
import { jsenvPluginReferenceAnalysis } from "../plugins/reference_analysis/jsenv_plugin_reference_analysis.js";
|
|
74
75
|
import { renderBuildDoneLog } from "./build_content_report.js";
|
|
@@ -1488,7 +1489,10 @@ const prepareEntryPointBuild = async (
|
|
|
1488
1489
|
const registerHtmlMutation = (callback) => {
|
|
1489
1490
|
htmlMutationCallbackSet.add(callback);
|
|
1490
1491
|
};
|
|
1491
|
-
htmlRefine(htmlAst, {
|
|
1492
|
+
htmlRefine(htmlAst, {
|
|
1493
|
+
registerHtmlMutation,
|
|
1494
|
+
htmlUrlInfo: urlInfo,
|
|
1495
|
+
});
|
|
1492
1496
|
for (const htmlMutationCallback of htmlMutationCallbackSet) {
|
|
1493
1497
|
htmlMutationCallback();
|
|
1494
1498
|
}
|
|
@@ -1572,20 +1576,3 @@ const prepareEntryPointBuild = async (
|
|
|
1572
1576
|
},
|
|
1573
1577
|
};
|
|
1574
1578
|
};
|
|
1575
|
-
|
|
1576
|
-
const isBareSpecifier = (specifier) => {
|
|
1577
|
-
if (
|
|
1578
|
-
specifier[0] === "/" ||
|
|
1579
|
-
specifier.startsWith("./") ||
|
|
1580
|
-
specifier.startsWith("../")
|
|
1581
|
-
) {
|
|
1582
|
-
return false;
|
|
1583
|
-
}
|
|
1584
|
-
try {
|
|
1585
|
-
// eslint-disable-next-line no-new
|
|
1586
|
-
new URL(specifier);
|
|
1587
|
-
return false;
|
|
1588
|
-
} catch {
|
|
1589
|
-
return true;
|
|
1590
|
-
}
|
|
1591
|
-
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
23
23
|
import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
25
26
|
import { prependContent } from "../kitchen/prepend_content.js";
|
|
26
27
|
import { GRAPH_VISITOR } from "../kitchen/url_graph/url_graph_visitor.js";
|
|
27
28
|
import { isWebWorkerUrlInfo } from "../kitchen/web_workers.js";
|
|
@@ -895,7 +896,7 @@ export const createBuildSpecifierManager = ({
|
|
|
895
896
|
|
|
896
897
|
prepareResyncResourceHints: ({ registerHtmlRefine }) => {
|
|
897
898
|
const hintToInjectMap = new Map();
|
|
898
|
-
registerHtmlRefine((htmlAst, { registerHtmlMutation }) => {
|
|
899
|
+
registerHtmlRefine((htmlAst, { registerHtmlMutation, htmlUrlInfo }) => {
|
|
899
900
|
visitHtmlNodes(htmlAst, {
|
|
900
901
|
link: (node) => {
|
|
901
902
|
if (getHtmlNodeAttribute(node, "jsenv-ignore") !== undefined) {
|
|
@@ -919,12 +920,39 @@ export const createBuildSpecifierManager = ({
|
|
|
919
920
|
return;
|
|
920
921
|
}
|
|
921
922
|
const rawUrl = href;
|
|
923
|
+
if (targetsAFileThatDoesNotExist(rawUrl)) {
|
|
924
|
+
// this hint never designated anything, so nothing can explain its
|
|
925
|
+
// removal; taking away markup written by hand on a guess is worse
|
|
926
|
+
// than leaving it there
|
|
927
|
+
logger.warn(
|
|
928
|
+
createDetailedMessage(
|
|
929
|
+
`${UNICODE.WARNING} resource hint kept as is: "${href}" cannot be resolved`,
|
|
930
|
+
{
|
|
931
|
+
"html file": htmlUrlInfo.url,
|
|
932
|
+
...(rawKitchen.context.hasInjections
|
|
933
|
+
? {
|
|
934
|
+
suggestion: `when that url is written by an injection, jsenv resolves it in html attributes before analyzing references; check the placeholder spelling`,
|
|
935
|
+
}
|
|
936
|
+
: {}),
|
|
937
|
+
},
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
// the build url means nothing for something jsenv could not resolve,
|
|
941
|
+
// and it would leak a local path into the build
|
|
942
|
+
registerHtmlMutation(() => {
|
|
943
|
+
setHtmlNodeAttributes(node, {
|
|
944
|
+
href: urlToRelativeUrl(rawUrl, htmlUrlInfo.url),
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
922
949
|
const finalUrl = internalRedirections.get(rawUrl) || rawUrl;
|
|
923
950
|
const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
|
|
924
951
|
if (!urlInfo) {
|
|
925
|
-
if (
|
|
926
|
-
//
|
|
927
|
-
//
|
|
952
|
+
if (!href.startsWith("file:")) {
|
|
953
|
+
// the hint designates a resource jsenv does not own: an origin for
|
|
954
|
+
// preconnect/dns-prefetch, a remote file for the others. The author
|
|
955
|
+
// knows what the page will request at runtime, keep it as-is.
|
|
928
956
|
return;
|
|
929
957
|
}
|
|
930
958
|
logger.warn(
|
|
@@ -1369,5 +1397,15 @@ const asBuildUrlVersioned = ({
|
|
|
1369
1397
|
return `${buildDirectoryUrl}${pathname}${search}${hash}`;
|
|
1370
1398
|
};
|
|
1371
1399
|
|
|
1400
|
+
const targetsAFileThatDoesNotExist = (url) => {
|
|
1401
|
+
if (!url.startsWith("file:")) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
const urlObject = new URL(url);
|
|
1405
|
+
urlObject.search = "";
|
|
1406
|
+
urlObject.hash = "";
|
|
1407
|
+
return !existsSync(urlObject);
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1372
1410
|
// export for unit tests
|
|
1373
1411
|
export { generateVersion };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A bare specifier ("preact", "@jsenv/core/x.js") is resolved by node esm resolution,
|
|
2
|
+
// everything else ("/a.js", "./a.js", "http://example.com/a.js") by url resolution
|
|
3
|
+
export const isBareSpecifier = (specifier) => {
|
|
4
|
+
if (
|
|
5
|
+
specifier[0] === "/" ||
|
|
6
|
+
specifier.startsWith("./") ||
|
|
7
|
+
specifier.startsWith("../")
|
|
8
|
+
) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
// eslint-disable-next-line no-new
|
|
13
|
+
new URL(specifier);
|
|
14
|
+
return false;
|
|
15
|
+
} catch {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
};
|
package/src/kitchen/errors.js
CHANGED
|
@@ -373,9 +373,10 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
373
373
|
return getFirstReferenceInProject(firstReference);
|
|
374
374
|
};
|
|
375
375
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// (the key is free-form), tell the file it comes
|
|
376
|
+
// Injections write urls in html attributes before references are analyzed, so an url
|
|
377
|
+
// that still cannot be resolved may be a placeholder no injection replaced. Rather than
|
|
378
|
+
// guessing what a placeholder looks like (the key is free-form), tell the file it comes
|
|
379
|
+
// from: injections are configured for it.
|
|
379
380
|
const detailsFromInjectionsOnOwner = (reference) => {
|
|
380
381
|
if (!reference) {
|
|
381
382
|
return {};
|
|
@@ -394,7 +395,7 @@ const detailsFromInjectionsOnOwner = (reference) => {
|
|
|
394
395
|
return {};
|
|
395
396
|
}
|
|
396
397
|
return {
|
|
397
|
-
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, add "jsenv-ignore" so jsenv leaves that url alone:
|
|
398
|
+
suggestion: `injections are configured for this file; when "${reference.specifier}" is meant to be written by one of them, check the placeholder spelling, or add "jsenv-ignore" so jsenv leaves that url alone:
|
|
398
399
|
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
399
400
|
};
|
|
400
401
|
};
|
|
@@ -214,6 +214,9 @@ const createUrlInfo = (url, context) => {
|
|
|
214
214
|
contentFinalized: false,
|
|
215
215
|
contentSideEffects: [],
|
|
216
216
|
contentInjections: {},
|
|
217
|
+
// placeholders already consumed somewhere else than the content (in a specifier),
|
|
218
|
+
// so that not finding them in the content is not worth a warning
|
|
219
|
+
contentInjectionUsedKeySet: new Set(),
|
|
217
220
|
|
|
218
221
|
sourcemap: null,
|
|
219
222
|
sourcemapIsWrong: false,
|
|
@@ -25,6 +25,13 @@ export const INJECTIONS = {
|
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export const readInjectionValue = (injection) => {
|
|
29
|
+
if (injection && injection[injectionSymbol]) {
|
|
30
|
+
return injection.value;
|
|
31
|
+
}
|
|
32
|
+
return injection;
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
export const isPlaceholderInjection = (value) => {
|
|
29
36
|
return (
|
|
30
37
|
!value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
|
|
@@ -98,7 +105,7 @@ export const injectPlaceholderReplacements = (
|
|
|
98
105
|
for (const { key, isOptional, value } of placeholderReplacements) {
|
|
99
106
|
let index = content.indexOf(key);
|
|
100
107
|
if (index === -1) {
|
|
101
|
-
if (!isOptional) {
|
|
108
|
+
if (!isOptional && !urlInfo.contentInjectionUsedKeySet.has(key)) {
|
|
102
109
|
urlInfo.context.logger.warn(
|
|
103
110
|
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
104
111
|
--- suggestion a ---
|
|
@@ -123,7 +130,7 @@ return {
|
|
|
123
130
|
magicSource.replace({
|
|
124
131
|
start,
|
|
125
132
|
end,
|
|
126
|
-
replacement: asReplacement(value, urlInfo),
|
|
133
|
+
replacement: asReplacement(value, urlInfo.type),
|
|
127
134
|
});
|
|
128
135
|
index = content.indexOf(key, end);
|
|
129
136
|
}
|
|
@@ -134,8 +141,8 @@ return {
|
|
|
134
141
|
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
135
142
|
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
136
143
|
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
137
|
-
const asReplacement = (value,
|
|
138
|
-
if (
|
|
144
|
+
export const asReplacement = (value, type) => {
|
|
145
|
+
if (type === "js_classic" || type === "js_module") {
|
|
139
146
|
return JSON.stringify(value, null, " ");
|
|
140
147
|
}
|
|
141
148
|
if (typeof value === "string") {
|
|
@@ -3,6 +3,7 @@ import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
|
|
|
3
3
|
import {
|
|
4
4
|
INJECTIONS,
|
|
5
5
|
isPlaceholderInjection,
|
|
6
|
+
readInjectionValue,
|
|
6
7
|
} from "../../kitchen/url_graph/url_info_injections.js";
|
|
7
8
|
|
|
8
9
|
export const jsenvPluginInjections = (rawAssociations) => {
|
|
@@ -18,6 +19,7 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
18
19
|
}
|
|
19
20
|
return null;
|
|
20
21
|
};
|
|
22
|
+
const injectionsForUrlInfoMap = new WeakMap();
|
|
21
23
|
let getInjections = null;
|
|
22
24
|
|
|
23
25
|
return {
|
|
@@ -36,8 +38,8 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
36
38
|
});
|
|
37
39
|
return injectionsGetter;
|
|
38
40
|
};
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
+
// errors and the build read this to know an unresolved url may come from
|
|
42
|
+
// an injection, and word what they report accordingly
|
|
41
43
|
context.hasInjections = (url) => {
|
|
42
44
|
return Boolean(findInjectionsGetterForUrl(url));
|
|
43
45
|
};
|
|
@@ -91,16 +93,61 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
91
93
|
contentInjections: defaultInjections,
|
|
92
94
|
};
|
|
93
95
|
}
|
|
96
|
+
injectionsForUrlInfoMap.set(urlInfo, injections);
|
|
94
97
|
return {
|
|
95
|
-
contentInjections: {
|
|
96
|
-
...defaultInjections,
|
|
97
|
-
...injections,
|
|
98
|
-
},
|
|
98
|
+
contentInjections: { ...defaultInjections, ...injections },
|
|
99
99
|
};
|
|
100
100
|
},
|
|
101
|
+
// The content still holds the placeholders when references are analyzed: they are
|
|
102
|
+
// replaced at the very end, once the type of every inline content is known.
|
|
103
|
+
// A specifier must not wait for that, it would resolve "__BACKEND_URL__/users/me"
|
|
104
|
+
// as a file sitting next to the document. Only the placeholder is resolved here,
|
|
105
|
+
// then the specifier goes to whoever resolves this kind of url (node esm, web, ...)
|
|
106
|
+
resolveReference: (reference) => {
|
|
107
|
+
const { ownerUrlInfo } = reference;
|
|
108
|
+
const injections = injectionsForUrlInfoMap.get(ownerUrlInfo);
|
|
109
|
+
if (!injections) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const { specifier, keySet } = injectIntoSpecifier(
|
|
113
|
+
reference.specifier,
|
|
114
|
+
injections,
|
|
115
|
+
);
|
|
116
|
+
if (keySet.size === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
reference.specifier = specifier;
|
|
120
|
+
for (const key of keySet) {
|
|
121
|
+
// rewriting the specifier takes the placeholder out of the content,
|
|
122
|
+
// so the injection step must not expect to find it there
|
|
123
|
+
ownerUrlInfo.contentInjectionUsedKeySet.add(key);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
101
127
|
};
|
|
102
128
|
};
|
|
103
129
|
|
|
130
|
+
const injectIntoSpecifier = (specifier, injections) => {
|
|
131
|
+
let specifierInjected = specifier;
|
|
132
|
+
const keySet = new Set();
|
|
133
|
+
for (const key of Object.keys(injections)) {
|
|
134
|
+
if (!specifierInjected.includes(key)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const injection = injections[key];
|
|
138
|
+
if (!isPlaceholderInjection(injection)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const value = readInjectionValue(injection);
|
|
142
|
+
if (typeof value !== "string") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
specifierInjected = specifierInjected.replaceAll(key, value);
|
|
146
|
+
keySet.add(key);
|
|
147
|
+
}
|
|
148
|
+
return { specifier: specifierInjected, keySet };
|
|
149
|
+
};
|
|
150
|
+
|
|
104
151
|
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
105
152
|
// and inherits its injections, with two adjustments:
|
|
106
153
|
// - a global belongs to the file itself, injecting it into each inline content would
|
package/src/plugins/plugins.js
CHANGED
|
@@ -87,8 +87,10 @@ export const getCorePlugins = ({
|
|
|
87
87
|
...(packageBundle
|
|
88
88
|
? [jsenvPluginWorkspaceBundle({ packageDirectory })]
|
|
89
89
|
: []),
|
|
90
|
-
|
|
90
|
+
// before reference analysis: an url written by an injection must hold its
|
|
91
|
+
// final value when references are analyzed
|
|
91
92
|
jsenvPluginInjections(injections),
|
|
93
|
+
jsenvPluginReferenceAnalysis(referenceAnalysis),
|
|
92
94
|
jsenvPluginTranspilation(transpilation),
|
|
93
95
|
// "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
|
|
94
96
|
...(inlining ? [jsenvPluginInlining()] : []),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { createMagicSource } from "@jsenv/sourcemap";
|
|
7
7
|
import { urlToExtension } from "@jsenv/urls";
|
|
8
8
|
import { JS_QUOTES } from "@jsenv/utils/src/string/js_quotes.js";
|
|
9
|
+
import { isBareSpecifier } from "../../../helpers/bare_specifier.js";
|
|
9
10
|
|
|
10
11
|
export const jsenvPluginJsReferenceAnalysis = ({ inlineContent }) => {
|
|
11
12
|
return [
|
|
@@ -192,20 +193,3 @@ const parseAndTransformJsReferences = async (
|
|
|
192
193
|
const { content, sourcemap } = magicSource.toContentAndSourcemap();
|
|
193
194
|
return { content, sourcemap };
|
|
194
195
|
};
|
|
195
|
-
|
|
196
|
-
const isBareSpecifier = (specifier) => {
|
|
197
|
-
if (
|
|
198
|
-
specifier[0] === "/" ||
|
|
199
|
-
specifier.startsWith("./") ||
|
|
200
|
-
specifier.startsWith("../")
|
|
201
|
-
) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
// eslint-disable-next-line no-new
|
|
206
|
-
new URL(specifier);
|
|
207
|
-
return false;
|
|
208
|
-
} catch {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { URL_META } from "@jsenv/url-meta";
|
|
15
15
|
import { urlToBasename, urlToExtension } from "@jsenv/urls";
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
|
+
import { isBareSpecifier } from "../../helpers/bare_specifier.js";
|
|
17
18
|
|
|
18
19
|
export const createNodeEsmResolver = ({
|
|
19
20
|
packageDirectory,
|
|
@@ -487,20 +488,3 @@ const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
|
487
488
|
}
|
|
488
489
|
};
|
|
489
490
|
};
|
|
490
|
-
|
|
491
|
-
const isBareSpecifier = (specifier) => {
|
|
492
|
-
if (
|
|
493
|
-
specifier[0] === "/" ||
|
|
494
|
-
specifier.startsWith("./") ||
|
|
495
|
-
specifier.startsWith("../")
|
|
496
|
-
) {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
try {
|
|
500
|
-
// eslint-disable-next-line no-new
|
|
501
|
-
new URL(specifier);
|
|
502
|
-
return false;
|
|
503
|
-
} catch {
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
};
|