@jsenv/core 41.4.3 → 41.4.5
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 +167 -16
- package/dist/client/html_syntax_error/html_syntax_error.html +1 -1
- package/dist/js/page_switcher.js +136 -13
- package/dist/jsenv_core.js +11 -1
- package/dist/start_dev_server/start_dev_server.js +136 -17
- package/package.json +6 -6
- package/src/build/build.js +30 -0
- package/src/build/build_specifier_manager.js +5 -0
- package/src/dev/start_dev_server.js +1 -0
- package/src/kitchen/errors.js +28 -0
- package/src/kitchen/url_graph/url_info_injections.js +34 -8
- package/src/plugins/injections/jsenv_plugin_injections.js +64 -8
- package/src/plugins/page_switcher/client/page_switcher.js +136 -13
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +3 -1
- package/src/plugins/protocol_file/html_pages.js +7 -1
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +4 -0
|
@@ -1321,7 +1321,9 @@ const jsenvPluginClientMonitoring = () => {
|
|
|
1321
1321
|
|
|
1322
1322
|
/*
|
|
1323
1323
|
* cmd+K (ctrl+K elsewhere) on any page the dev server serves opens a list of
|
|
1324
|
-
* the .html files it serves, filter as you type, Enter to go there.
|
|
1324
|
+
* the .html files it serves, filter as you type, Enter to go there. cmd+E
|
|
1325
|
+
* (ctrl+E elsewhere) opens a page's file in the editor instead of going to it:
|
|
1326
|
+
* the current page from anywhere, the selected row from inside the switcher.
|
|
1325
1327
|
*
|
|
1326
1328
|
* The list is the one the filesystem plugin already publishes for everyone
|
|
1327
1329
|
* (GET /.internal/pages.json, see protocol_file/html_pages.js) — this only adds
|
|
@@ -3327,7 +3329,9 @@ const FILE_AND_SERVER_URLS_CONVERTER = {
|
|
|
3327
3329
|
* of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
|
|
3328
3330
|
* gets the same answer.
|
|
3329
3331
|
*
|
|
3330
|
-
* Each page comes with
|
|
3332
|
+
* Each page comes with where its file is (so it can be opened in an editor as
|
|
3333
|
+
* well as in the browser) and with what kind of page it is, read from where it
|
|
3334
|
+
* sits and
|
|
3331
3335
|
* what it is called — the two conventions this repo already follows:
|
|
3332
3336
|
* - "experiment": something tried out, under a lab/ directory or named
|
|
3333
3337
|
* *_experiment.html;
|
|
@@ -3426,6 +3430,10 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
|
3426
3430
|
);
|
|
3427
3431
|
return {
|
|
3428
3432
|
url: `/${relativeUrl}`,
|
|
3433
|
+
// Where the file actually is, so whoever wants to open it in an editor
|
|
3434
|
+
// rather than in the browser has what GET /.internal/open_file/* asks
|
|
3435
|
+
// for (a file url) without having to know the root directory.
|
|
3436
|
+
fileUrl,
|
|
3429
3437
|
kind: readKind(meta),
|
|
3430
3438
|
// Relative to the root and without its trailing slash, which is how a
|
|
3431
3439
|
// tree names its own nodes.
|
|
@@ -4092,6 +4100,10 @@ const jsenvPluginFsRedirection = ({
|
|
|
4092
4100
|
}
|
|
4093
4101
|
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
4094
4102
|
reference.ownerUrlInfo.context;
|
|
4103
|
+
if (!requestedUrl) {
|
|
4104
|
+
// the SPA fallback answers a request; during build there is none
|
|
4105
|
+
return null;
|
|
4106
|
+
}
|
|
4095
4107
|
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
4096
4108
|
requestedUrl,
|
|
4097
4109
|
rootDirectoryUrl,
|
|
@@ -4551,6 +4563,7 @@ ${reason}`,
|
|
|
4551
4563
|
}
|
|
4552
4564
|
return createFailedToResolveUrlError({
|
|
4553
4565
|
reason: `An error occured during specifier resolution`,
|
|
4566
|
+
...detailsFromInjectionsOnOwner(reference),
|
|
4554
4567
|
...detailsFromValueThrown(error),
|
|
4555
4568
|
});
|
|
4556
4569
|
};
|
|
@@ -4612,6 +4625,7 @@ ${reason}`,
|
|
|
4612
4625
|
return createFailedToFetchUrlContentError({
|
|
4613
4626
|
code: "NOT_FOUND",
|
|
4614
4627
|
reason: "no entry on filesystem",
|
|
4628
|
+
...detailsFromInjectionsOnOwner(urlInfo.firstReference),
|
|
4615
4629
|
});
|
|
4616
4630
|
}
|
|
4617
4631
|
}
|
|
@@ -4844,6 +4858,32 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
4844
4858
|
return getFirstReferenceInProject(firstReference);
|
|
4845
4859
|
};
|
|
4846
4860
|
|
|
4861
|
+
// An url written by an injection cannot be resolved: the placeholder is still there
|
|
4862
|
+
// when references are analyzed. Rather than guessing what a placeholder looks like
|
|
4863
|
+
// (the key is free-form), tell the file it comes from: injections are configured for it.
|
|
4864
|
+
const detailsFromInjectionsOnOwner = (reference) => {
|
|
4865
|
+
if (!reference) {
|
|
4866
|
+
return {};
|
|
4867
|
+
}
|
|
4868
|
+
const ownerUrlInfo = reference.ownerUrlInfo;
|
|
4869
|
+
if (ownerUrlInfo.type !== "html") {
|
|
4870
|
+
// "jsenv-ignore" is an html attribute
|
|
4871
|
+
return {};
|
|
4872
|
+
}
|
|
4873
|
+
const { hasInjections } = ownerUrlInfo.context;
|
|
4874
|
+
if (!hasInjections || !hasInjections(ownerUrlInfo.url)) {
|
|
4875
|
+
return {};
|
|
4876
|
+
}
|
|
4877
|
+
const { node, attributeName } = reference.astInfo || {};
|
|
4878
|
+
if (!node || !attributeName) {
|
|
4879
|
+
return {};
|
|
4880
|
+
}
|
|
4881
|
+
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:
|
|
4883
|
+
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
4884
|
+
};
|
|
4885
|
+
};
|
|
4886
|
+
|
|
4847
4887
|
const detailsFromPluginController = (jsenvPluginsController) => {
|
|
4848
4888
|
const currentPlugin = jsenvPluginsController.getCurrentPlugin();
|
|
4849
4889
|
if (!currentPlugin) {
|
|
@@ -4986,11 +5026,21 @@ const jsenvPluginDirectoryReferenceEffect = (
|
|
|
4986
5026
|
|
|
4987
5027
|
const injectionSymbol = Symbol.for("jsenv_injection");
|
|
4988
5028
|
const INJECTIONS = {
|
|
5029
|
+
/**
|
|
5030
|
+
* Inject `Object.assign(window, { [key]: value })` at the top of the file
|
|
5031
|
+
* (into a script for html, into the module itself for js) instead of
|
|
5032
|
+
* replacing a placeholder: the value is read at runtime as a global.
|
|
5033
|
+
*/
|
|
4989
5034
|
global: (value) => {
|
|
4990
5035
|
return { [injectionSymbol]: "global", value };
|
|
4991
5036
|
},
|
|
5037
|
+
/**
|
|
5038
|
+
* Replace the placeholder when the file contains it, stay silent when it does not
|
|
5039
|
+
* (without this a missing placeholder is reported as a warning).
|
|
5040
|
+
*/
|
|
4992
5041
|
optional: (value) => {
|
|
4993
|
-
if (value && value[injectionSymbol]
|
|
5042
|
+
if (value && value[injectionSymbol]) {
|
|
5043
|
+
// a global injection is not a placeholder, it can't be missing from the file
|
|
4994
5044
|
return value;
|
|
4995
5045
|
}
|
|
4996
5046
|
return { [injectionSymbol]: "optional", value };
|
|
@@ -5095,12 +5145,7 @@ return {
|
|
|
5095
5145
|
magicSource.replace({
|
|
5096
5146
|
start,
|
|
5097
5147
|
end,
|
|
5098
|
-
replacement:
|
|
5099
|
-
urlInfo.type === "js_classic" ||
|
|
5100
|
-
urlInfo.type === "js_module" ||
|
|
5101
|
-
urlInfo.type === "html"
|
|
5102
|
-
? JSON.stringify(value, null, " ")
|
|
5103
|
-
: value,
|
|
5148
|
+
replacement: asReplacement(value, urlInfo),
|
|
5104
5149
|
});
|
|
5105
5150
|
index = content.indexOf(key, end);
|
|
5106
5151
|
}
|
|
@@ -5108,6 +5153,19 @@ return {
|
|
|
5108
5153
|
return magicSource.toContentAndSourcemap();
|
|
5109
5154
|
};
|
|
5110
5155
|
|
|
5156
|
+
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
5157
|
+
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
5158
|
+
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
5159
|
+
const asReplacement = (value, urlInfo) => {
|
|
5160
|
+
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
|
|
5161
|
+
return JSON.stringify(value, null, " ");
|
|
5162
|
+
}
|
|
5163
|
+
if (typeof value === "string") {
|
|
5164
|
+
return value;
|
|
5165
|
+
}
|
|
5166
|
+
return JSON.stringify(value, null, " ");
|
|
5167
|
+
};
|
|
5168
|
+
|
|
5111
5169
|
const injectGlobals = (content, globals, urlInfo) => {
|
|
5112
5170
|
if (urlInfo.type === "html") {
|
|
5113
5171
|
return globalInjectorOnHtml(content, globals, urlInfo);
|
|
@@ -5115,7 +5173,14 @@ const injectGlobals = (content, globals, urlInfo) => {
|
|
|
5115
5173
|
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
|
|
5116
5174
|
return globalsInjectorOnJs(content, globals, urlInfo);
|
|
5117
5175
|
}
|
|
5118
|
-
throw new Error(
|
|
5176
|
+
throw new Error(
|
|
5177
|
+
createDetailedMessage(`cannot inject globals into "${urlInfo.type}"`, {
|
|
5178
|
+
file: urlInfo.url,
|
|
5179
|
+
...(urlInfo.isInline
|
|
5180
|
+
? { "inline content of": urlInfo.inlineUrlSite.url }
|
|
5181
|
+
: {}),
|
|
5182
|
+
}),
|
|
5183
|
+
);
|
|
5119
5184
|
};
|
|
5120
5185
|
const globalInjectorOnHtml = (content, globals, urlInfo) => {
|
|
5121
5186
|
// ideally we would inject an importmap but browser support is too low
|
|
@@ -5181,18 +5246,52 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5181
5246
|
{ injectionsGetter: rawAssociations },
|
|
5182
5247
|
context.rootDirectoryUrl,
|
|
5183
5248
|
);
|
|
5184
|
-
|
|
5249
|
+
const findInjectionsGetterForUrl = (url) => {
|
|
5185
5250
|
const { injectionsGetter } = URL_META.applyAssociations({
|
|
5186
|
-
url: asUrlWithoutSearch(
|
|
5251
|
+
url: asUrlWithoutSearch(url),
|
|
5187
5252
|
associations: resolvedAssociations,
|
|
5188
5253
|
});
|
|
5189
|
-
|
|
5254
|
+
return injectionsGetter;
|
|
5255
|
+
};
|
|
5256
|
+
// an url written by an injection cannot be resolved during reference analysis;
|
|
5257
|
+
// errors use this to tell the file holds injections and suggest "jsenv-ignore"
|
|
5258
|
+
context.hasInjections = (url) => {
|
|
5259
|
+
return Boolean(findInjectionsGetterForUrl(url));
|
|
5260
|
+
};
|
|
5261
|
+
const findInjectionsGetter = (urlInfo) => {
|
|
5262
|
+
const injectionsGetter = findInjectionsGetterForUrl(urlInfo.url);
|
|
5263
|
+
if (injectionsGetter) {
|
|
5264
|
+
return { injectionsGetter, isInherited: false };
|
|
5265
|
+
}
|
|
5266
|
+
if (urlInfo.isInline) {
|
|
5267
|
+
// content inlined into a file (a <script> inside html) is authored in that
|
|
5268
|
+
// file, so injections configured for the file must reach it too
|
|
5269
|
+
const found = findInjectionsGetter(
|
|
5270
|
+
urlInfo.firstReference.ownerUrlInfo,
|
|
5271
|
+
);
|
|
5272
|
+
if (found) {
|
|
5273
|
+
return {
|
|
5274
|
+
injectionsGetter: found.injectionsGetter,
|
|
5275
|
+
isInherited: true,
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
return null;
|
|
5280
|
+
};
|
|
5281
|
+
getInjections = async (urlInfo) => {
|
|
5282
|
+
const found = findInjectionsGetter(urlInfo);
|
|
5283
|
+
if (!found) {
|
|
5190
5284
|
return null;
|
|
5191
5285
|
}
|
|
5286
|
+
const { injectionsGetter, isInherited } = found;
|
|
5192
5287
|
if (typeof injectionsGetter !== "function") {
|
|
5193
5288
|
throw new TypeError("injectionsGetter must be a function");
|
|
5194
5289
|
}
|
|
5195
|
-
|
|
5290
|
+
const injections = await injectionsGetter(urlInfo);
|
|
5291
|
+
if (!injections || !isInherited) {
|
|
5292
|
+
return injections;
|
|
5293
|
+
}
|
|
5294
|
+
return asInheritedInjections(injections);
|
|
5196
5295
|
};
|
|
5197
5296
|
}
|
|
5198
5297
|
},
|
|
@@ -5203,13 +5302,12 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5203
5302
|
contentInjections: defaultInjections,
|
|
5204
5303
|
};
|
|
5205
5304
|
}
|
|
5206
|
-
const
|
|
5207
|
-
if (!
|
|
5305
|
+
const injections = await getInjections(urlInfo);
|
|
5306
|
+
if (!injections) {
|
|
5208
5307
|
return {
|
|
5209
5308
|
contentInjections: defaultInjections,
|
|
5210
5309
|
};
|
|
5211
5310
|
}
|
|
5212
|
-
const injections = await injectionsResult;
|
|
5213
5311
|
return {
|
|
5214
5312
|
contentInjections: {
|
|
5215
5313
|
...defaultInjections,
|
|
@@ -5220,6 +5318,26 @@ const jsenvPluginInjections = (rawAssociations) => {
|
|
|
5220
5318
|
};
|
|
5221
5319
|
};
|
|
5222
5320
|
|
|
5321
|
+
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
5322
|
+
// and inherits its injections, with two adjustments:
|
|
5323
|
+
// - a global belongs to the file itself, injecting it into each inline content would
|
|
5324
|
+
// repeat it and reach types that cannot receive globals (css)
|
|
5325
|
+
// - a placeholder configured for the file is expected in one of its inline contents,
|
|
5326
|
+
// not in each, so a missing one is not worth a warning
|
|
5327
|
+
const asInheritedInjections = (injections) => {
|
|
5328
|
+
const inheritedInjections = {};
|
|
5329
|
+
for (const key of Object.keys(injections)) {
|
|
5330
|
+
const value = injections[key];
|
|
5331
|
+
if (isPlaceholderInjection(value)) {
|
|
5332
|
+
inheritedInjections[key] = INJECTIONS.optional(value);
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
if (Object.keys(inheritedInjections).length === 0) {
|
|
5336
|
+
return null;
|
|
5337
|
+
}
|
|
5338
|
+
return inheritedInjections;
|
|
5339
|
+
};
|
|
5340
|
+
|
|
5223
5341
|
const jsenvPluginInliningAsDataUrl = () => {
|
|
5224
5342
|
return {
|
|
5225
5343
|
name: "jsenv:inlining_as_data_url",
|
|
@@ -11434,6 +11552,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
11434
11552
|
* @param {boolean} [params.ribbon=true] - The dev "ribbon" overlay.
|
|
11435
11553
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
11436
11554
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
11555
|
+
* @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`.
|
|
11437
11556
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
11438
11557
|
* @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
|
|
11439
11558
|
* @param {AbortSignal} [params.signal] - Abort to stop the server.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.4.
|
|
3
|
+
"version": "41.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@jsenv/ast": "6.8.
|
|
76
|
-
"@jsenv/js-module-fallback": "1.4.
|
|
75
|
+
"@jsenv/ast": "6.8.5",
|
|
76
|
+
"@jsenv/js-module-fallback": "1.4.38",
|
|
77
77
|
"@jsenv/plugin-bundling": "2.10.16",
|
|
78
|
-
"@jsenv/plugin-minification": "1.7.
|
|
79
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
80
|
-
"@jsenv/plugin-transpilation": "1.5.
|
|
78
|
+
"@jsenv/plugin-minification": "1.7.6",
|
|
79
|
+
"@jsenv/plugin-supervisor": "1.8.9",
|
|
80
|
+
"@jsenv/plugin-transpilation": "1.5.79",
|
|
81
81
|
"@jsenv/server": "17.6.0",
|
|
82
82
|
"@jsenv/sourcemap": "1.4.2",
|
|
83
83
|
"react-table": "7.8.0"
|
package/src/build/build.js
CHANGED
|
@@ -133,6 +133,25 @@ import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
|
|
|
133
133
|
* How URLs are versioned for this entry point (defaults to "search_param")
|
|
134
134
|
* @param {('none'|'inline'|'file'|'programmatic')} [entryPoint.sourcemaps]
|
|
135
135
|
* Sourcemap generation strategy for this entry point (defaults to "none")
|
|
136
|
+
* @param {object} [entryPoint.injections]
|
|
137
|
+
* Values to inject into files, as { urlPattern: getInjections }.
|
|
138
|
+
* Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
|
|
139
|
+
* values are functions receiving urlInfo and returning (or resolving to)
|
|
140
|
+
* an object of placeholders to replace, named `__LIKE_THIS__` by convention:
|
|
141
|
+
*
|
|
142
|
+
* injections: {
|
|
143
|
+
* "./index.html": () => ({ __BACKEND_URL__: "https://api.example.com" }),
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* In JS files the value is injected as a JS literal (a string value brings its own quotes),
|
|
147
|
+
* everywhere else it is injected as-is, so it can be concatenated:
|
|
148
|
+
* `href="__BACKEND_URL__/users/me"`.
|
|
149
|
+
* An html url pattern also covers what is inlined in that html: an inline
|
|
150
|
+
* `<script>window.backendUrl = __BACKEND_URL__;</script>` gets the JS literal,
|
|
151
|
+
* which is how a value is shared with every js file of the page.
|
|
152
|
+
* Use INJECTIONS.optional(value) for a placeholder that may be absent from the file
|
|
153
|
+
* and INJECTIONS.global(value) to inject `Object.assign(window, { ... })` instead of
|
|
154
|
+
* replacing a placeholder.
|
|
136
155
|
*
|
|
137
156
|
* @return {Promise<Object>} buildReturnValue
|
|
138
157
|
* @return {Promise<Object>} [buildReturnValue.buildInlineContents]
|
|
@@ -170,6 +189,17 @@ export const build = async ({
|
|
|
170
189
|
{
|
|
171
190
|
const unexpectedParamNames = Object.keys(rest);
|
|
172
191
|
if (unexpectedParamNames.length > 0) {
|
|
192
|
+
const entryPointParamNames = unexpectedParamNames.filter((name) =>
|
|
193
|
+
Object.hasOwn(entryPointDefaultParams, name),
|
|
194
|
+
);
|
|
195
|
+
if (entryPointParamNames.length > 0) {
|
|
196
|
+
throw new TypeError(
|
|
197
|
+
`${entryPointParamNames.join(",")}: param(s) configured per entry point, move them into entryPoints, as in:
|
|
198
|
+
entryPoints: {
|
|
199
|
+
"./index.html": { ${entryPointParamNames.map((name) => `${name}: ...`).join(", ")} },
|
|
200
|
+
}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
173
203
|
throw new TypeError(
|
|
174
204
|
`${unexpectedParamNames.join(",")}: there is no such param`,
|
|
175
205
|
);
|
|
@@ -898,6 +898,11 @@ export const createBuildSpecifierManager = ({
|
|
|
898
898
|
registerHtmlRefine((htmlAst, { registerHtmlMutation }) => {
|
|
899
899
|
visitHtmlNodes(htmlAst, {
|
|
900
900
|
link: (node) => {
|
|
901
|
+
if (getHtmlNodeAttribute(node, "jsenv-ignore") !== undefined) {
|
|
902
|
+
// reference analysis skipped this node, so it has no urlInfo in the graph
|
|
903
|
+
// and there is nothing to resync
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
901
906
|
const href = getHtmlNodeAttribute(node, "href");
|
|
902
907
|
if (href === undefined || href.startsWith("data:")) {
|
|
903
908
|
return;
|
|
@@ -51,6 +51,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
51
51
|
* @param {boolean} [params.ribbon=true] - The dev "ribbon" overlay.
|
|
52
52
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
53
53
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
54
|
+
* @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`.
|
|
54
55
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
55
56
|
* @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
|
|
56
57
|
* @param {AbortSignal} [params.signal] - Abort to stop the server.
|
package/src/kitchen/errors.js
CHANGED
|
@@ -78,6 +78,7 @@ ${reason}`,
|
|
|
78
78
|
}
|
|
79
79
|
return createFailedToResolveUrlError({
|
|
80
80
|
reason: `An error occured during specifier resolution`,
|
|
81
|
+
...detailsFromInjectionsOnOwner(reference),
|
|
81
82
|
...detailsFromValueThrown(error),
|
|
82
83
|
});
|
|
83
84
|
};
|
|
@@ -139,6 +140,7 @@ ${reason}`,
|
|
|
139
140
|
return createFailedToFetchUrlContentError({
|
|
140
141
|
code: "NOT_FOUND",
|
|
141
142
|
reason: "no entry on filesystem",
|
|
143
|
+
...detailsFromInjectionsOnOwner(urlInfo.firstReference),
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -371,6 +373,32 @@ const getFirstReferenceInProject = (reference) => {
|
|
|
371
373
|
return getFirstReferenceInProject(firstReference);
|
|
372
374
|
};
|
|
373
375
|
|
|
376
|
+
// An url written by an injection cannot be resolved: the placeholder is still there
|
|
377
|
+
// when references are analyzed. Rather than guessing what a placeholder looks like
|
|
378
|
+
// (the key is free-form), tell the file it comes from: injections are configured for it.
|
|
379
|
+
const detailsFromInjectionsOnOwner = (reference) => {
|
|
380
|
+
if (!reference) {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
const ownerUrlInfo = reference.ownerUrlInfo;
|
|
384
|
+
if (ownerUrlInfo.type !== "html") {
|
|
385
|
+
// "jsenv-ignore" is an html attribute
|
|
386
|
+
return {};
|
|
387
|
+
}
|
|
388
|
+
const { hasInjections } = ownerUrlInfo.context;
|
|
389
|
+
if (!hasInjections || !hasInjections(ownerUrlInfo.url)) {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
const { node, attributeName } = reference.astInfo || {};
|
|
393
|
+
if (!node || !attributeName) {
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
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
|
+
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
374
402
|
const detailsFromPluginController = (jsenvPluginsController) => {
|
|
375
403
|
const currentPlugin = jsenvPluginsController.getCurrentPlugin();
|
|
376
404
|
if (!currentPlugin) {
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
|
|
2
|
+
import { createDetailedMessage } from "@jsenv/humanize";
|
|
2
3
|
import { composeTwoSourcemaps, createMagicSource } from "@jsenv/sourcemap";
|
|
3
4
|
|
|
4
5
|
const injectionSymbol = Symbol.for("jsenv_injection");
|
|
5
6
|
export const INJECTIONS = {
|
|
7
|
+
/**
|
|
8
|
+
* Inject `Object.assign(window, { [key]: value })` at the top of the file
|
|
9
|
+
* (into a script for html, into the module itself for js) instead of
|
|
10
|
+
* replacing a placeholder: the value is read at runtime as a global.
|
|
11
|
+
*/
|
|
6
12
|
global: (value) => {
|
|
7
13
|
return { [injectionSymbol]: "global", value };
|
|
8
14
|
},
|
|
15
|
+
/**
|
|
16
|
+
* Replace the placeholder when the file contains it, stay silent when it does not
|
|
17
|
+
* (without this a missing placeholder is reported as a warning).
|
|
18
|
+
*/
|
|
9
19
|
optional: (value) => {
|
|
10
|
-
if (value && value[injectionSymbol]
|
|
20
|
+
if (value && value[injectionSymbol]) {
|
|
21
|
+
// a global injection is not a placeholder, it can't be missing from the file
|
|
11
22
|
return value;
|
|
12
23
|
}
|
|
13
24
|
return { [injectionSymbol]: "optional", value };
|
|
@@ -112,12 +123,7 @@ return {
|
|
|
112
123
|
magicSource.replace({
|
|
113
124
|
start,
|
|
114
125
|
end,
|
|
115
|
-
replacement:
|
|
116
|
-
urlInfo.type === "js_classic" ||
|
|
117
|
-
urlInfo.type === "js_module" ||
|
|
118
|
-
urlInfo.type === "html"
|
|
119
|
-
? JSON.stringify(value, null, " ")
|
|
120
|
-
: value,
|
|
126
|
+
replacement: asReplacement(value, urlInfo),
|
|
121
127
|
});
|
|
122
128
|
index = content.indexOf(key, end);
|
|
123
129
|
}
|
|
@@ -125,6 +131,19 @@ return {
|
|
|
125
131
|
return magicSource.toContentAndSourcemap();
|
|
126
132
|
};
|
|
127
133
|
|
|
134
|
+
// In JS the placeholder stands for a value, so it must be substituted by a literal.
|
|
135
|
+
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
|
|
136
|
+
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
|
|
137
|
+
const asReplacement = (value, urlInfo) => {
|
|
138
|
+
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
|
|
139
|
+
return JSON.stringify(value, null, " ");
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === "string") {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify(value, null, " ");
|
|
145
|
+
};
|
|
146
|
+
|
|
128
147
|
export const injectGlobals = (content, globals, urlInfo) => {
|
|
129
148
|
if (urlInfo.type === "html") {
|
|
130
149
|
return globalInjectorOnHtml(content, globals, urlInfo);
|
|
@@ -132,7 +151,14 @@ export const injectGlobals = (content, globals, urlInfo) => {
|
|
|
132
151
|
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
|
|
133
152
|
return globalsInjectorOnJs(content, globals, urlInfo);
|
|
134
153
|
}
|
|
135
|
-
throw new Error(
|
|
154
|
+
throw new Error(
|
|
155
|
+
createDetailedMessage(`cannot inject globals into "${urlInfo.type}"`, {
|
|
156
|
+
file: urlInfo.url,
|
|
157
|
+
...(urlInfo.isInline
|
|
158
|
+
? { "inline content of": urlInfo.inlineUrlSite.url }
|
|
159
|
+
: {}),
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
136
162
|
};
|
|
137
163
|
const globalInjectorOnHtml = (content, globals, urlInfo) => {
|
|
138
164
|
// ideally we would inject an importmap but browser support is too low
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { URL_META } from "@jsenv/url-meta";
|
|
2
2
|
import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
INJECTIONS,
|
|
5
|
+
isPlaceholderInjection,
|
|
6
|
+
} from "../../kitchen/url_graph/url_info_injections.js";
|
|
4
7
|
|
|
5
8
|
export const jsenvPluginInjections = (rawAssociations) => {
|
|
6
9
|
const getDefaultInjections = (urlInfo) => {
|
|
@@ -26,18 +29,52 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
26
29
|
{ injectionsGetter: rawAssociations },
|
|
27
30
|
context.rootDirectoryUrl,
|
|
28
31
|
);
|
|
29
|
-
|
|
32
|
+
const findInjectionsGetterForUrl = (url) => {
|
|
30
33
|
const { injectionsGetter } = URL_META.applyAssociations({
|
|
31
|
-
url: asUrlWithoutSearch(
|
|
34
|
+
url: asUrlWithoutSearch(url),
|
|
32
35
|
associations: resolvedAssociations,
|
|
33
36
|
});
|
|
34
|
-
|
|
37
|
+
return injectionsGetter;
|
|
38
|
+
};
|
|
39
|
+
// an url written by an injection cannot be resolved during reference analysis;
|
|
40
|
+
// errors use this to tell the file holds injections and suggest "jsenv-ignore"
|
|
41
|
+
context.hasInjections = (url) => {
|
|
42
|
+
return Boolean(findInjectionsGetterForUrl(url));
|
|
43
|
+
};
|
|
44
|
+
const findInjectionsGetter = (urlInfo) => {
|
|
45
|
+
const injectionsGetter = findInjectionsGetterForUrl(urlInfo.url);
|
|
46
|
+
if (injectionsGetter) {
|
|
47
|
+
return { injectionsGetter, isInherited: false };
|
|
48
|
+
}
|
|
49
|
+
if (urlInfo.isInline) {
|
|
50
|
+
// content inlined into a file (a <script> inside html) is authored in that
|
|
51
|
+
// file, so injections configured for the file must reach it too
|
|
52
|
+
const found = findInjectionsGetter(
|
|
53
|
+
urlInfo.firstReference.ownerUrlInfo,
|
|
54
|
+
);
|
|
55
|
+
if (found) {
|
|
56
|
+
return {
|
|
57
|
+
injectionsGetter: found.injectionsGetter,
|
|
58
|
+
isInherited: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
getInjections = async (urlInfo) => {
|
|
65
|
+
const found = findInjectionsGetter(urlInfo);
|
|
66
|
+
if (!found) {
|
|
35
67
|
return null;
|
|
36
68
|
}
|
|
69
|
+
const { injectionsGetter, isInherited } = found;
|
|
37
70
|
if (typeof injectionsGetter !== "function") {
|
|
38
71
|
throw new TypeError("injectionsGetter must be a function");
|
|
39
72
|
}
|
|
40
|
-
|
|
73
|
+
const injections = await injectionsGetter(urlInfo);
|
|
74
|
+
if (!injections || !isInherited) {
|
|
75
|
+
return injections;
|
|
76
|
+
}
|
|
77
|
+
return asInheritedInjections(injections);
|
|
41
78
|
};
|
|
42
79
|
}
|
|
43
80
|
},
|
|
@@ -48,13 +85,12 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
48
85
|
contentInjections: defaultInjections,
|
|
49
86
|
};
|
|
50
87
|
}
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
88
|
+
const injections = await getInjections(urlInfo);
|
|
89
|
+
if (!injections) {
|
|
53
90
|
return {
|
|
54
91
|
contentInjections: defaultInjections,
|
|
55
92
|
};
|
|
56
93
|
}
|
|
57
|
-
const injections = await injectionsResult;
|
|
58
94
|
return {
|
|
59
95
|
contentInjections: {
|
|
60
96
|
...defaultInjections,
|
|
@@ -64,3 +100,23 @@ export const jsenvPluginInjections = (rawAssociations) => {
|
|
|
64
100
|
},
|
|
65
101
|
};
|
|
66
102
|
};
|
|
103
|
+
|
|
104
|
+
// What a file inlines (a <script> or a <style> inside html) is authored in that file
|
|
105
|
+
// and inherits its injections, with two adjustments:
|
|
106
|
+
// - a global belongs to the file itself, injecting it into each inline content would
|
|
107
|
+
// repeat it and reach types that cannot receive globals (css)
|
|
108
|
+
// - a placeholder configured for the file is expected in one of its inline contents,
|
|
109
|
+
// not in each, so a missing one is not worth a warning
|
|
110
|
+
const asInheritedInjections = (injections) => {
|
|
111
|
+
const inheritedInjections = {};
|
|
112
|
+
for (const key of Object.keys(injections)) {
|
|
113
|
+
const value = injections[key];
|
|
114
|
+
if (isPlaceholderInjection(value)) {
|
|
115
|
+
inheritedInjections[key] = INJECTIONS.optional(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (Object.keys(inheritedInjections).length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return inheritedInjections;
|
|
122
|
+
};
|