@jsenv/core 41.4.5 → 41.4.7
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 +202 -117
- package/dist/start_dev_server/start_dev_server.js +111 -52
- package/package.json +1 -1
- package/src/build/build.js +6 -18
- package/src/build/build_specifier_manager.js +49 -21
- package/src/build/build_urls_generator.js +43 -22
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +10 -1
- 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/src/plugins/resolution_web/jsenv_plugin_web_resolution.js +7 -1
|
@@ -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 = {},
|
|
@@ -3243,7 +3228,13 @@ const jsenvPluginWebResolution = () => {
|
|
|
3243
3228
|
if (ownerUrlInfo.originalUrl?.startsWith("http")) {
|
|
3244
3229
|
return new URL(resource, ownerUrlInfo.originalUrl);
|
|
3245
3230
|
}
|
|
3246
|
-
|
|
3231
|
+
// "/x" is the web meaning of a root-relative url: the root of what is
|
|
3232
|
+
// served, which is the source directory — not the directory the entry
|
|
3233
|
+
// point happens to sit in.
|
|
3234
|
+
const url = new URL(
|
|
3235
|
+
resource.slice(1),
|
|
3236
|
+
ownerUrlInfo.context.rootDirectoryUrl,
|
|
3237
|
+
);
|
|
3247
3238
|
return url;
|
|
3248
3239
|
}
|
|
3249
3240
|
// baseUrl happens second argument to new URL() is different from
|
|
@@ -4858,9 +4849,10 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
4858
4849
|
return getFirstReferenceInProject(firstReference);
|
|
4859
4850
|
};
|
|
4860
4851
|
|
|
4861
|
-
//
|
|
4862
|
-
//
|
|
4863
|
-
// (the key is free-form), tell the file it comes
|
|
4852
|
+
// Injections write urls in html attributes before references are analyzed, so an url
|
|
4853
|
+
// that still cannot be resolved may be a placeholder no injection replaced. Rather than
|
|
4854
|
+
// guessing what a placeholder looks like (the key is free-form), tell the file it comes
|
|
4855
|
+
// from: injections are configured for it.
|
|
4864
4856
|
const detailsFromInjectionsOnOwner = (reference) => {
|
|
4865
4857
|
if (!reference) {
|
|
4866
4858
|
return {};
|
|
@@ -4879,7 +4871,7 @@ const detailsFromInjectionsOnOwner = (reference) => {
|
|
|
4879
4871
|
return {};
|
|
4880
4872
|
}
|
|
4881
4873
|
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:
|
|
4874
|
+
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
4875
|
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
4884
4876
|
};
|
|
4885
4877
|
};
|
|
@@ -5047,6 +5039,13 @@ const INJECTIONS = {
|
|
|
5047
5039
|
},
|
|
5048
5040
|
};
|
|
5049
5041
|
|
|
5042
|
+
const readInjectionValue = (injection) => {
|
|
5043
|
+
if (injection && injection[injectionSymbol]) {
|
|
5044
|
+
return injection.value;
|
|
5045
|
+
}
|
|
5046
|
+
return injection;
|
|
5047
|
+
};
|
|
5048
|
+
|
|
5050
5049
|
const isPlaceholderInjection = (value) => {
|
|
5051
5050
|
return (
|
|
5052
5051
|
!value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
|
|
@@ -5120,7 +5119,7 @@ const injectPlaceholderReplacements = (
|
|
|
5120
5119
|
for (const { key, isOptional, value } of placeholderReplacements) {
|
|
5121
5120
|
let index = content.indexOf(key);
|
|
5122
5121
|
if (index === -1) {
|
|
5123
|
-
if (!isOptional) {
|
|
5122
|
+
if (!isOptional && !urlInfo.contentInjectionUsedKeySet.has(key)) {
|
|
5124
5123
|
urlInfo.context.logger.warn(
|
|
5125
5124
|
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
5126
5125
|
--- suggestion a ---
|
|
@@ -5145,7 +5144,7 @@ return {
|
|
|
5145
5144
|
magicSource.replace({
|
|
5146
5145
|
start,
|
|
5147
5146
|
end,
|
|
5148
|
-
replacement: asReplacement(value, urlInfo),
|
|
5147
|
+
replacement: asReplacement(value, urlInfo.type),
|
|
5149
5148
|
});
|
|
5150
5149
|
index = content.indexOf(key, end);
|
|
5151
5150
|
}
|
|
@@ -5156,8 +5155,8 @@ return {
|
|
|
5156
5155
|
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
5157
5156
|
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
5158
5157
|
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
5159
|
-
const asReplacement = (value,
|
|
5160
|
-
if (
|
|
5158
|
+
const asReplacement = (value, type) => {
|
|
5159
|
+
if (type === "js_classic" || type === "js_module") {
|
|
5161
5160
|
return JSON.stringify(value, null, " ");
|
|
5162
5161
|
}
|
|
5163
5162
|
if (typeof value === "string") {
|
|
@@ -5235,6 +5234,7 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5235
5234
|
}
|
|
5236
5235
|
return null;
|
|
5237
5236
|
};
|
|
5237
|
+
const injectionsForUrlInfoMap = new WeakMap();
|
|
5238
5238
|
let getInjections = null;
|
|
5239
5239
|
|
|
5240
5240
|
return {
|
|
@@ -5253,8 +5253,8 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5253
5253
|
});
|
|
5254
5254
|
return injectionsGetter;
|
|
5255
5255
|
};
|
|
5256
|
-
//
|
|
5257
|
-
//
|
|
5256
|
+
// errors and the build read this to know an unresolved url may come from
|
|
5257
|
+
// an injection, and word what they report accordingly
|
|
5258
5258
|
context.hasInjections = (url) => {
|
|
5259
5259
|
return Boolean(findInjectionsGetterForUrl(url));
|
|
5260
5260
|
};
|
|
@@ -5308,16 +5308,61 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5308
5308
|
contentInjections: defaultInjections,
|
|
5309
5309
|
};
|
|
5310
5310
|
}
|
|
5311
|
+
injectionsForUrlInfoMap.set(urlInfo, injections);
|
|
5311
5312
|
return {
|
|
5312
|
-
contentInjections: {
|
|
5313
|
-
...defaultInjections,
|
|
5314
|
-
...injections,
|
|
5315
|
-
},
|
|
5313
|
+
contentInjections: { ...defaultInjections, ...injections },
|
|
5316
5314
|
};
|
|
5317
5315
|
},
|
|
5316
|
+
// The content still holds the placeholders when references are analyzed: they are
|
|
5317
|
+
// replaced at the very end, once the type of every inline content is known.
|
|
5318
|
+
// A specifier must not wait for that, it would resolve "__BACKEND_URL__/users/me"
|
|
5319
|
+
// as a file sitting next to the document. Only the placeholder is resolved here,
|
|
5320
|
+
// then the specifier goes to whoever resolves this kind of url (node esm, web, ...)
|
|
5321
|
+
resolveReference: (reference) => {
|
|
5322
|
+
const { ownerUrlInfo } = reference;
|
|
5323
|
+
const injections = injectionsForUrlInfoMap.get(ownerUrlInfo);
|
|
5324
|
+
if (!injections) {
|
|
5325
|
+
return null;
|
|
5326
|
+
}
|
|
5327
|
+
const { specifier, keySet } = injectIntoSpecifier(
|
|
5328
|
+
reference.specifier,
|
|
5329
|
+
injections,
|
|
5330
|
+
);
|
|
5331
|
+
if (keySet.size === 0) {
|
|
5332
|
+
return null;
|
|
5333
|
+
}
|
|
5334
|
+
reference.specifier = specifier;
|
|
5335
|
+
for (const key of keySet) {
|
|
5336
|
+
// rewriting the specifier takes the placeholder out of the content,
|
|
5337
|
+
// so the injection step must not expect to find it there
|
|
5338
|
+
ownerUrlInfo.contentInjectionUsedKeySet.add(key);
|
|
5339
|
+
}
|
|
5340
|
+
return null;
|
|
5341
|
+
},
|
|
5318
5342
|
};
|
|
5319
5343
|
};
|
|
5320
5344
|
|
|
5345
|
+
const injectIntoSpecifier = (specifier, injections) => {
|
|
5346
|
+
let specifierInjected = specifier;
|
|
5347
|
+
const keySet = new Set();
|
|
5348
|
+
for (const key of Object.keys(injections)) {
|
|
5349
|
+
if (!specifierInjected.includes(key)) {
|
|
5350
|
+
continue;
|
|
5351
|
+
}
|
|
5352
|
+
const injection = injections[key];
|
|
5353
|
+
if (!isPlaceholderInjection(injection)) {
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
const value = readInjectionValue(injection);
|
|
5357
|
+
if (typeof value !== "string") {
|
|
5358
|
+
continue;
|
|
5359
|
+
}
|
|
5360
|
+
specifierInjected = specifierInjected.replaceAll(key, value);
|
|
5361
|
+
keySet.add(key);
|
|
5362
|
+
}
|
|
5363
|
+
return { specifier: specifierInjected, keySet };
|
|
5364
|
+
};
|
|
5365
|
+
|
|
5321
5366
|
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
5322
5367
|
// and inherits its injections, with two adjustments:
|
|
5323
5368
|
// - a global belongs to the file itself, injecting it into each inline content would
|
|
@@ -7645,8 +7690,10 @@ const getCorePlugins = ({
|
|
|
7645
7690
|
...(packageBundle
|
|
7646
7691
|
? [jsenvPluginWorkspaceBundle({ packageDirectory })]
|
|
7647
7692
|
: []),
|
|
7648
|
-
|
|
7693
|
+
// before reference analysis: an url written by an injection must hold its
|
|
7694
|
+
// final value when references are analyzed
|
|
7649
7695
|
jsenvPluginInjections(injections),
|
|
7696
|
+
jsenvPluginReferenceAnalysis(referenceAnalysis),
|
|
7650
7697
|
jsenvPluginTranspilation(transpilation),
|
|
7651
7698
|
// "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
|
|
7652
7699
|
...(inlining ? [jsenvPluginInlining()] : []),
|
|
@@ -9309,6 +9356,9 @@ const createUrlInfo = (url, context) => {
|
|
|
9309
9356
|
contentFinalized: false,
|
|
9310
9357
|
contentSideEffects: [],
|
|
9311
9358
|
contentInjections: {},
|
|
9359
|
+
// placeholders already consumed somewhere else than the content (in a specifier),
|
|
9360
|
+
// so that not finding them in the content is not worth a warning
|
|
9361
|
+
contentInjectionUsedKeySet: new Set(),
|
|
9312
9362
|
|
|
9313
9363
|
sourcemap: null,
|
|
9314
9364
|
sourcemapIsWrong: false,
|
|
@@ -11110,7 +11160,16 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11110
11160
|
}
|
|
11111
11161
|
}
|
|
11112
11162
|
});
|
|
11113
|
-
|
|
11163
|
+
// A client we cannot identify (curl, fetch, a healthcheck, a proxy dropping
|
|
11164
|
+
// the user agent) is assumed to be one of the runtimes the project targets.
|
|
11165
|
+
// Treating it as a runtime supporting nothing would serve it the js module
|
|
11166
|
+
// fallback, breaking tooling perfectly capable of ES modules; an actual
|
|
11167
|
+
// ancient browser hiding its identity fails loudly instead, which is the
|
|
11168
|
+
// cheaper of the two mistakes.
|
|
11169
|
+
const clientRuntimeCompat =
|
|
11170
|
+
runtimeName === "unknown"
|
|
11171
|
+
? runtimeCompat
|
|
11172
|
+
: { [runtimeName]: runtimeVersion };
|
|
11114
11173
|
|
|
11115
11174
|
kitchen = createKitchen({
|
|
11116
11175
|
name: runtimeId,
|
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";
|
|
@@ -1248,6 +1249,7 @@ const prepareEntryPointBuild = async (
|
|
|
1248
1249
|
base,
|
|
1249
1250
|
assetsDirectory,
|
|
1250
1251
|
buildUrlsGenerator,
|
|
1252
|
+
entryKey: sourceRelativeUrl,
|
|
1251
1253
|
|
|
1252
1254
|
versioning,
|
|
1253
1255
|
versioningMethod,
|
|
@@ -1488,7 +1490,10 @@ const prepareEntryPointBuild = async (
|
|
|
1488
1490
|
const registerHtmlMutation = (callback) => {
|
|
1489
1491
|
htmlMutationCallbackSet.add(callback);
|
|
1490
1492
|
};
|
|
1491
|
-
htmlRefine(htmlAst, {
|
|
1493
|
+
htmlRefine(htmlAst, {
|
|
1494
|
+
registerHtmlMutation,
|
|
1495
|
+
htmlUrlInfo: urlInfo,
|
|
1496
|
+
});
|
|
1492
1497
|
for (const htmlMutationCallback of htmlMutationCallbackSet) {
|
|
1493
1498
|
htmlMutationCallback();
|
|
1494
1499
|
}
|
|
@@ -1572,20 +1577,3 @@ const prepareEntryPointBuild = async (
|
|
|
1572
1577
|
},
|
|
1573
1578
|
};
|
|
1574
1579
|
};
|
|
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";
|
|
@@ -38,6 +39,7 @@ export const createBuildSpecifierManager = ({
|
|
|
38
39
|
buildDirectoryUrl,
|
|
39
40
|
assetsDirectory,
|
|
40
41
|
buildUrlsGenerator,
|
|
42
|
+
entryKey,
|
|
41
43
|
base,
|
|
42
44
|
length = 8,
|
|
43
45
|
|
|
@@ -76,6 +78,7 @@ export const createBuildSpecifierManager = ({
|
|
|
76
78
|
urlInfo,
|
|
77
79
|
ownerUrlInfo: reference.ownerUrlInfo,
|
|
78
80
|
assetsDirectory,
|
|
81
|
+
entryKey,
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -895,7 +898,7 @@ export const createBuildSpecifierManager = ({
|
|
|
895
898
|
|
|
896
899
|
prepareResyncResourceHints: ({ registerHtmlRefine }) => {
|
|
897
900
|
const hintToInjectMap = new Map();
|
|
898
|
-
registerHtmlRefine((htmlAst, { registerHtmlMutation }) => {
|
|
901
|
+
registerHtmlRefine((htmlAst, { registerHtmlMutation, htmlUrlInfo }) => {
|
|
899
902
|
visitHtmlNodes(htmlAst, {
|
|
900
903
|
link: (node) => {
|
|
901
904
|
if (getHtmlNodeAttribute(node, "jsenv-ignore") !== undefined) {
|
|
@@ -919,12 +922,39 @@ export const createBuildSpecifierManager = ({
|
|
|
919
922
|
return;
|
|
920
923
|
}
|
|
921
924
|
const rawUrl = href;
|
|
925
|
+
if (targetsAFileThatDoesNotExist(rawUrl)) {
|
|
926
|
+
// this hint never designated anything, so nothing can explain its
|
|
927
|
+
// removal; taking away markup written by hand on a guess is worse
|
|
928
|
+
// than leaving it there
|
|
929
|
+
logger.warn(
|
|
930
|
+
createDetailedMessage(
|
|
931
|
+
`${UNICODE.WARNING} resource hint kept as is: "${href}" cannot be resolved`,
|
|
932
|
+
{
|
|
933
|
+
"html file": htmlUrlInfo.url,
|
|
934
|
+
...(rawKitchen.context.hasInjections
|
|
935
|
+
? {
|
|
936
|
+
suggestion: `when that url is written by an injection, jsenv resolves it in html attributes before analyzing references; check the placeholder spelling`,
|
|
937
|
+
}
|
|
938
|
+
: {}),
|
|
939
|
+
},
|
|
940
|
+
),
|
|
941
|
+
);
|
|
942
|
+
// the build url means nothing for something jsenv could not resolve,
|
|
943
|
+
// and it would leak a local path into the build
|
|
944
|
+
registerHtmlMutation(() => {
|
|
945
|
+
setHtmlNodeAttributes(node, {
|
|
946
|
+
href: urlToRelativeUrl(rawUrl, htmlUrlInfo.url),
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
922
951
|
const finalUrl = internalRedirections.get(rawUrl) || rawUrl;
|
|
923
952
|
const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
|
|
924
953
|
if (!urlInfo) {
|
|
925
|
-
if (
|
|
926
|
-
//
|
|
927
|
-
//
|
|
954
|
+
if (!href.startsWith("file:")) {
|
|
955
|
+
// the hint designates a resource jsenv does not own: an origin for
|
|
956
|
+
// preconnect/dns-prefetch, a remote file for the others. The author
|
|
957
|
+
// knows what the page will request at runtime, keep it as-is.
|
|
928
958
|
return;
|
|
929
959
|
}
|
|
930
960
|
logger.warn(
|
|
@@ -1106,9 +1136,14 @@ export const createBuildSpecifierManager = ({
|
|
|
1106
1136
|
let contentKey;
|
|
1107
1137
|
// if to guard for html where versioned build specifier is not generated
|
|
1108
1138
|
if (buildSpecifierVersioned) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1139
|
+
// The version goes on the build url, not on the specifier: a
|
|
1140
|
+
// specifier is written for one importer to read (base: "./" makes
|
|
1141
|
+
// it relative to that importer, base: "https://cdn…" makes it
|
|
1142
|
+
// absolute elsewhere) and says nothing about where the file lands.
|
|
1143
|
+
const buildUrlVersioned = injectVersionIntoBuildSpecifier({
|
|
1144
|
+
buildSpecifier: buildUrl,
|
|
1145
|
+
version: versionMap.get(urlInfo),
|
|
1146
|
+
versioningMethod,
|
|
1112
1147
|
});
|
|
1113
1148
|
const buildRelativeUrlVersioned = urlToRelativeUrl(
|
|
1114
1149
|
buildUrlVersioned,
|
|
@@ -1352,21 +1387,14 @@ const injectVersionIntoBuildSpecifier = ({
|
|
|
1352
1387
|
);
|
|
1353
1388
|
};
|
|
1354
1389
|
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}) => {
|
|
1359
|
-
if (buildSpecifierVersioned[0] === "/") {
|
|
1360
|
-
return new URL(buildSpecifierVersioned.slice(1), buildDirectoryUrl).href;
|
|
1361
|
-
}
|
|
1362
|
-
const buildUrl = new URL(buildSpecifierVersioned, buildDirectoryUrl).href;
|
|
1363
|
-
if (buildUrl.startsWith(buildDirectoryUrl)) {
|
|
1364
|
-
return buildUrl;
|
|
1390
|
+
const targetsAFileThatDoesNotExist = (url) => {
|
|
1391
|
+
if (!url.startsWith("file:")) {
|
|
1392
|
+
return false;
|
|
1365
1393
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
return
|
|
1394
|
+
const urlObject = new URL(url);
|
|
1395
|
+
urlObject.search = "";
|
|
1396
|
+
urlObject.hash = "";
|
|
1397
|
+
return !existsSync(urlObject);
|
|
1370
1398
|
};
|
|
1371
1399
|
|
|
1372
1400
|
// export for unit tests
|
|
@@ -31,12 +31,41 @@ export const createBuildUrlsGenerator = ({
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const nameSetPerDirectoryMap = new Map();
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const reserveName = (directoryPath, urlName) => {
|
|
35
|
+
let nameSet = nameSetPerDirectoryMap.get(directoryPath);
|
|
36
|
+
if (!nameSet) {
|
|
37
|
+
nameSet = new Set();
|
|
38
|
+
nameSetPerDirectoryMap.set(directoryPath, nameSet);
|
|
39
|
+
}
|
|
40
|
+
let [basename, extension] = splitFileExtension(urlName);
|
|
41
|
+
extension = extensionMappings[extension] || extension;
|
|
42
|
+
let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
|
|
43
|
+
let integer = 1;
|
|
44
|
+
while (nameSet.has(nameCandidate)) {
|
|
45
|
+
integer++;
|
|
46
|
+
nameCandidate = `${basename}${integer}${extension}`;
|
|
47
|
+
}
|
|
48
|
+
nameSet.add(nameCandidate);
|
|
49
|
+
return nameCandidate;
|
|
50
|
+
};
|
|
51
|
+
const generate = (
|
|
52
|
+
url,
|
|
53
|
+
{ urlInfo, ownerUrlInfo, assetsDirectory, entryKey },
|
|
54
|
+
) => {
|
|
55
|
+
// A url already inside the build directory names a chunk a bundler just
|
|
56
|
+
// produced, and each entry point is bundled on its own: two of them
|
|
57
|
+
// routinely produce a chunk of the same name holding different code (the
|
|
58
|
+
// shared dependencies of THAT entry point, exported under names decided by
|
|
59
|
+
// THAT bundle). They are told apart by the entry point they come from —
|
|
60
|
+
// sharing the file would leave one entry importing exports the file on disk
|
|
61
|
+
// does not have.
|
|
62
|
+
const insideBuildDirectory = urlIsOrIsInsideOf(url, buildDirectoryUrl);
|
|
63
|
+
const key = insideBuildDirectory ? `${entryKey} ${url}` : url;
|
|
64
|
+
const buildUrlFromMap = buildUrlMap.get(key);
|
|
36
65
|
if (buildUrlFromMap) {
|
|
37
66
|
return buildUrlFromMap;
|
|
38
67
|
}
|
|
39
|
-
if (
|
|
68
|
+
if (insideBuildDirectory) {
|
|
40
69
|
if (ownerUrlInfo.searchParams.has("dynamic_import_id")) {
|
|
41
70
|
const ownerDirectoryPath = determineDirectoryPath({
|
|
42
71
|
sourceDirectoryUrl,
|
|
@@ -48,11 +77,18 @@ export const createBuildUrlsGenerator = ({
|
|
|
48
77
|
buildUrl = injectQueryParams(buildUrl, {
|
|
49
78
|
dynamic_import_id: undefined,
|
|
50
79
|
});
|
|
51
|
-
associateBuildUrl(
|
|
80
|
+
associateBuildUrl(key, buildUrl);
|
|
52
81
|
return buildUrl;
|
|
53
82
|
}
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
const urlObject = new URL(url);
|
|
84
|
+
const chunkDirectoryPath = urlToRelativeUrl(
|
|
85
|
+
new URL("./", urlObject),
|
|
86
|
+
buildDirectoryUrl,
|
|
87
|
+
);
|
|
88
|
+
const chunkName = reserveName(chunkDirectoryPath, urlToFilename(url));
|
|
89
|
+
const buildUrl = `${buildDirectoryUrl}${chunkDirectoryPath}${chunkName}${urlObject.search}${urlObject.hash}`;
|
|
90
|
+
associateBuildUrl(key, buildUrl);
|
|
91
|
+
return buildUrl;
|
|
56
92
|
}
|
|
57
93
|
if (urlInfo.type === "entry_build") {
|
|
58
94
|
const buildUrl = new URL(urlInfo.filenameHint, buildDirectoryUrl).href;
|
|
@@ -84,25 +120,10 @@ export const createBuildUrlsGenerator = ({
|
|
|
84
120
|
urlInfo,
|
|
85
121
|
ownerUrlInfo,
|
|
86
122
|
});
|
|
87
|
-
let nameSet = nameSetPerDirectoryMap.get(directoryPath);
|
|
88
|
-
if (!nameSet) {
|
|
89
|
-
nameSet = new Set();
|
|
90
|
-
nameSetPerDirectoryMap.set(directoryPath, nameSet);
|
|
91
|
-
}
|
|
92
123
|
const urlObject = new URL(url);
|
|
93
124
|
injectQueryParams(urlObject, { dynamic_import_id: undefined });
|
|
94
125
|
let { search, hash } = urlObject;
|
|
95
|
-
|
|
96
|
-
let [basename, extension] = splitFileExtension(urlName);
|
|
97
|
-
extension = extensionMappings[extension] || extension;
|
|
98
|
-
let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
|
|
99
|
-
let integer = 1;
|
|
100
|
-
while (nameSet.has(nameCandidate)) {
|
|
101
|
-
integer++;
|
|
102
|
-
nameCandidate = `${basename}${integer}${extension}`;
|
|
103
|
-
}
|
|
104
|
-
const name = nameCandidate;
|
|
105
|
-
nameSet.add(name);
|
|
126
|
+
const name = reserveName(directoryPath, getUrlName(url, urlInfo));
|
|
106
127
|
const buildUrl = `${buildDirectoryUrl}${directoryPath}${name}${search}${hash}`;
|
|
107
128
|
associateBuildUrl(url, buildUrl);
|
|
108
129
|
return buildUrl;
|
|
@@ -71,7 +71,16 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
|
|
74
|
+
// A client we cannot identify (curl, fetch, a healthcheck, a proxy dropping
|
|
75
|
+
// the user agent) is assumed to be one of the runtimes the project targets.
|
|
76
|
+
// Treating it as a runtime supporting nothing would serve it the js module
|
|
77
|
+
// fallback, breaking tooling perfectly capable of ES modules; an actual
|
|
78
|
+
// ancient browser hiding its identity fails loudly instead, which is the
|
|
79
|
+
// cheaper of the two mistakes.
|
|
80
|
+
const clientRuntimeCompat =
|
|
81
|
+
runtimeName === "unknown"
|
|
82
|
+
? runtimeCompat
|
|
83
|
+
: { [runtimeName]: runtimeVersion };
|
|
75
84
|
|
|
76
85
|
kitchen = createKitchen({
|
|
77
86
|
name: runtimeId,
|
|
@@ -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") {
|