@jsenv/core 40.6.2 → 40.7.1
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 +62 -48
- package/dist/build/build.js +412 -185
- package/dist/build/jsenv_core_packages.js +103 -105
- package/dist/client/directory_listing/js/directory_listing.js +41 -26
- package/dist/client/ribbon/ribbon.js +40 -37
- package/dist/jsenv_core.js +4 -0
- package/dist/start_build_server/jsenv_core_packages.js +29 -29
- package/dist/start_dev_server/jsenv_core_packages.js +103 -105
- package/dist/start_dev_server/start_dev_server.js +412 -182
- package/package.json +21 -12
- package/src/build/build.js +9 -9
- package/src/build/build_specifier_manager.js +3 -3
- package/src/build/build_urls_generator.js +2 -2
- package/src/dev/start_dev_server.js +11 -8
- package/src/helpers/web_url_converter.js +2 -2
- package/src/kitchen/errors.js +1 -1
- package/src/kitchen/kitchen.js +2 -0
- package/src/kitchen/out_directory_url.js +2 -2
- 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/autoreload/jsenv_plugin_autoreload_server.js +2 -2
- package/src/plugins/chrome_devtools_json/jsenv_plugin_chrome_devtools_json.js +1 -0
- 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 +28 -7
- package/src/plugins/plugins.js +3 -1
- package/src/plugins/protocol_file/client/directory_listing.jsx +42 -23
- package/src/plugins/protocol_file/file_and_server_urls_converter.js +2 -5
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +65 -49
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +36 -3
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +3 -0
- package/src/plugins/ribbon/client/ribbon.js +40 -37
- package/src/plugins/injections/internal/inject_globals.js +0 -52
|
@@ -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
|
-
};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { jsenvPluginHtmlSyntaxErrorFallback } from "./html_syntax_error_fallback/jsenv_plugin_html_syntax_error_fallback.js";
|
|
3
3
|
|
|
4
|
-
export const createPluginStore = (plugins) => {
|
|
4
|
+
export const createPluginStore = async (plugins) => {
|
|
5
5
|
const allDevServerRoutes = [];
|
|
6
|
+
const allDevServerServices = [];
|
|
6
7
|
const pluginArray = [];
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
const pluginPromises = [];
|
|
10
|
+
const addPlugin = async (plugin) => {
|
|
11
|
+
if (plugin && typeof plugin.then === "function") {
|
|
12
|
+
pluginPromises.push(plugin);
|
|
13
|
+
const value = await plugin;
|
|
14
|
+
addPlugin(value);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
8
17
|
if (Array.isArray(plugin)) {
|
|
9
18
|
for (const subplugin of plugin) {
|
|
10
19
|
addPlugin(subplugin);
|
|
@@ -23,21 +32,28 @@ export const createPluginStore = (plugins) => {
|
|
|
23
32
|
allDevServerRoutes.push(devServerRoute);
|
|
24
33
|
}
|
|
25
34
|
}
|
|
35
|
+
if (plugin.devServerServices) {
|
|
36
|
+
const devServerServices = plugin.devServerServices;
|
|
37
|
+
for (const devServerService of devServerServices) {
|
|
38
|
+
allDevServerServices.push(devServerService);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
pluginArray.push(plugin);
|
|
27
42
|
};
|
|
28
43
|
addPlugin(jsenvPluginHtmlSyntaxErrorFallback());
|
|
29
44
|
for (const plugin of plugins) {
|
|
30
45
|
addPlugin(plugin);
|
|
31
46
|
}
|
|
47
|
+
await Promise.all(pluginPromises);
|
|
32
48
|
|
|
33
49
|
return {
|
|
34
50
|
pluginArray,
|
|
35
|
-
|
|
36
51
|
allDevServerRoutes,
|
|
52
|
+
allDevServerServices,
|
|
37
53
|
};
|
|
38
54
|
};
|
|
39
55
|
|
|
40
|
-
export const createPluginController = (
|
|
56
|
+
export const createPluginController = async (
|
|
41
57
|
pluginStore,
|
|
42
58
|
kitchen,
|
|
43
59
|
{ initialPuginsMeta = {} } = {},
|
|
@@ -60,7 +76,7 @@ export const createPluginController = (
|
|
|
60
76
|
pluginCandidate.destroy?.();
|
|
61
77
|
continue;
|
|
62
78
|
}
|
|
63
|
-
const initPluginResult = initPlugin(pluginCandidate, kitchen);
|
|
79
|
+
const initPluginResult = await initPlugin(pluginCandidate, kitchen);
|
|
64
80
|
if (!initPluginResult) {
|
|
65
81
|
pluginCandidate.destroy?.();
|
|
66
82
|
continue;
|
|
@@ -112,6 +128,7 @@ export const createPluginController = (
|
|
|
112
128
|
key === "serverEvents" ||
|
|
113
129
|
key === "mustStayFirst" ||
|
|
114
130
|
key === "devServerRoutes" ||
|
|
131
|
+
key === "devServerServices" ||
|
|
115
132
|
key === "effect"
|
|
116
133
|
) {
|
|
117
134
|
continue;
|
|
@@ -285,6 +302,7 @@ export const createPluginController = (
|
|
|
285
302
|
const HOOK_NAMES = [
|
|
286
303
|
"init",
|
|
287
304
|
"devServerRoutes", // is called only during dev/tests
|
|
305
|
+
"devServerServices", // is called only during dev/tests
|
|
288
306
|
"resolveReference",
|
|
289
307
|
"redirectReference",
|
|
290
308
|
"transformReferenceSearchParams",
|
|
@@ -339,12 +357,12 @@ const testAppliesDuring = (plugin, kitchen) => {
|
|
|
339
357
|
`"appliesDuring" must be an object or a string, got ${appliesDuring}`,
|
|
340
358
|
);
|
|
341
359
|
};
|
|
342
|
-
const initPlugin = (plugin, kitchen) => {
|
|
360
|
+
const initPlugin = async (plugin, kitchen) => {
|
|
343
361
|
const { init } = plugin;
|
|
344
362
|
if (!init) {
|
|
345
363
|
return true;
|
|
346
364
|
}
|
|
347
|
-
const initReturnValue = init(kitchen.context, { plugin });
|
|
365
|
+
const initReturnValue = await init(kitchen.context, { plugin });
|
|
348
366
|
if (initReturnValue === false) {
|
|
349
367
|
return false;
|
|
350
368
|
}
|
|
@@ -423,6 +441,9 @@ const returnValueAssertions = [
|
|
|
423
441
|
return undefined;
|
|
424
442
|
}
|
|
425
443
|
if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
|
|
444
|
+
if (Object.hasOwn(valueReturned, "contentInjections")) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
426
447
|
throw new Error(
|
|
427
448
|
`Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
|
|
428
449
|
);
|
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,
|
|
@@ -6,7 +6,7 @@ const fileIconUrl = import.meta.resolve("./assets/file.png");
|
|
|
6
6
|
const homeIconUrl = import.meta.resolve("./assets/home.svg#root");
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
|
-
|
|
9
|
+
breadcrumb,
|
|
10
10
|
mainFilePath,
|
|
11
11
|
directoryContentItems,
|
|
12
12
|
enoentDetails,
|
|
@@ -49,38 +49,51 @@ const DirectoryListing = () => {
|
|
|
49
49
|
return (
|
|
50
50
|
<>
|
|
51
51
|
{enoentDetails ? <ErrorMessage /> : null}
|
|
52
|
-
<
|
|
52
|
+
<Breadcrumb items={breadcrumb} />
|
|
53
53
|
<DirectoryContent items={directoryItems} />
|
|
54
54
|
</>
|
|
55
55
|
);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
const ErrorMessage = () => {
|
|
59
|
-
const {
|
|
59
|
+
const { filePathExisting, filePathNotFound } = enoentDetails;
|
|
60
|
+
|
|
61
|
+
let errorText;
|
|
62
|
+
let errorSuggestion;
|
|
63
|
+
errorText = (
|
|
64
|
+
<>
|
|
65
|
+
<strong>File not found:</strong>
|
|
66
|
+
<code>
|
|
67
|
+
<span className="file_path_good">{filePathExisting}</span>
|
|
68
|
+
<span className="file_path_bad">{filePathNotFound}</span>
|
|
69
|
+
</code>{" "}
|
|
70
|
+
does not exist on the server.
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
errorSuggestion = (
|
|
74
|
+
<>
|
|
75
|
+
<span className="icon">🔍</span> Check available routes in{" "}
|
|
76
|
+
<a href="/.internal/route_inspector">route inspector</a>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
60
79
|
|
|
61
80
|
return (
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<br />
|
|
72
|
-
<span className="error_text" style="font-size: 70%;">
|
|
73
|
-
See also available routes in the{" "}
|
|
74
|
-
<a href="/.internal/route_inspector">route inspector</a>.
|
|
75
|
-
</span>
|
|
76
|
-
</p>
|
|
81
|
+
<div className="error_message">
|
|
82
|
+
<p className="error_text">{errorText}</p>
|
|
83
|
+
<p
|
|
84
|
+
className="error_suggestion"
|
|
85
|
+
style="font-size: 0.8em; margin-top: 10px;"
|
|
86
|
+
>
|
|
87
|
+
{errorSuggestion}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
77
90
|
);
|
|
78
91
|
};
|
|
79
92
|
|
|
80
|
-
const
|
|
93
|
+
const Breadcrumb = ({ items }) => {
|
|
81
94
|
return (
|
|
82
95
|
<h1 className="nav">
|
|
83
|
-
{
|
|
96
|
+
{items.map((navItem) => {
|
|
84
97
|
const {
|
|
85
98
|
url,
|
|
86
99
|
urlRelativeToServer,
|
|
@@ -91,7 +104,7 @@ const Nav = () => {
|
|
|
91
104
|
const isDirectory = new URL(url).pathname.endsWith("/");
|
|
92
105
|
return (
|
|
93
106
|
<>
|
|
94
|
-
<
|
|
107
|
+
<BreadcrumbItem
|
|
95
108
|
key={url}
|
|
96
109
|
url={urlRelativeToServer}
|
|
97
110
|
isCurrent={isCurrent}
|
|
@@ -99,7 +112,7 @@ const Nav = () => {
|
|
|
99
112
|
iconLinkUrl={isServerRootDirectory ? `/${mainFilePath}` : ""}
|
|
100
113
|
>
|
|
101
114
|
{name}
|
|
102
|
-
</
|
|
115
|
+
</BreadcrumbItem>
|
|
103
116
|
{isDirectory ? (
|
|
104
117
|
<span className="directory_separator">/</span>
|
|
105
118
|
) : null}
|
|
@@ -109,7 +122,13 @@ const Nav = () => {
|
|
|
109
122
|
</h1>
|
|
110
123
|
);
|
|
111
124
|
};
|
|
112
|
-
const
|
|
125
|
+
const BreadcrumbItem = ({
|
|
126
|
+
url,
|
|
127
|
+
iconImageUrl,
|
|
128
|
+
iconLinkUrl,
|
|
129
|
+
isCurrent,
|
|
130
|
+
children,
|
|
131
|
+
}) => {
|
|
113
132
|
return (
|
|
114
133
|
<span className="nav_item" data-current={isCurrent ? "" : undefined}>
|
|
115
134
|
{iconLinkUrl ? (
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { urlIsOrIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
2
2
|
|
|
3
3
|
export const FILE_AND_SERVER_URLS_CONVERTER = {
|
|
4
4
|
asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
|
|
5
|
-
if (fileUrl
|
|
6
|
-
return "/";
|
|
7
|
-
}
|
|
8
|
-
if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
|
|
5
|
+
if (urlIsOrIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
|
|
9
6
|
const urlRelativeToServer = urlToRelativeUrl(
|
|
10
7
|
fileUrl,
|
|
11
8
|
serverRootDirectoryUrl,
|
|
@@ -31,13 +31,12 @@ import { pickContentType, WebSocketResponse } from "@jsenv/server";
|
|
|
31
31
|
import {
|
|
32
32
|
asUrlWithoutSearch,
|
|
33
33
|
ensurePathnameTrailingSlash,
|
|
34
|
-
|
|
34
|
+
urlIsOrIsInsideOf,
|
|
35
35
|
urlToFilename,
|
|
36
36
|
urlToRelativeUrl,
|
|
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(
|
|
@@ -45,6 +44,7 @@ const htmlFileUrlForDirectory = import.meta.resolve(
|
|
|
45
44
|
);
|
|
46
45
|
|
|
47
46
|
export const jsenvPluginDirectoryListing = ({
|
|
47
|
+
spa,
|
|
48
48
|
urlMocks = false,
|
|
49
49
|
autoreload = true,
|
|
50
50
|
directoryContentMagicName,
|
|
@@ -86,7 +86,7 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(
|
|
89
|
+
return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(requestedUrl)}&enoent`;
|
|
90
90
|
}
|
|
91
91
|
const isDirectory = fsStat?.isDirectory();
|
|
92
92
|
if (!isDirectory) {
|
|
@@ -112,34 +112,35 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
112
112
|
if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
115
|
+
const urlNotFound = urlInfo.searchParams.get("url");
|
|
116
|
+
if (!urlNotFound) {
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
|
+
|
|
119
120
|
urlInfo.headers["cache-control"] = "no-cache";
|
|
120
121
|
const enoent = urlInfo.searchParams.has("enoent");
|
|
121
122
|
if (enoent) {
|
|
122
123
|
urlInfo.status = 404;
|
|
123
|
-
urlInfo.headers["cache-control"] = "no-cache";
|
|
124
124
|
}
|
|
125
125
|
const request = urlInfo.context.request;
|
|
126
126
|
const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const directoryListingInjections = generateDirectoryListingInjection(
|
|
128
|
+
urlNotFound,
|
|
129
129
|
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}),
|
|
130
|
+
spa,
|
|
131
|
+
autoreload,
|
|
132
|
+
request,
|
|
133
|
+
urlMocks,
|
|
134
|
+
directoryContentMagicName,
|
|
135
|
+
rootDirectoryUrl,
|
|
136
|
+
mainFilePath,
|
|
137
|
+
packageDirectory,
|
|
138
|
+
enoent,
|
|
140
139
|
},
|
|
141
|
-
urlInfo,
|
|
142
140
|
);
|
|
141
|
+
return {
|
|
142
|
+
contentInjections: directoryListingInjections,
|
|
143
|
+
};
|
|
143
144
|
},
|
|
144
145
|
},
|
|
145
146
|
devServerRoutes: [
|
|
@@ -158,8 +159,10 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
158
159
|
directoryRelativeUrl,
|
|
159
160
|
rootDirectoryUrl,
|
|
160
161
|
);
|
|
161
|
-
const closestDirectoryUrl =
|
|
162
|
-
|
|
162
|
+
const closestDirectoryUrl = getFirstExistingDirectoryUrl(
|
|
163
|
+
requestedUrl,
|
|
164
|
+
rootDirectoryUrl,
|
|
165
|
+
);
|
|
163
166
|
const sendMessage = (message) => {
|
|
164
167
|
websocket.send(JSON.stringify(message));
|
|
165
168
|
};
|
|
@@ -218,8 +221,9 @@ export const jsenvPluginDirectoryListing = ({
|
|
|
218
221
|
};
|
|
219
222
|
|
|
220
223
|
const generateDirectoryListingInjection = (
|
|
221
|
-
|
|
224
|
+
urlNotFound,
|
|
222
225
|
{
|
|
226
|
+
spa,
|
|
223
227
|
rootDirectoryUrl,
|
|
224
228
|
mainFilePath,
|
|
225
229
|
packageDirectory,
|
|
@@ -232,13 +236,13 @@ const generateDirectoryListingInjection = (
|
|
|
232
236
|
) => {
|
|
233
237
|
let serverRootDirectoryUrl = rootDirectoryUrl;
|
|
234
238
|
const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
|
|
235
|
-
|
|
239
|
+
urlNotFound,
|
|
236
240
|
serverRootDirectoryUrl,
|
|
237
241
|
);
|
|
238
242
|
const directoryContentItems = getDirectoryContentItems({
|
|
239
243
|
serverRootDirectoryUrl,
|
|
240
244
|
mainFilePath,
|
|
241
|
-
requestedUrl,
|
|
245
|
+
requestedUrl: urlNotFound,
|
|
242
246
|
firstExistingDirectoryUrl,
|
|
243
247
|
});
|
|
244
248
|
package_workspaces: {
|
|
@@ -284,8 +288,8 @@ const generateDirectoryListingInjection = (
|
|
|
284
288
|
const { host } = new URL(request.url);
|
|
285
289
|
const websocketUrl = `${websocketScheme}://${host}/.internal/directory_content.websocket?directory=${encodeURIComponent(directoryUrlRelativeToServer)}`;
|
|
286
290
|
|
|
287
|
-
const
|
|
288
|
-
|
|
291
|
+
const generateBreadcrumb = () => {
|
|
292
|
+
const breadcrumb = [];
|
|
289
293
|
const lastItemUrl = firstExistingDirectoryUrl;
|
|
290
294
|
const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
|
|
291
295
|
const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
|
|
@@ -295,7 +299,6 @@ const generateDirectoryListingInjection = (
|
|
|
295
299
|
} else {
|
|
296
300
|
parts = [rootDirectoryUrlName];
|
|
297
301
|
}
|
|
298
|
-
|
|
299
302
|
let i = 0;
|
|
300
303
|
while (i < parts.length) {
|
|
301
304
|
const part = parts[i];
|
|
@@ -316,7 +319,7 @@ const generateDirectoryListingInjection = (
|
|
|
316
319
|
navItemUrl,
|
|
317
320
|
serverRootDirectoryUrl,
|
|
318
321
|
);
|
|
319
|
-
let urlRelativeToDocument = urlToRelativeUrl(navItemUrl,
|
|
322
|
+
let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, urlNotFound);
|
|
320
323
|
const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
|
|
321
324
|
if (isServerRootDirectory) {
|
|
322
325
|
urlRelativeToServer = `/${directoryContentMagicName}`;
|
|
@@ -324,7 +327,7 @@ const generateDirectoryListingInjection = (
|
|
|
324
327
|
}
|
|
325
328
|
const name = part;
|
|
326
329
|
const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
|
|
327
|
-
|
|
330
|
+
breadcrumb.push({
|
|
328
331
|
url: navItemUrl,
|
|
329
332
|
urlRelativeToServer,
|
|
330
333
|
urlRelativeToDocument,
|
|
@@ -334,34 +337,47 @@ const generateDirectoryListingInjection = (
|
|
|
334
337
|
});
|
|
335
338
|
i++;
|
|
336
339
|
}
|
|
337
|
-
|
|
340
|
+
return breadcrumb;
|
|
341
|
+
};
|
|
342
|
+
const breadcrumb = generateBreadcrumb(urlNotFound);
|
|
338
343
|
|
|
339
344
|
let enoentDetails = null;
|
|
340
345
|
if (enoent) {
|
|
346
|
+
const buildEnoentPathInfo = (urlBase, closestExistingUrl) => {
|
|
347
|
+
let filePathExisting;
|
|
348
|
+
let filePathNotFound;
|
|
349
|
+
const existingIndex = String(closestExistingUrl).length;
|
|
350
|
+
filePathExisting = urlToRelativeUrl(
|
|
351
|
+
closestExistingUrl,
|
|
352
|
+
serverRootDirectoryUrl,
|
|
353
|
+
);
|
|
354
|
+
filePathNotFound = urlBase.slice(existingIndex);
|
|
355
|
+
return [filePathExisting, filePathNotFound];
|
|
356
|
+
};
|
|
341
357
|
const fileRelativeUrl = urlToRelativeUrl(
|
|
342
|
-
|
|
358
|
+
urlNotFound,
|
|
343
359
|
serverRootDirectoryUrl,
|
|
344
360
|
);
|
|
345
|
-
let filePathExisting;
|
|
346
|
-
let filePathNotFound;
|
|
347
|
-
const existingIndex = String(firstExistingDirectoryUrl).length;
|
|
348
|
-
filePathExisting = urlToRelativeUrl(
|
|
349
|
-
firstExistingDirectoryUrl,
|
|
350
|
-
serverRootDirectoryUrl,
|
|
351
|
-
);
|
|
352
|
-
filePathNotFound = requestedUrl.slice(existingIndex);
|
|
353
361
|
enoentDetails = {
|
|
354
|
-
fileUrl:
|
|
362
|
+
fileUrl: urlNotFound,
|
|
355
363
|
fileRelativeUrl,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const [filePathExisting, filePathNotFound] = buildEnoentPathInfo(
|
|
367
|
+
urlNotFound,
|
|
368
|
+
firstExistingDirectoryUrl,
|
|
369
|
+
);
|
|
370
|
+
Object.assign(enoentDetails, {
|
|
356
371
|
filePathExisting: `/${filePathExisting}`,
|
|
357
372
|
filePathNotFound,
|
|
358
|
-
};
|
|
373
|
+
});
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
return {
|
|
362
377
|
__DIRECTORY_LISTING__: {
|
|
378
|
+
spa,
|
|
363
379
|
enoentDetails,
|
|
364
|
-
|
|
380
|
+
breadcrumb,
|
|
365
381
|
urlMocks,
|
|
366
382
|
directoryContentMagicName,
|
|
367
383
|
directoryUrl: firstExistingDirectoryUrl,
|
|
@@ -374,16 +390,16 @@ const generateDirectoryListingInjection = (
|
|
|
374
390
|
},
|
|
375
391
|
};
|
|
376
392
|
};
|
|
377
|
-
const getFirstExistingDirectoryUrl = (
|
|
378
|
-
let
|
|
379
|
-
while (!existsSync(
|
|
380
|
-
|
|
381
|
-
if (!
|
|
382
|
-
|
|
393
|
+
const getFirstExistingDirectoryUrl = (urlBase, serverRootDirectoryUrl) => {
|
|
394
|
+
let directoryUrlCandidate = new URL("./", urlBase);
|
|
395
|
+
while (!existsSync(directoryUrlCandidate)) {
|
|
396
|
+
directoryUrlCandidate = new URL("../", directoryUrlCandidate);
|
|
397
|
+
if (!urlIsOrIsInsideOf(directoryUrlCandidate, serverRootDirectoryUrl)) {
|
|
398
|
+
directoryUrlCandidate = new URL(serverRootDirectoryUrl);
|
|
383
399
|
break;
|
|
384
400
|
}
|
|
385
401
|
}
|
|
386
|
-
return
|
|
402
|
+
return directoryUrlCandidate;
|
|
387
403
|
};
|
|
388
404
|
const getDirectoryContentItems = ({
|
|
389
405
|
serverRootDirectoryUrl,
|