@jsenv/core 39.11.2 → 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.
Files changed (37) hide show
  1. package/dist/css/directory_listing.css +211 -0
  2. package/dist/html/directory_listing.html +18 -0
  3. package/dist/js/directory_listing.js +240 -0
  4. package/dist/jsenv_core.js +1057 -757
  5. package/dist/other/dir.png +0 -0
  6. package/dist/other/file.png +0 -0
  7. package/dist/other/home.svg +6 -0
  8. package/package.json +6 -6
  9. package/src/build/build.js +7 -7
  10. package/src/build/build_specifier_manager.js +0 -1
  11. package/src/dev/start_dev_server.js +39 -49
  12. package/src/kitchen/kitchen.js +20 -4
  13. package/src/kitchen/out_directory_url.js +2 -1
  14. package/src/kitchen/url_graph/references.js +3 -1
  15. package/src/kitchen/url_graph/url_graph.js +1 -0
  16. package/src/kitchen/url_graph/url_info_transformations.js +37 -4
  17. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
  18. package/src/plugins/plugin_controller.js +170 -114
  19. package/src/plugins/plugins.js +5 -4
  20. package/src/plugins/protocol_file/client/assets/home.svg +6 -0
  21. package/src/plugins/protocol_file/client/directory_listing.css +190 -0
  22. package/src/plugins/protocol_file/client/directory_listing.html +18 -0
  23. package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
  24. package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
  25. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +40 -333
  27. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
  28. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
  29. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
  30. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
  31. package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
  32. package/dist/html/directory.html +0 -165
  33. package/dist/html/html_404_and_ancestor_dir.html +0 -203
  34. package/src/plugins/protocol_file/client/assets/directory.css +0 -133
  35. package/src/plugins/protocol_file/client/directory.html +0 -17
  36. package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
  37. 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
+ };