@jsenv/core 39.12.0 → 39.13.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/css/directory_listing.css +211 -0
- package/dist/html/directory_listing.html +18 -0
- package/dist/js/directory_listing.js +240 -0
- package/dist/jsenv_core.js +1057 -793
- package/dist/other/dir.png +0 -0
- package/dist/other/file.png +0 -0
- package/dist/other/home.svg +6 -0
- package/package.json +6 -6
- package/src/build/build.js +7 -7
- package/src/build/build_specifier_manager.js +0 -1
- package/src/dev/start_dev_server.js +39 -49
- package/src/kitchen/kitchen.js +20 -4
- package/src/kitchen/out_directory_url.js +2 -1
- package/src/kitchen/url_graph/references.js +3 -1
- package/src/kitchen/url_graph/url_graph.js +1 -0
- package/src/kitchen/url_graph/url_info_transformations.js +37 -4
- package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
- package/src/plugins/plugin_controller.js +170 -114
- package/src/plugins/plugins.js +5 -4
- package/src/plugins/protocol_file/client/assets/home.svg +5 -5
- package/src/plugins/protocol_file/client/directory_listing.css +190 -0
- package/src/plugins/protocol_file/client/directory_listing.html +18 -0
- package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
- package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +40 -369
- package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
- package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
- package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
- package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
- package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
- package/dist/html/directory.html +0 -184
- package/dist/html/html_404_and_ancestor_dir.html +0 -222
- package/src/plugins/protocol_file/client/assets/directory.css +0 -150
- package/src/plugins/protocol_file/client/directory.html +0 -17
- package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
- package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +0 -37
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { render } from "preact";
|
|
2
|
+
import { useSyncExternalStore } from "preact/compat";
|
|
3
|
+
|
|
4
|
+
const directoryIconUrl = import.meta.resolve("./assets/dir.png");
|
|
5
|
+
const fileIconUrl = import.meta.resolve("./assets/file.png");
|
|
6
|
+
const homeIconUrl = import.meta.resolve("./assets/home.svg#root");
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
navItems,
|
|
10
|
+
mainFilePath,
|
|
11
|
+
directoryContentItems,
|
|
12
|
+
enoentDetails,
|
|
13
|
+
websocketUrl,
|
|
14
|
+
autoreload,
|
|
15
|
+
} = window.DIRECTORY_LISTING;
|
|
16
|
+
|
|
17
|
+
const directoryItemsChangeCallbackSet = new Set();
|
|
18
|
+
const updateDirectoryContentItems = (value) => {
|
|
19
|
+
directoryContentItems = value;
|
|
20
|
+
for (const dirContentItem of value) {
|
|
21
|
+
if (dirContentItem.isMainFile && window.location.pathname === "/") {
|
|
22
|
+
window.location.reload();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const isDirectory = new URL(dirContentItem.url).pathname.endsWith("/");
|
|
26
|
+
if (
|
|
27
|
+
!isDirectory &&
|
|
28
|
+
dirContentItem.urlRelativeToServer === window.location.pathname
|
|
29
|
+
) {
|
|
30
|
+
window.location.reload();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const directoryItemsChangeCallback of directoryItemsChangeCallbackSet) {
|
|
35
|
+
directoryItemsChangeCallback();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DirectoryListing = () => {
|
|
40
|
+
const directoryItems = useSyncExternalStore(
|
|
41
|
+
(callback) => {
|
|
42
|
+
directoryItemsChangeCallbackSet.add(callback);
|
|
43
|
+
},
|
|
44
|
+
() => {
|
|
45
|
+
return directoryContentItems;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{enoentDetails ? <ErrorMessage /> : null}
|
|
52
|
+
<Nav />
|
|
53
|
+
<DirectoryContent items={directoryItems} />
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ErrorMessage = () => {
|
|
59
|
+
const { fileUrl, filePathExisting, filePathNotFound } = enoentDetails;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<p className="error_message">
|
|
63
|
+
<span className="error_text">
|
|
64
|
+
No filesystem entry at{" "}
|
|
65
|
+
<code title={fileUrl}>
|
|
66
|
+
<span className="file_path_good">{filePathExisting}</span>
|
|
67
|
+
<span className="file_path_bad">{filePathNotFound}</span>
|
|
68
|
+
</code>
|
|
69
|
+
.
|
|
70
|
+
</span>
|
|
71
|
+
</p>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const Nav = () => {
|
|
76
|
+
return (
|
|
77
|
+
<h1 className="nav">
|
|
78
|
+
{navItems.map((navItem) => {
|
|
79
|
+
const {
|
|
80
|
+
url,
|
|
81
|
+
urlRelativeToServer,
|
|
82
|
+
name,
|
|
83
|
+
isCurrent,
|
|
84
|
+
isServerRootDirectory,
|
|
85
|
+
} = navItem;
|
|
86
|
+
const isDirectory = new URL(url).pathname.endsWith("/");
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<NavItem
|
|
90
|
+
key={url}
|
|
91
|
+
url={urlRelativeToServer}
|
|
92
|
+
isCurrent={isCurrent}
|
|
93
|
+
iconImageUrl={isServerRootDirectory ? homeIconUrl : ""}
|
|
94
|
+
iconLinkUrl={isServerRootDirectory ? `/${mainFilePath}` : ""}
|
|
95
|
+
>
|
|
96
|
+
{name}
|
|
97
|
+
</NavItem>
|
|
98
|
+
{isDirectory ? (
|
|
99
|
+
<span className="directory_separator">/</span>
|
|
100
|
+
) : null}
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</h1>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
const NavItem = ({ url, iconImageUrl, iconLinkUrl, isCurrent, children }) => {
|
|
108
|
+
return (
|
|
109
|
+
<span className="nav_item" data-current={isCurrent ? "" : undefined}>
|
|
110
|
+
{iconLinkUrl ? (
|
|
111
|
+
<a
|
|
112
|
+
className="nav_item_icon"
|
|
113
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
114
|
+
hot-decline
|
|
115
|
+
href={iconLinkUrl}
|
|
116
|
+
>
|
|
117
|
+
<Icon url={iconImageUrl} />
|
|
118
|
+
</a>
|
|
119
|
+
) : iconImageUrl ? (
|
|
120
|
+
<span className="nav_item_icon">
|
|
121
|
+
<Icon url={iconImageUrl} />
|
|
122
|
+
</span>
|
|
123
|
+
) : null}
|
|
124
|
+
{url ? (
|
|
125
|
+
<a className="nav_item_text" href={url}>
|
|
126
|
+
{children}
|
|
127
|
+
</a>
|
|
128
|
+
) : (
|
|
129
|
+
<span className="nav_item_text">{children}</span>
|
|
130
|
+
)}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const DirectoryContent = ({ items }) => {
|
|
136
|
+
if (items.length === 0) {
|
|
137
|
+
return <p className="directory_empty_message">Directory is empty</p>;
|
|
138
|
+
}
|
|
139
|
+
return (
|
|
140
|
+
<ul className="directory_content">
|
|
141
|
+
{items.map((directoryItem) => {
|
|
142
|
+
return (
|
|
143
|
+
<DirectoryContentItem
|
|
144
|
+
key={directoryItem.url}
|
|
145
|
+
url={directoryItem.urlRelativeToServer}
|
|
146
|
+
isDirectory={directoryItem.url.endsWith("/")}
|
|
147
|
+
isMainFile={directoryItem.isMainFile}
|
|
148
|
+
>
|
|
149
|
+
{directoryItem.urlRelativeToCurrentDirectory}
|
|
150
|
+
</DirectoryContentItem>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</ul>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
const DirectoryContentItem = ({ url, isDirectory, isMainFile, children }) => {
|
|
157
|
+
return (
|
|
158
|
+
<li
|
|
159
|
+
className="directory_content_item"
|
|
160
|
+
data-directory={isDirectory ? "" : undefined}
|
|
161
|
+
data-file={isDirectory ? undefined : ""}
|
|
162
|
+
>
|
|
163
|
+
<a
|
|
164
|
+
className="directory_content_item_link"
|
|
165
|
+
href={url}
|
|
166
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
167
|
+
hot-decline={isMainFile ? true : undefined}
|
|
168
|
+
>
|
|
169
|
+
<span className="directory_content_item_icon">
|
|
170
|
+
<Icon
|
|
171
|
+
url={
|
|
172
|
+
isMainFile
|
|
173
|
+
? homeIconUrl
|
|
174
|
+
: isDirectory
|
|
175
|
+
? directoryIconUrl
|
|
176
|
+
: fileIconUrl
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
</span>
|
|
180
|
+
{children}
|
|
181
|
+
{isDirectory ? (
|
|
182
|
+
<>
|
|
183
|
+
<span style="flex:1"></span>
|
|
184
|
+
<span className="directory_content_item_arrow">
|
|
185
|
+
<RightArrowSvg />
|
|
186
|
+
</span>
|
|
187
|
+
</>
|
|
188
|
+
) : null}
|
|
189
|
+
</a>
|
|
190
|
+
</li>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
const RightArrowSvg = () => {
|
|
194
|
+
return (
|
|
195
|
+
<svg fill="currentColor" viewBox="0 0 330 330">
|
|
196
|
+
<path
|
|
197
|
+
stroke="currentColor"
|
|
198
|
+
d="M250.606,154.389l-150-149.996c-5.857-5.858-15.355-5.858-21.213,0.001
|
|
199
|
+
c-5.857,5.858-5.857,15.355,0.001,21.213l139.393,139.39L79.393,304.394c-5.857,5.858-5.857,15.355,0.001,21.213
|
|
200
|
+
C82.322,328.536,86.161,330,90,330s7.678-1.464,10.607-4.394l149.999-150.004c2.814-2.813,4.394-6.628,4.394-10.606
|
|
201
|
+
C255,161.018,253.42,157.202,250.606,154.389z"
|
|
202
|
+
/>
|
|
203
|
+
</svg>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const Icon = ({ url }) => {
|
|
208
|
+
if (urlToFilename(url).endsWith(".svg")) {
|
|
209
|
+
return (
|
|
210
|
+
<svg>
|
|
211
|
+
<use href={url} />
|
|
212
|
+
</svg>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return <img src={url} />;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const urlToFilename = (url) => {
|
|
219
|
+
const pathname = new URL(url).pathname;
|
|
220
|
+
const pathnameBeforeLastSlash = pathname.endsWith("/")
|
|
221
|
+
? pathname.slice(0, -1)
|
|
222
|
+
: pathname;
|
|
223
|
+
const slashLastIndex = pathnameBeforeLastSlash.lastIndexOf("/");
|
|
224
|
+
const filename =
|
|
225
|
+
slashLastIndex === -1
|
|
226
|
+
? pathnameBeforeLastSlash
|
|
227
|
+
: pathnameBeforeLastSlash.slice(slashLastIndex + 1);
|
|
228
|
+
return filename;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (autoreload) {
|
|
232
|
+
const socket = new WebSocket(websocketUrl, ["watch-directory"]);
|
|
233
|
+
socket.onopen = () => {
|
|
234
|
+
socket.onopen = null;
|
|
235
|
+
setInterval(() => {
|
|
236
|
+
socket.send('{"type":"ping"}');
|
|
237
|
+
}, 30_000);
|
|
238
|
+
};
|
|
239
|
+
socket.onmessage = (messageEvent) => {
|
|
240
|
+
const event = JSON.parse(messageEvent.data);
|
|
241
|
+
const { type, reason, items } = event;
|
|
242
|
+
if (type === "change") {
|
|
243
|
+
console.log(`update list (reason: ${reason})`);
|
|
244
|
+
// TODO: if index.html is added AND we are at "/" we must reload
|
|
245
|
+
updateDirectoryContentItems(items);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
render(<DirectoryListing />, document.querySelector("#root"));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
2
|
+
|
|
3
|
+
export const FILE_AND_SERVER_URLS_CONVERTER = {
|
|
4
|
+
asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
|
|
5
|
+
if (fileUrl === serverRootDirectoryUrl) {
|
|
6
|
+
return "/";
|
|
7
|
+
}
|
|
8
|
+
if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
|
|
9
|
+
const urlRelativeToServer = urlToRelativeUrl(
|
|
10
|
+
fileUrl,
|
|
11
|
+
serverRootDirectoryUrl,
|
|
12
|
+
);
|
|
13
|
+
return `/${urlRelativeToServer}`;
|
|
14
|
+
}
|
|
15
|
+
const urlRelativeToFilesystemRoot = String(fileUrl).slice(
|
|
16
|
+
"file:///".length,
|
|
17
|
+
);
|
|
18
|
+
return `/@fs/${urlRelativeToFilesystemRoot}`;
|
|
19
|
+
},
|
|
20
|
+
asFileUrl: (urlRelativeToServer, serverRootDirectoryUrl) => {
|
|
21
|
+
if (urlRelativeToServer.startsWith("/@fs/")) {
|
|
22
|
+
const urlRelativeToFilesystemRoot = urlRelativeToServer.slice(
|
|
23
|
+
"/@fs/".length,
|
|
24
|
+
);
|
|
25
|
+
return `file:///${urlRelativeToFilesystemRoot}`;
|
|
26
|
+
}
|
|
27
|
+
if (urlRelativeToServer[0] === "/") {
|
|
28
|
+
return new URL(urlRelativeToServer.slice(1), serverRootDirectoryUrl).href;
|
|
29
|
+
}
|
|
30
|
+
return new URL(urlRelativeToServer, serverRootDirectoryUrl).href;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* NICE TO HAVE:
|
|
3
|
+
*
|
|
4
|
+
* - when visiting urls outside server root directory the UI is messed up
|
|
5
|
+
*
|
|
6
|
+
* Let's say I visit file outside the server root directory that is in 404
|
|
7
|
+
* We must update the enoent message and maybe other things to take into account
|
|
8
|
+
* that url is no longer /something but "@fs/project_root/something" in the browser url bar
|
|
9
|
+
*
|
|
10
|
+
* - watching directory might result into things that are not properly handled:
|
|
11
|
+
* 1. the existing directory is deleted
|
|
12
|
+
* -> we should update the whole page to use a new "firstExistingDirectoryUrl"
|
|
13
|
+
* 2. the enoent is impacted
|
|
14
|
+
* -> we should update the ENOENT message
|
|
15
|
+
* It means the websocket should contain more data and we can't assume firstExistingDirectoryUrl won't change
|
|
16
|
+
*
|
|
17
|
+
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
comparePathnames,
|
|
22
|
+
readEntryStatSync,
|
|
23
|
+
registerDirectoryLifecycle,
|
|
24
|
+
} from "@jsenv/filesystem";
|
|
25
|
+
import { pickContentType } from "@jsenv/server";
|
|
26
|
+
import {
|
|
27
|
+
asUrlWithoutSearch,
|
|
28
|
+
ensurePathnameTrailingSlash,
|
|
29
|
+
urlIsInsideOf,
|
|
30
|
+
urlToFilename,
|
|
31
|
+
urlToRelativeUrl,
|
|
32
|
+
} from "@jsenv/urls";
|
|
33
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
34
|
+
import { lookupPackageDirectory } from "../../helpers/lookup_package_directory.js";
|
|
35
|
+
import { replacePlaceholders } from "../injections/jsenv_plugin_injections.js";
|
|
36
|
+
import { FILE_AND_SERVER_URLS_CONVERTER } from "./file_and_server_urls_converter.js";
|
|
37
|
+
|
|
38
|
+
const htmlFileUrlForDirectory = new URL(
|
|
39
|
+
"./client/directory_listing.html",
|
|
40
|
+
import.meta.url,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const jsenvPluginDirectoryListing = ({
|
|
44
|
+
directoryContentMagicName,
|
|
45
|
+
directoryListingUrlMocks,
|
|
46
|
+
autoreload = true,
|
|
47
|
+
}) => {
|
|
48
|
+
return {
|
|
49
|
+
name: "jsenv:directory_listing",
|
|
50
|
+
appliesDuring: "dev",
|
|
51
|
+
redirectReference: (reference) => {
|
|
52
|
+
if (reference.isInline) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const url = reference.url;
|
|
56
|
+
if (!url.startsWith("file:")) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
let { fsStat } = reference;
|
|
60
|
+
if (!fsStat) {
|
|
61
|
+
fsStat = readEntryStatSync(url, { nullIfNotFound: true });
|
|
62
|
+
reference.fsStat = fsStat;
|
|
63
|
+
}
|
|
64
|
+
const { request } = reference.ownerUrlInfo.context;
|
|
65
|
+
if (!fsStat) {
|
|
66
|
+
if (
|
|
67
|
+
reference.isDirectRequest &&
|
|
68
|
+
request &&
|
|
69
|
+
request.headers["sec-fetch-dest"] === "document"
|
|
70
|
+
) {
|
|
71
|
+
return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}&enoent`;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const isDirectory = fsStat?.isDirectory();
|
|
76
|
+
if (!isDirectory) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (reference.type === "filesystem") {
|
|
80
|
+
// TODO: we should redirect to something like /...json
|
|
81
|
+
// and any file name ...json is a special file serving directory content as json
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const acceptsHtml = request
|
|
85
|
+
? pickContentType(request, ["text/html"])
|
|
86
|
+
: false;
|
|
87
|
+
if (!acceptsHtml) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
reference.fsStat = null; // reset fsStat, now it's not a directory anyor
|
|
91
|
+
return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}`;
|
|
92
|
+
},
|
|
93
|
+
transformUrlContent: {
|
|
94
|
+
html: (urlInfo) => {
|
|
95
|
+
const urlWithoutSearch = asUrlWithoutSearch(urlInfo.url);
|
|
96
|
+
if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const requestedUrl = urlInfo.searchParams.get("url");
|
|
100
|
+
if (!requestedUrl) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
urlInfo.headers["cache-control"] = "no-cache";
|
|
104
|
+
const enoent = urlInfo.searchParams.has("enoent");
|
|
105
|
+
if (enoent) {
|
|
106
|
+
urlInfo.status = 404;
|
|
107
|
+
urlInfo.headers["cache-control"] = "no-cache";
|
|
108
|
+
}
|
|
109
|
+
const request = urlInfo.context.request;
|
|
110
|
+
const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
|
|
111
|
+
return replacePlaceholders(
|
|
112
|
+
urlInfo.content,
|
|
113
|
+
{
|
|
114
|
+
...generateDirectoryListingInjection(requestedUrl, {
|
|
115
|
+
autoreload,
|
|
116
|
+
request,
|
|
117
|
+
directoryListingUrlMocks,
|
|
118
|
+
directoryContentMagicName,
|
|
119
|
+
rootDirectoryUrl,
|
|
120
|
+
mainFilePath,
|
|
121
|
+
enoent,
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
urlInfo,
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
serveWebsocket: ({ websocket, request, context }) => {
|
|
129
|
+
if (!autoreload) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const secProtocol = request.headers["sec-websocket-protocol"];
|
|
133
|
+
if (secProtocol !== "watch-directory") {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const { rootDirectoryUrl, mainFilePath } = context;
|
|
137
|
+
const requestedUrl = FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(
|
|
138
|
+
request.pathname,
|
|
139
|
+
rootDirectoryUrl,
|
|
140
|
+
);
|
|
141
|
+
const closestDirectoryUrl = getFirstExistingDirectoryUrl(requestedUrl);
|
|
142
|
+
const sendMessage = (message) => {
|
|
143
|
+
websocket.send(JSON.stringify(message));
|
|
144
|
+
};
|
|
145
|
+
const generateItems = () => {
|
|
146
|
+
const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
|
|
147
|
+
requestedUrl,
|
|
148
|
+
rootDirectoryUrl,
|
|
149
|
+
);
|
|
150
|
+
const items = getDirectoryContentItems({
|
|
151
|
+
serverRootDirectoryUrl: rootDirectoryUrl,
|
|
152
|
+
mainFilePath,
|
|
153
|
+
requestedUrl,
|
|
154
|
+
firstExistingDirectoryUrl,
|
|
155
|
+
});
|
|
156
|
+
return items;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const unwatch = registerDirectoryLifecycle(closestDirectoryUrl, {
|
|
160
|
+
added: ({ relativeUrl }) => {
|
|
161
|
+
sendMessage({
|
|
162
|
+
type: "change",
|
|
163
|
+
reason: `${relativeUrl} added`,
|
|
164
|
+
items: generateItems(),
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
updated: ({ relativeUrl }) => {
|
|
168
|
+
sendMessage({
|
|
169
|
+
type: "change",
|
|
170
|
+
reason: `${relativeUrl} updated`,
|
|
171
|
+
items: generateItems(),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
removed: ({ relativeUrl }) => {
|
|
175
|
+
sendMessage({
|
|
176
|
+
type: "change",
|
|
177
|
+
reason: `${relativeUrl} removed`,
|
|
178
|
+
items: generateItems(),
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
websocket.signal.addEventListener("abort", () => {
|
|
183
|
+
unwatch();
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const generateDirectoryListingInjection = (
|
|
191
|
+
requestedUrl,
|
|
192
|
+
{
|
|
193
|
+
rootDirectoryUrl,
|
|
194
|
+
mainFilePath,
|
|
195
|
+
request,
|
|
196
|
+
directoryListingUrlMocks,
|
|
197
|
+
directoryContentMagicName,
|
|
198
|
+
autoreload,
|
|
199
|
+
enoent,
|
|
200
|
+
},
|
|
201
|
+
) => {
|
|
202
|
+
let serverRootDirectoryUrl = rootDirectoryUrl;
|
|
203
|
+
const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
|
|
204
|
+
requestedUrl,
|
|
205
|
+
serverRootDirectoryUrl,
|
|
206
|
+
);
|
|
207
|
+
const directoryContentItems = getDirectoryContentItems({
|
|
208
|
+
serverRootDirectoryUrl,
|
|
209
|
+
mainFilePath,
|
|
210
|
+
requestedUrl,
|
|
211
|
+
firstExistingDirectoryUrl,
|
|
212
|
+
});
|
|
213
|
+
package_workspaces: {
|
|
214
|
+
const packageDirectoryUrl = lookupPackageDirectory(serverRootDirectoryUrl);
|
|
215
|
+
if (!packageDirectoryUrl) {
|
|
216
|
+
break package_workspaces;
|
|
217
|
+
}
|
|
218
|
+
if (String(packageDirectoryUrl) === String(serverRootDirectoryUrl)) {
|
|
219
|
+
break package_workspaces;
|
|
220
|
+
}
|
|
221
|
+
rootDirectoryUrl = packageDirectoryUrl;
|
|
222
|
+
// if (String(firstExistingDirectoryUrl) === String(serverRootDirectoryUrl)) {
|
|
223
|
+
// let packageContent;
|
|
224
|
+
// try {
|
|
225
|
+
// packageContent = JSON.parse(
|
|
226
|
+
// readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
|
|
227
|
+
// );
|
|
228
|
+
// } catch {
|
|
229
|
+
// break package_workspaces;
|
|
230
|
+
// }
|
|
231
|
+
// const { workspaces } = packageContent;
|
|
232
|
+
// if (Array.isArray(workspaces)) {
|
|
233
|
+
// for (const workspace of workspaces) {
|
|
234
|
+
// const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
|
|
235
|
+
// const workspaceUrl = workspaceUrlObject.href;
|
|
236
|
+
// if (workspaceUrl.endsWith("*")) {
|
|
237
|
+
// const directoryUrl = ensurePathnameTrailingSlash(
|
|
238
|
+
// workspaceUrl.slice(0, -1),
|
|
239
|
+
// );
|
|
240
|
+
// fileUrls.push(new URL(directoryUrl));
|
|
241
|
+
// } else {
|
|
242
|
+
// fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
|
|
243
|
+
// }
|
|
244
|
+
// }
|
|
245
|
+
// }
|
|
246
|
+
// }
|
|
247
|
+
}
|
|
248
|
+
const directoryUrlRelativeToServer =
|
|
249
|
+
FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
250
|
+
firstExistingDirectoryUrl,
|
|
251
|
+
serverRootDirectoryUrl,
|
|
252
|
+
);
|
|
253
|
+
const websocketScheme = request.protocol === "https" ? "wss" : "ws";
|
|
254
|
+
const { host } = new URL(request.url);
|
|
255
|
+
const websocketUrl = `${websocketScheme}://${host}${directoryUrlRelativeToServer}`;
|
|
256
|
+
|
|
257
|
+
const navItems = [];
|
|
258
|
+
nav_items: {
|
|
259
|
+
const lastItemUrl = firstExistingDirectoryUrl;
|
|
260
|
+
const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
|
|
261
|
+
const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
|
|
262
|
+
let parts;
|
|
263
|
+
if (lastItemRelativeUrl) {
|
|
264
|
+
parts = `${rootDirectoryUrlName}/${lastItemRelativeUrl}`.split("/");
|
|
265
|
+
} else {
|
|
266
|
+
parts = [rootDirectoryUrlName];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let i = 0;
|
|
270
|
+
while (i < parts.length) {
|
|
271
|
+
const part = parts[i];
|
|
272
|
+
const isLastPart = i === parts.length - 1;
|
|
273
|
+
if (isLastPart && part === "") {
|
|
274
|
+
// ignore trailing slash
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
let navItemRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
|
|
278
|
+
let navItemUrl =
|
|
279
|
+
navItemRelativeUrl === ""
|
|
280
|
+
? rootDirectoryUrl
|
|
281
|
+
: new URL(navItemRelativeUrl, rootDirectoryUrl).href;
|
|
282
|
+
if (!isLastPart) {
|
|
283
|
+
navItemUrl = ensurePathnameTrailingSlash(navItemUrl);
|
|
284
|
+
}
|
|
285
|
+
let urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
286
|
+
navItemUrl,
|
|
287
|
+
serverRootDirectoryUrl,
|
|
288
|
+
);
|
|
289
|
+
let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, requestedUrl);
|
|
290
|
+
const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
|
|
291
|
+
if (isServerRootDirectory) {
|
|
292
|
+
urlRelativeToServer = `/${directoryContentMagicName}`;
|
|
293
|
+
urlRelativeToDocument = `/${directoryContentMagicName}`;
|
|
294
|
+
}
|
|
295
|
+
const name = part;
|
|
296
|
+
const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
|
|
297
|
+
navItems.push({
|
|
298
|
+
url: navItemUrl,
|
|
299
|
+
urlRelativeToServer,
|
|
300
|
+
urlRelativeToDocument,
|
|
301
|
+
isServerRootDirectory,
|
|
302
|
+
isCurrent,
|
|
303
|
+
name,
|
|
304
|
+
});
|
|
305
|
+
i++;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let enoentDetails = null;
|
|
310
|
+
if (enoent) {
|
|
311
|
+
const fileRelativeUrl = urlToRelativeUrl(
|
|
312
|
+
requestedUrl,
|
|
313
|
+
serverRootDirectoryUrl,
|
|
314
|
+
);
|
|
315
|
+
let filePathExisting;
|
|
316
|
+
let filePathNotFound;
|
|
317
|
+
const existingIndex = String(firstExistingDirectoryUrl).length;
|
|
318
|
+
filePathExisting = urlToRelativeUrl(
|
|
319
|
+
firstExistingDirectoryUrl,
|
|
320
|
+
serverRootDirectoryUrl,
|
|
321
|
+
);
|
|
322
|
+
filePathNotFound = requestedUrl.slice(existingIndex);
|
|
323
|
+
enoentDetails = {
|
|
324
|
+
fileUrl: requestedUrl,
|
|
325
|
+
fileRelativeUrl,
|
|
326
|
+
filePathExisting: `/${filePathExisting}`,
|
|
327
|
+
filePathNotFound,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
__DIRECTORY_LISTING__: {
|
|
333
|
+
enoentDetails,
|
|
334
|
+
navItems,
|
|
335
|
+
directoryListingUrlMocks,
|
|
336
|
+
directoryContentMagicName,
|
|
337
|
+
directoryUrl: firstExistingDirectoryUrl,
|
|
338
|
+
serverRootDirectoryUrl,
|
|
339
|
+
rootDirectoryUrl,
|
|
340
|
+
mainFilePath,
|
|
341
|
+
directoryContentItems,
|
|
342
|
+
websocketUrl,
|
|
343
|
+
autoreload,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
|
|
348
|
+
let firstExistingDirectoryUrl = new URL("./", requestedUrl);
|
|
349
|
+
while (!existsSync(firstExistingDirectoryUrl)) {
|
|
350
|
+
firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
|
|
351
|
+
if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
|
|
352
|
+
firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return firstExistingDirectoryUrl;
|
|
357
|
+
};
|
|
358
|
+
const getDirectoryContentItems = ({
|
|
359
|
+
serverRootDirectoryUrl,
|
|
360
|
+
mainFilePath,
|
|
361
|
+
firstExistingDirectoryUrl,
|
|
362
|
+
}) => {
|
|
363
|
+
const directoryContentArray = readdirSync(new URL(firstExistingDirectoryUrl));
|
|
364
|
+
const fileUrls = [];
|
|
365
|
+
for (const filename of directoryContentArray) {
|
|
366
|
+
const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
|
|
367
|
+
if (lstatSync(fileUrlObject).isDirectory()) {
|
|
368
|
+
fileUrls.push(ensurePathnameTrailingSlash(fileUrlObject));
|
|
369
|
+
} else {
|
|
370
|
+
fileUrls.push(fileUrlObject);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
fileUrls.sort((a, b) => {
|
|
374
|
+
return comparePathnames(a.pathname, b.pathname);
|
|
375
|
+
});
|
|
376
|
+
const items = [];
|
|
377
|
+
for (const fileUrl of fileUrls) {
|
|
378
|
+
const urlRelativeToCurrentDirectory = urlToRelativeUrl(
|
|
379
|
+
fileUrl,
|
|
380
|
+
firstExistingDirectoryUrl,
|
|
381
|
+
);
|
|
382
|
+
const urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
383
|
+
fileUrl,
|
|
384
|
+
serverRootDirectoryUrl,
|
|
385
|
+
);
|
|
386
|
+
const url = String(fileUrl);
|
|
387
|
+
const mainFileUrl = new URL(mainFilePath, serverRootDirectoryUrl).href;
|
|
388
|
+
const isMainFile = url === mainFileUrl;
|
|
389
|
+
|
|
390
|
+
items.push({
|
|
391
|
+
url,
|
|
392
|
+
urlRelativeToCurrentDirectory,
|
|
393
|
+
urlRelativeToServer,
|
|
394
|
+
isMainFile,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return items;
|
|
398
|
+
};
|