@jsenv/core 40.6.1 → 40.7.0
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/browserslist_index/browserslist_index.js +1 -0
- package/dist/build/build.js +335 -135
- package/dist/build/jsenv_core_node_modules.js +3 -4
- package/dist/build/jsenv_core_packages.js +100 -102
- package/dist/jsenv_core.js +4 -0
- package/dist/start_build_server/jsenv_core_node_modules.js +3 -4
- package/dist/start_build_server/jsenv_core_packages.js +29 -29
- package/dist/start_dev_server/jsenv_core_node_modules.js +3 -4
- package/dist/start_dev_server/jsenv_core_packages.js +100 -102
- package/dist/start_dev_server/start_dev_server.js +338 -136
- package/package.json +10 -11
- package/src/dev/start_dev_server.js +3 -1
- package/src/kitchen/kitchen.js +2 -0
- package/src/kitchen/url_graph/url_graph.js +1 -0
- package/src/kitchen/url_graph/url_info_injections.js +172 -0
- package/src/kitchen/url_graph/url_info_transformations.js +28 -7
- package/src/main.js +1 -1
- package/src/plugins/global_scenarios/jsenv_plugin_global_scenarios.js +4 -9
- package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +2 -0
- package/src/plugins/injections/jsenv_plugin_injections.js +51 -85
- package/src/plugins/plugin_controller.js +3 -0
- package/src/plugins/plugins.js +3 -1
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +23 -22
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +41 -3
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +2 -0
- package/src/plugins/injections/internal/inject_globals.js +0 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "40.
|
|
3
|
+
"version": "40.7.0",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
},
|
|
40
40
|
"packageManager": "npm@11.3.0",
|
|
41
41
|
"workspaces": [
|
|
42
|
-
"./packages/back_and_front/*",
|
|
43
42
|
"./packages/backend/*",
|
|
44
43
|
"./packages/frontend/*",
|
|
45
44
|
"./packages/internal/*",
|
|
@@ -82,12 +81,12 @@
|
|
|
82
81
|
},
|
|
83
82
|
"dependencies": {
|
|
84
83
|
"@financial-times/polyfill-useragent-normaliser": "1.10.2",
|
|
85
|
-
"@jsenv/ast": "6.7.
|
|
86
|
-
"@jsenv/js-module-fallback": "1.4.
|
|
84
|
+
"@jsenv/ast": "6.7.3",
|
|
85
|
+
"@jsenv/js-module-fallback": "1.4.14",
|
|
87
86
|
"@jsenv/plugin-bundling": "2.9.7",
|
|
88
87
|
"@jsenv/plugin-minification": "1.7.0",
|
|
89
|
-
"@jsenv/plugin-supervisor": "1.7.
|
|
90
|
-
"@jsenv/plugin-transpilation": "1.5.
|
|
88
|
+
"@jsenv/plugin-supervisor": "1.7.2",
|
|
89
|
+
"@jsenv/plugin-transpilation": "1.5.21",
|
|
91
90
|
"@jsenv/server": "16.1.2",
|
|
92
91
|
"@jsenv/sourcemap": "1.3.8"
|
|
93
92
|
},
|
|
@@ -120,13 +119,13 @@
|
|
|
120
119
|
"@jsenv/url-meta": "workspace:*",
|
|
121
120
|
"@jsenv/urls": "workspace:*",
|
|
122
121
|
"@jsenv/utils": "workspace:*",
|
|
123
|
-
"@playwright/browser-chromium": "1.
|
|
124
|
-
"@playwright/browser-firefox": "1.
|
|
125
|
-
"@playwright/browser-webkit": "1.
|
|
122
|
+
"@playwright/browser-chromium": "1.52.0",
|
|
123
|
+
"@playwright/browser-firefox": "1.52.0",
|
|
124
|
+
"@playwright/browser-webkit": "1.52.0",
|
|
126
125
|
"babel-plugin-transform-async-to-promises": "0.8.18",
|
|
127
|
-
"eslint": "9.
|
|
126
|
+
"eslint": "9.25.1",
|
|
128
127
|
"open": "10.1.1",
|
|
129
|
-
"playwright": "1.
|
|
128
|
+
"playwright": "1.52.0",
|
|
130
129
|
"preact": "10.26.5",
|
|
131
130
|
"prettier": "3.5.3",
|
|
132
131
|
"prettier-plugin-organize-imports": "4.1.0",
|
|
@@ -96,6 +96,7 @@ export const startDevServer = async ({
|
|
|
96
96
|
ribbon = true,
|
|
97
97
|
// toolbar = false,
|
|
98
98
|
onKitchenCreated = () => {},
|
|
99
|
+
spa,
|
|
99
100
|
|
|
100
101
|
sourcemaps = "inline",
|
|
101
102
|
sourcemapsSourcesContent,
|
|
@@ -265,6 +266,7 @@ export const startDevServer = async ({
|
|
|
265
266
|
supervisor,
|
|
266
267
|
injections,
|
|
267
268
|
transpilation,
|
|
269
|
+
spa,
|
|
268
270
|
|
|
269
271
|
clientAutoreload,
|
|
270
272
|
clientAutoreloadOnServerRestart,
|
|
@@ -643,7 +645,7 @@ export const startDevServer = async ({
|
|
|
643
645
|
body: response.body,
|
|
644
646
|
});
|
|
645
647
|
return {
|
|
646
|
-
status:
|
|
648
|
+
status: response.status,
|
|
647
649
|
headers: {
|
|
648
650
|
"content-type": "application/json",
|
|
649
651
|
"content-length": Buffer.byteLength(body),
|
package/src/kitchen/kitchen.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
determineSourcemapFileUrl,
|
|
18
18
|
} from "./out_directory_url.js";
|
|
19
19
|
import { createUrlGraph } from "./url_graph/url_graph.js";
|
|
20
|
+
import { isPlaceholderInjection } from "./url_graph/url_info_injections.js";
|
|
20
21
|
import { createUrlInfoTransformer } from "./url_graph/url_info_transformations.js";
|
|
21
22
|
import { urlSpecifierEncoding } from "./url_graph/url_specifier_encoding.js";
|
|
22
23
|
|
|
@@ -102,6 +103,7 @@ export const createKitchen = ({
|
|
|
102
103
|
inlineContentClientFileUrl,
|
|
103
104
|
isSupportedOnCurrentClients: memoizeIsSupported(clientRuntimeCompat),
|
|
104
105
|
isSupportedOnFutureClients: memoizeIsSupported(runtimeCompat),
|
|
106
|
+
isPlaceholderInjection,
|
|
105
107
|
getPluginMeta: null,
|
|
106
108
|
sourcemaps,
|
|
107
109
|
outDirectoryUrl,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
|
|
2
|
+
import { composeTwoSourcemaps, createMagicSource } from "@jsenv/sourcemap";
|
|
3
|
+
|
|
4
|
+
const injectionSymbol = Symbol.for("jsenv_injection");
|
|
5
|
+
export const INJECTIONS = {
|
|
6
|
+
global: (value) => {
|
|
7
|
+
return { [injectionSymbol]: "global", value };
|
|
8
|
+
},
|
|
9
|
+
optional: (value) => {
|
|
10
|
+
return { [injectionSymbol]: "optional", value };
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isPlaceholderInjection = (value) => {
|
|
15
|
+
return (
|
|
16
|
+
!value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const applyContentInjections = (content, contentInjections, urlInfo) => {
|
|
21
|
+
const keys = Object.keys(contentInjections);
|
|
22
|
+
const globals = {};
|
|
23
|
+
const placeholderReplacements = [];
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
const contentInjection = contentInjections[key];
|
|
26
|
+
if (contentInjection && contentInjection[injectionSymbol]) {
|
|
27
|
+
const valueBehindSymbol = contentInjection[injectionSymbol];
|
|
28
|
+
if (valueBehindSymbol === "global") {
|
|
29
|
+
globals[key] = contentInjection.value;
|
|
30
|
+
} else if (valueBehindSymbol === "optional") {
|
|
31
|
+
placeholderReplacements.push({
|
|
32
|
+
key,
|
|
33
|
+
isOptional: true,
|
|
34
|
+
value: contentInjection.value,
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(`unknown injection type "${valueBehindSymbol}"`);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
placeholderReplacements.push({
|
|
41
|
+
key,
|
|
42
|
+
value: contentInjection,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const needGlobalsInjection = Object.keys(globals).length > 0;
|
|
48
|
+
const needPlaceholderReplacements = placeholderReplacements.length > 0;
|
|
49
|
+
|
|
50
|
+
if (needGlobalsInjection && needPlaceholderReplacements) {
|
|
51
|
+
const globalInjectionResult = injectGlobals(content, globals, urlInfo);
|
|
52
|
+
const replaceInjectionResult = injectPlaceholderReplacements(
|
|
53
|
+
globalInjectionResult.content,
|
|
54
|
+
placeholderReplacements,
|
|
55
|
+
urlInfo,
|
|
56
|
+
);
|
|
57
|
+
return {
|
|
58
|
+
content: replaceInjectionResult.content,
|
|
59
|
+
sourcemap: composeTwoSourcemaps(
|
|
60
|
+
globalInjectionResult.sourcemap,
|
|
61
|
+
replaceInjectionResult.sourcemap,
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (needGlobalsInjection) {
|
|
66
|
+
return injectGlobals(content, globals, urlInfo);
|
|
67
|
+
}
|
|
68
|
+
if (needPlaceholderReplacements) {
|
|
69
|
+
return injectPlaceholderReplacements(
|
|
70
|
+
content,
|
|
71
|
+
placeholderReplacements,
|
|
72
|
+
urlInfo,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const injectPlaceholderReplacements = (
|
|
79
|
+
content,
|
|
80
|
+
placeholderReplacements,
|
|
81
|
+
urlInfo,
|
|
82
|
+
) => {
|
|
83
|
+
const magicSource = createMagicSource(content);
|
|
84
|
+
for (const { key, isOptional, value } of placeholderReplacements) {
|
|
85
|
+
let index = content.indexOf(key);
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
if (!isOptional) {
|
|
88
|
+
urlInfo.context.logger.warn(
|
|
89
|
+
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
90
|
+
--- suggestion a ---
|
|
91
|
+
Add "${key}" in that file.
|
|
92
|
+
--- suggestion b ---
|
|
93
|
+
Fix eventual typo in "${key}"?
|
|
94
|
+
--- suggestion c ---
|
|
95
|
+
Mark injection as optional using INJECTIONS.optional():
|
|
96
|
+
import { INJECTIONS } from "@jsenv/core";
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"${key}": INJECTIONS.optional(${JSON.stringify(value)}),
|
|
100
|
+
};`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
while (index !== -1) {
|
|
107
|
+
const start = index;
|
|
108
|
+
const end = index + key.length;
|
|
109
|
+
magicSource.replace({
|
|
110
|
+
start,
|
|
111
|
+
end,
|
|
112
|
+
replacement:
|
|
113
|
+
urlInfo.type === "js_classic" ||
|
|
114
|
+
urlInfo.type === "js_module" ||
|
|
115
|
+
urlInfo.type === "html"
|
|
116
|
+
? JSON.stringify(value, null, " ")
|
|
117
|
+
: value,
|
|
118
|
+
});
|
|
119
|
+
index = content.indexOf(key, end);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return magicSource.toContentAndSourcemap();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const injectGlobals = (content, globals, urlInfo) => {
|
|
126
|
+
if (urlInfo.type === "html") {
|
|
127
|
+
return globalInjectorOnHtml(content, globals, urlInfo);
|
|
128
|
+
}
|
|
129
|
+
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
|
|
130
|
+
return globalsInjectorOnJs(content, globals, urlInfo);
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`cannot inject globals into "${urlInfo.type}"`);
|
|
133
|
+
};
|
|
134
|
+
const globalInjectorOnHtml = (content, globals, urlInfo) => {
|
|
135
|
+
// ideally we would inject an importmap but browser support is too low
|
|
136
|
+
// (even worse for worker/service worker)
|
|
137
|
+
// so for now we inject code into entry points
|
|
138
|
+
const htmlAst = parseHtml({
|
|
139
|
+
html: content,
|
|
140
|
+
url: urlInfo.url,
|
|
141
|
+
storeOriginalPositions: false,
|
|
142
|
+
});
|
|
143
|
+
const clientCode = generateClientCodeForGlobals(globals, {
|
|
144
|
+
isWebWorker: false,
|
|
145
|
+
});
|
|
146
|
+
injectJsenvScript(htmlAst, {
|
|
147
|
+
content: clientCode,
|
|
148
|
+
pluginName: "jsenv:inject_globals",
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
content: stringifyHtmlAst(htmlAst),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const globalsInjectorOnJs = (content, globals, urlInfo) => {
|
|
155
|
+
const clientCode = generateClientCodeForGlobals(globals, {
|
|
156
|
+
isWebWorker:
|
|
157
|
+
urlInfo.subtype === "worker" ||
|
|
158
|
+
urlInfo.subtype === "service_worker" ||
|
|
159
|
+
urlInfo.subtype === "shared_worker",
|
|
160
|
+
});
|
|
161
|
+
const magicSource = createMagicSource(content);
|
|
162
|
+
magicSource.prepend(clientCode);
|
|
163
|
+
return magicSource.toContentAndSourcemap();
|
|
164
|
+
};
|
|
165
|
+
const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
|
|
166
|
+
const globalName = isWebWorker ? "self" : "window";
|
|
167
|
+
return `Object.assign(${globalName}, ${JSON.stringify(
|
|
168
|
+
globals,
|
|
169
|
+
null,
|
|
170
|
+
" ",
|
|
171
|
+
)});`;
|
|
172
|
+
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
defineGettersOnPropertiesDerivedFromContent,
|
|
18
18
|
defineGettersOnPropertiesDerivedFromOriginalContent,
|
|
19
19
|
} from "./url_content.js";
|
|
20
|
+
import { applyContentInjections } from "./url_info_injections.js";
|
|
20
21
|
|
|
21
22
|
export const createUrlInfoTransformer = ({
|
|
22
23
|
logger,
|
|
@@ -195,6 +196,7 @@ export const createUrlInfoTransformer = ({
|
|
|
195
196
|
contentLength,
|
|
196
197
|
sourcemap,
|
|
197
198
|
sourcemapIsWrong,
|
|
199
|
+
contentInjections,
|
|
198
200
|
} = transformations;
|
|
199
201
|
if (type) {
|
|
200
202
|
urlInfo.type = type;
|
|
@@ -202,13 +204,23 @@ export const createUrlInfoTransformer = ({
|
|
|
202
204
|
if (contentType) {
|
|
203
205
|
urlInfo.contentType = contentType;
|
|
204
206
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
if (Object.hasOwn(transformations, "contentInjections")) {
|
|
208
|
+
if (contentInjections) {
|
|
209
|
+
Object.assign(urlInfo.contentInjections, contentInjections);
|
|
210
|
+
}
|
|
211
|
+
if (content === undefined) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let contentModified;
|
|
216
|
+
if (Object.hasOwn(transformations, "content")) {
|
|
217
|
+
contentModified = setContentProperties(urlInfo, {
|
|
218
|
+
content,
|
|
219
|
+
contentAst,
|
|
220
|
+
contentEtag,
|
|
221
|
+
contentLength,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
212
224
|
if (
|
|
213
225
|
sourcemap &&
|
|
214
226
|
mayHaveSourcemap(urlInfo) &&
|
|
@@ -400,6 +412,15 @@ export const createUrlInfoTransformer = ({
|
|
|
400
412
|
if (transformations) {
|
|
401
413
|
applyTransformations(urlInfo, transformations);
|
|
402
414
|
}
|
|
415
|
+
const { contentInjections } = urlInfo;
|
|
416
|
+
if (contentInjections && Object.keys(contentInjections).length > 0) {
|
|
417
|
+
const injectionTransformations = applyContentInjections(
|
|
418
|
+
urlInfo.content,
|
|
419
|
+
contentInjections,
|
|
420
|
+
urlInfo,
|
|
421
|
+
);
|
|
422
|
+
applyTransformations(urlInfo, injectionTransformations);
|
|
423
|
+
}
|
|
403
424
|
applyContentEffects(urlInfo);
|
|
404
425
|
urlInfo.contentFinalized = true;
|
|
405
426
|
};
|
package/src/main.js
CHANGED
|
@@ -5,21 +5,16 @@
|
|
|
5
5
|
* That will be replaced with true/false
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
INJECTIONS,
|
|
10
|
-
replacePlaceholders,
|
|
11
|
-
} from "../injections/jsenv_plugin_injections.js";
|
|
8
|
+
import { INJECTIONS } from "../../kitchen/url_graph/url_info_injections.js";
|
|
12
9
|
|
|
13
10
|
export const jsenvPluginGlobalScenarios = () => {
|
|
14
11
|
const transformIfNeeded = (urlInfo) => {
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
{
|
|
12
|
+
return {
|
|
13
|
+
contentInjections: {
|
|
18
14
|
__DEV__: INJECTIONS.optional(urlInfo.context.dev),
|
|
19
15
|
__BUILD__: INJECTIONS.optional(urlInfo.context.build),
|
|
20
16
|
},
|
|
21
|
-
|
|
22
|
-
);
|
|
17
|
+
};
|
|
23
18
|
};
|
|
24
19
|
|
|
25
20
|
return {
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - replaced by true: When scenario matches (import.meta.dev and it's the dev server)
|
|
7
7
|
* - left as is to be evaluated to undefined (import.meta.build but it's the dev server)
|
|
8
8
|
* - replaced by undefined (import.meta.dev but it's build; the goal is to ensure it's tree-shaked)
|
|
9
|
+
*
|
|
10
|
+
* TODO: ideally during dev we would keep import.meta.dev and ensure we set it to true rather than replacing it with true?
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { applyBabelPlugins } from "@jsenv/ast";
|
|
@@ -1,100 +1,66 @@
|
|
|
1
|
-
import { createMagicSource } from "@jsenv/sourcemap";
|
|
2
1
|
import { URL_META } from "@jsenv/url-meta";
|
|
3
|
-
import { asUrlWithoutSearch } from "@jsenv/urls";
|
|
2
|
+
import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
|
|
3
|
+
import { INJECTIONS } from "../../kitchen/url_graph/url_info_injections.js";
|
|
4
4
|
|
|
5
5
|
export const jsenvPluginInjections = (rawAssociations) => {
|
|
6
|
-
|
|
6
|
+
const getDefaultInjections = (urlInfo) => {
|
|
7
|
+
if (urlInfo.context.dev && urlInfo.type === "html") {
|
|
8
|
+
const relativeUrl = urlToRelativeUrl(
|
|
9
|
+
urlInfo.url,
|
|
10
|
+
urlInfo.context.rootDirectoryUrl,
|
|
11
|
+
);
|
|
12
|
+
return {
|
|
13
|
+
HTML_ROOT_PATHNAME: INJECTIONS.global(`/${relativeUrl}`),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
let getInjections = null;
|
|
7
19
|
|
|
8
20
|
return {
|
|
9
21
|
name: "jsenv:injections",
|
|
10
22
|
appliesDuring: "*",
|
|
11
23
|
init: (context) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
if (rawAssociations && Object.keys(rawAssociations).length > 0) {
|
|
25
|
+
const resolvedAssociations = URL_META.resolveAssociations(
|
|
26
|
+
{ injectionsGetter: rawAssociations },
|
|
27
|
+
context.rootDirectoryUrl,
|
|
28
|
+
);
|
|
29
|
+
getInjections = (urlInfo) => {
|
|
30
|
+
const { injectionsGetter } = URL_META.applyAssociations({
|
|
31
|
+
url: asUrlWithoutSearch(urlInfo.url),
|
|
32
|
+
associations: resolvedAssociations,
|
|
33
|
+
});
|
|
34
|
+
if (!injectionsGetter) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (typeof injectionsGetter !== "function") {
|
|
38
|
+
throw new TypeError("injectionsGetter must be a function");
|
|
39
|
+
}
|
|
40
|
+
return injectionsGetter(urlInfo);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
16
43
|
},
|
|
17
44
|
transformUrlContent: async (urlInfo) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
if (typeof injectionsGetter !== "function") {
|
|
26
|
-
throw new TypeError("injectionsGetter must be a function");
|
|
27
|
-
}
|
|
28
|
-
const injections = await injectionsGetter(urlInfo);
|
|
29
|
-
if (!injections) {
|
|
30
|
-
return null;
|
|
45
|
+
const defaultInjections = getDefaultInjections(urlInfo);
|
|
46
|
+
if (!getInjections) {
|
|
47
|
+
return {
|
|
48
|
+
contentInjections: defaultInjections,
|
|
49
|
+
};
|
|
31
50
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
51
|
+
const injectionsResult = getInjections(urlInfo);
|
|
52
|
+
if (!injectionsResult) {
|
|
53
|
+
return {
|
|
54
|
+
contentInjections: defaultInjections,
|
|
55
|
+
};
|
|
35
56
|
}
|
|
36
|
-
|
|
57
|
+
const injections = await injectionsResult;
|
|
58
|
+
return {
|
|
59
|
+
contentInjections: {
|
|
60
|
+
...defaultInjections,
|
|
61
|
+
...injections,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
37
64
|
},
|
|
38
65
|
};
|
|
39
66
|
};
|
|
40
|
-
|
|
41
|
-
const injectionSymbol = Symbol.for("jsenv_injection");
|
|
42
|
-
export const INJECTIONS = {
|
|
43
|
-
optional: (value) => {
|
|
44
|
-
return { [injectionSymbol]: "optional", value };
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// we export this because it is imported by jsenv_plugin_placeholder.js and unit test
|
|
49
|
-
export const replacePlaceholders = (content, replacements, urlInfo) => {
|
|
50
|
-
const magicSource = createMagicSource(content);
|
|
51
|
-
for (const key of Object.keys(replacements)) {
|
|
52
|
-
let index = content.indexOf(key);
|
|
53
|
-
const replacement = replacements[key];
|
|
54
|
-
let isOptional;
|
|
55
|
-
let value;
|
|
56
|
-
if (replacement && replacement[injectionSymbol]) {
|
|
57
|
-
const valueBehindSymbol = replacement[injectionSymbol];
|
|
58
|
-
isOptional = valueBehindSymbol === "optional";
|
|
59
|
-
value = replacement.value;
|
|
60
|
-
} else {
|
|
61
|
-
value = replacement;
|
|
62
|
-
}
|
|
63
|
-
if (index === -1) {
|
|
64
|
-
if (!isOptional) {
|
|
65
|
-
urlInfo.context.logger.warn(
|
|
66
|
-
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
67
|
-
--- suggestion a ---
|
|
68
|
-
Add "${key}" in that file.
|
|
69
|
-
--- suggestion b ---
|
|
70
|
-
Fix eventual typo in "${key}"?
|
|
71
|
-
--- suggestion c ---
|
|
72
|
-
Mark injection as optional using INJECTIONS.optional():
|
|
73
|
-
import { INJECTIONS } from "@jsenv/core";
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
"${key}": INJECTIONS.optional(${JSON.stringify(value)}),
|
|
77
|
-
};`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
while (index !== -1) {
|
|
84
|
-
const start = index;
|
|
85
|
-
const end = index + key.length;
|
|
86
|
-
magicSource.replace({
|
|
87
|
-
start,
|
|
88
|
-
end,
|
|
89
|
-
replacement:
|
|
90
|
-
urlInfo.type === "js_classic" ||
|
|
91
|
-
urlInfo.type === "js_module" ||
|
|
92
|
-
urlInfo.type === "html"
|
|
93
|
-
? JSON.stringify(value, null, " ")
|
|
94
|
-
: value,
|
|
95
|
-
});
|
|
96
|
-
index = content.indexOf(key, end);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return magicSource.toContentAndSourcemap();
|
|
100
|
-
};
|
|
@@ -423,6 +423,9 @@ const returnValueAssertions = [
|
|
|
423
423
|
return undefined;
|
|
424
424
|
}
|
|
425
425
|
if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
|
|
426
|
+
if (Object.hasOwn(valueReturned, "contentInjections")) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
426
429
|
throw new Error(
|
|
427
430
|
`Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
|
|
428
431
|
);
|
package/src/plugins/plugins.js
CHANGED
|
@@ -46,6 +46,7 @@ export const getCorePlugins = ({
|
|
|
46
46
|
transpilation = true,
|
|
47
47
|
inlining = true,
|
|
48
48
|
http = false,
|
|
49
|
+
spa,
|
|
49
50
|
|
|
50
51
|
clientAutoreload,
|
|
51
52
|
clientAutoreloadOnServerRestart,
|
|
@@ -75,7 +76,7 @@ export const getCorePlugins = ({
|
|
|
75
76
|
|
|
76
77
|
return [
|
|
77
78
|
jsenvPluginReferenceAnalysis(referenceAnalysis),
|
|
78
|
-
|
|
79
|
+
jsenvPluginInjections(injections),
|
|
79
80
|
jsenvPluginTranspilation(transpilation),
|
|
80
81
|
// "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
|
|
81
82
|
...(inlining ? [jsenvPluginInlining()] : []),
|
|
@@ -88,6 +89,7 @@ export const getCorePlugins = ({
|
|
|
88
89
|
*/
|
|
89
90
|
jsenvPluginProtocolHttp(http),
|
|
90
91
|
jsenvPluginProtocolFile({
|
|
92
|
+
spa,
|
|
91
93
|
magicExtensions,
|
|
92
94
|
magicDirectoryIndex,
|
|
93
95
|
directoryListing,
|
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
} from "@jsenv/urls";
|
|
38
38
|
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
39
39
|
import { getDirectoryWatchPatterns } from "../../helpers/watch_source_files.js";
|
|
40
|
-
import { replacePlaceholders } from "../injections/jsenv_plugin_injections.js";
|
|
41
40
|
import { FILE_AND_SERVER_URLS_CONVERTER } from "./file_and_server_urls_converter.js";
|
|
42
41
|
|
|
43
42
|
const htmlFileUrlForDirectory = import.meta.resolve(
|
|
@@ -124,22 +123,22 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
124
123
|
}
|
|
125
124
|
const request = urlInfo.context.request;
|
|
126
125
|
const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
const directoryListingInjections = generateDirectoryListingInjection(
|
|
127
|
+
requestedUrl,
|
|
129
128
|
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
enoent,
|
|
139
|
-
}),
|
|
129
|
+
autoreload,
|
|
130
|
+
request,
|
|
131
|
+
urlMocks,
|
|
132
|
+
directoryContentMagicName,
|
|
133
|
+
rootDirectoryUrl,
|
|
134
|
+
mainFilePath,
|
|
135
|
+
packageDirectory,
|
|
136
|
+
enoent,
|
|
140
137
|
},
|
|
141
|
-
urlInfo,
|
|
142
138
|
);
|
|
139
|
+
return {
|
|
140
|
+
contentInjections: directoryListingInjections,
|
|
141
|
+
};
|
|
143
142
|
},
|
|
144
143
|
},
|
|
145
144
|
devServerRoutes: [
|
|
@@ -158,8 +157,10 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
158
157
|
directoryRelativeUrl,
|
|
159
158
|
rootDirectoryUrl,
|
|
160
159
|
);
|
|
161
|
-
const closestDirectoryUrl =
|
|
162
|
-
|
|
160
|
+
const closestDirectoryUrl = getFirstExistingDirectoryUrl(
|
|
161
|
+
requestedUrl,
|
|
162
|
+
rootDirectoryUrl,
|
|
163
|
+
);
|
|
163
164
|
const sendMessage = (message) => {
|
|
164
165
|
websocket.send(JSON.stringify(message));
|
|
165
166
|
};
|
|
@@ -375,15 +376,15 @@ const generateDirectoryListingInjection = (
|
|
|
375
376
|
};
|
|
376
377
|
};
|
|
377
378
|
const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
|
|
378
|
-
let
|
|
379
|
-
while (!existsSync(
|
|
380
|
-
|
|
381
|
-
if (!urlIsInsideOf(
|
|
382
|
-
|
|
379
|
+
let directoryUrlCandidate = new URL("./", requestedUrl);
|
|
380
|
+
while (!existsSync(directoryUrlCandidate)) {
|
|
381
|
+
directoryUrlCandidate = new URL("../", directoryUrlCandidate);
|
|
382
|
+
if (!urlIsInsideOf(directoryUrlCandidate, serverRootDirectoryUrl)) {
|
|
383
|
+
directoryUrlCandidate = new URL(serverRootDirectoryUrl);
|
|
383
384
|
break;
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
|
-
return
|
|
387
|
+
return directoryUrlCandidate;
|
|
387
388
|
};
|
|
388
389
|
const getDirectoryContentItems = ({
|
|
389
390
|
serverRootDirectoryUrl,
|