@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
|
@@ -1,32 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
comparePathnames,
|
|
4
|
-
readEntryStatSync,
|
|
5
|
-
} from "@jsenv/filesystem";
|
|
6
|
-
import { pickContentType } from "@jsenv/server";
|
|
7
|
-
import {
|
|
8
|
-
ensurePathnameTrailingSlash,
|
|
9
|
-
urlIsInsideOf,
|
|
10
|
-
urlToFilename,
|
|
11
|
-
urlToRelativeUrl,
|
|
12
|
-
} from "@jsenv/urls";
|
|
1
|
+
import { readEntryStatSync } from "@jsenv/filesystem";
|
|
2
|
+
import { ensurePathnameTrailingSlash } from "@jsenv/urls";
|
|
13
3
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
4
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { FILE_AND_SERVER_URLS_CONVERTER } from "./file_and_server_urls_converter.js";
|
|
6
|
+
import { jsenvPluginDirectoryListing } from "./jsenv_plugin_directory_listing.js";
|
|
17
7
|
import { jsenvPluginFsRedirection } from "./jsenv_plugin_fs_redirection.js";
|
|
18
8
|
|
|
19
|
-
const html404AndAncestorDirFileUrl = new URL(
|
|
20
|
-
"./client/html_404_and_ancestor_dir.html",
|
|
21
|
-
import.meta.url,
|
|
22
|
-
);
|
|
23
|
-
const htmlFileUrlForDirectory = new URL(
|
|
24
|
-
"./client/directory.html",
|
|
25
|
-
import.meta.url,
|
|
26
|
-
);
|
|
27
9
|
const directoryContentMagicName = "...";
|
|
28
10
|
|
|
29
11
|
export const jsenvPluginProtocolFile = ({
|
|
12
|
+
supervisorEnabled,
|
|
30
13
|
magicExtensions,
|
|
31
14
|
magicDirectoryIndex,
|
|
32
15
|
preserveSymlinks,
|
|
@@ -61,8 +44,7 @@ export const jsenvPluginProtocolFile = ({
|
|
|
61
44
|
appliesDuring: "dev",
|
|
62
45
|
resolveReference: (reference) => {
|
|
63
46
|
if (reference.specifier.startsWith("/@fs/")) {
|
|
64
|
-
|
|
65
|
-
return `file:///${fsRootRelativeUrl}`;
|
|
47
|
+
return FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(reference.specifier);
|
|
66
48
|
}
|
|
67
49
|
return null;
|
|
68
50
|
},
|
|
@@ -81,12 +63,40 @@ export const jsenvPluginProtocolFile = ({
|
|
|
81
63
|
}
|
|
82
64
|
}
|
|
83
65
|
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
return FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
67
|
+
generatedUrl,
|
|
68
|
+
rootDirectoryUrl,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
jsenvPluginDirectoryListing({
|
|
73
|
+
supervisorEnabled,
|
|
74
|
+
directoryContentMagicName,
|
|
75
|
+
directoryListingUrlMocks,
|
|
76
|
+
}),
|
|
77
|
+
{
|
|
78
|
+
name: "jsenv:directory_as_json",
|
|
79
|
+
appliesDuring: "*",
|
|
80
|
+
fetchUrlContent: (urlInfo) => {
|
|
81
|
+
const { firstReference } = urlInfo;
|
|
82
|
+
let { fsStat } = firstReference;
|
|
83
|
+
if (!fsStat) {
|
|
84
|
+
fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
|
|
85
|
+
}
|
|
86
|
+
if (!fsStat) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const isDirectory = fsStat.isDirectory();
|
|
90
|
+
if (!isDirectory) {
|
|
91
|
+
return null;
|
|
87
92
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
93
|
+
const directoryContentArray = readdirSync(new URL(urlInfo.url));
|
|
94
|
+
const content = JSON.stringify(directoryContentArray, null, " ");
|
|
95
|
+
return {
|
|
96
|
+
type: "directory",
|
|
97
|
+
contentType: "application/json",
|
|
98
|
+
content,
|
|
99
|
+
};
|
|
90
100
|
},
|
|
91
101
|
},
|
|
92
102
|
{
|
|
@@ -97,13 +107,10 @@ export const jsenvPluginProtocolFile = ({
|
|
|
97
107
|
return null;
|
|
98
108
|
}
|
|
99
109
|
const { firstReference } = urlInfo;
|
|
100
|
-
const { mainFilePath } = urlInfo.context;
|
|
101
110
|
let { fsStat } = firstReference;
|
|
102
111
|
if (!fsStat) {
|
|
103
112
|
fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
|
|
104
113
|
}
|
|
105
|
-
const isDirectory = fsStat?.isDirectory();
|
|
106
|
-
const { rootDirectoryUrl, request } = urlInfo.context;
|
|
107
114
|
const serveFile = (url) => {
|
|
108
115
|
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
109
116
|
const fileBuffer = readFileSync(new URL(url));
|
|
@@ -117,344 +124,8 @@ export const jsenvPluginProtocolFile = ({
|
|
|
117
124
|
};
|
|
118
125
|
};
|
|
119
126
|
|
|
120
|
-
if (!fsStat) {
|
|
121
|
-
if (request && request.headers["sec-fetch-dest"] === "document") {
|
|
122
|
-
const directoryContentItems = generateDirectoryContentItems(
|
|
123
|
-
urlInfo.url,
|
|
124
|
-
rootDirectoryUrl,
|
|
125
|
-
);
|
|
126
|
-
const html = generateHtmlForENOENT(
|
|
127
|
-
urlInfo.url,
|
|
128
|
-
directoryContentItems,
|
|
129
|
-
directoryListingUrlMocks,
|
|
130
|
-
{ mainFilePath },
|
|
131
|
-
);
|
|
132
|
-
return {
|
|
133
|
-
status: 404,
|
|
134
|
-
contentType: "text/html",
|
|
135
|
-
content: html,
|
|
136
|
-
headers: {
|
|
137
|
-
"cache-control": "no-cache",
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (isDirectory) {
|
|
143
|
-
const directoryContentArray = readdirSync(new URL(urlInfo.url));
|
|
144
|
-
if (firstReference.type === "filesystem") {
|
|
145
|
-
const content = JSON.stringify(directoryContentArray, null, " ");
|
|
146
|
-
return {
|
|
147
|
-
type: "directory",
|
|
148
|
-
contentType: "application/json",
|
|
149
|
-
content,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
const acceptsHtml = request
|
|
153
|
-
? pickContentType(request, ["text/html"])
|
|
154
|
-
: false;
|
|
155
|
-
if (acceptsHtml) {
|
|
156
|
-
firstReference.expectedType = "html";
|
|
157
|
-
const directoryUrl = urlInfo.url;
|
|
158
|
-
const directoryContentItems = generateDirectoryContentItems(
|
|
159
|
-
directoryUrl,
|
|
160
|
-
rootDirectoryUrl,
|
|
161
|
-
);
|
|
162
|
-
const html = generateHtmlForDirectory(directoryContentItems, {
|
|
163
|
-
mainFilePath,
|
|
164
|
-
});
|
|
165
|
-
return {
|
|
166
|
-
type: "html",
|
|
167
|
-
contentType: "text/html",
|
|
168
|
-
content: html,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
type: "directory",
|
|
173
|
-
contentType: "application/json",
|
|
174
|
-
content: JSON.stringify(directoryContentArray, null, " "),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
127
|
return serveFile(urlInfo.url);
|
|
178
128
|
},
|
|
179
129
|
},
|
|
180
130
|
];
|
|
181
131
|
};
|
|
182
|
-
|
|
183
|
-
const generateHtmlForDirectory = (directoryContentItems, { mainFilePath }) => {
|
|
184
|
-
let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
|
|
185
|
-
const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
|
|
186
|
-
directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
|
|
187
|
-
|
|
188
|
-
const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
|
|
189
|
-
const replacers = {
|
|
190
|
-
directoryUrl,
|
|
191
|
-
directoryNav: () =>
|
|
192
|
-
generateDirectoryNav(directoryUrl, {
|
|
193
|
-
rootDirectoryUrl,
|
|
194
|
-
rootDirectoryUrlForServer:
|
|
195
|
-
directoryContentItems.rootDirectoryUrlForServer,
|
|
196
|
-
mainFilePath,
|
|
197
|
-
}),
|
|
198
|
-
directoryContent: () =>
|
|
199
|
-
generateDirectoryContent(directoryContentItems, { mainFilePath }),
|
|
200
|
-
};
|
|
201
|
-
const html = replacePlaceholders(htmlForDirectory, replacers);
|
|
202
|
-
return html;
|
|
203
|
-
};
|
|
204
|
-
const generateHtmlForENOENT = (
|
|
205
|
-
url,
|
|
206
|
-
directoryContentItems,
|
|
207
|
-
directoryListingUrlMocks,
|
|
208
|
-
{ mainFilePath },
|
|
209
|
-
) => {
|
|
210
|
-
const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
|
|
211
|
-
const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
|
|
212
|
-
|
|
213
|
-
const htmlFor404AndAncestorDir = String(
|
|
214
|
-
readFileSync(html404AndAncestorDirFileUrl),
|
|
215
|
-
);
|
|
216
|
-
const fileRelativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
|
|
217
|
-
const ancestorDirectoryRelativeUrl = urlToRelativeUrl(
|
|
218
|
-
ancestorDirectoryUrl,
|
|
219
|
-
rootDirectoryUrl,
|
|
220
|
-
);
|
|
221
|
-
const replacers = {
|
|
222
|
-
fileUrl: directoryListingUrlMocks
|
|
223
|
-
? `@jsenv/core/${urlToRelativeUrl(url, jsenvCoreDirectoryUrl)}`
|
|
224
|
-
: url,
|
|
225
|
-
fileRelativeUrl,
|
|
226
|
-
ancestorDirectoryUrl,
|
|
227
|
-
ancestorDirectoryRelativeUrl,
|
|
228
|
-
ancestorDirectoryNav: () =>
|
|
229
|
-
generateDirectoryNav(ancestorDirectoryUrl, {
|
|
230
|
-
rootDirectoryUrl,
|
|
231
|
-
rootDirectoryUrlForServer:
|
|
232
|
-
directoryContentItems.rootDirectoryUrlForServer,
|
|
233
|
-
mainFilePath,
|
|
234
|
-
}),
|
|
235
|
-
ancestorDirectoryContent: () =>
|
|
236
|
-
generateDirectoryContent(directoryContentItems, { mainFilePath }),
|
|
237
|
-
};
|
|
238
|
-
const html = replacePlaceholders(htmlFor404AndAncestorDir, replacers);
|
|
239
|
-
return html;
|
|
240
|
-
};
|
|
241
|
-
const generateDirectoryNav = (
|
|
242
|
-
entryDirectoryUrl,
|
|
243
|
-
{ rootDirectoryUrl, rootDirectoryUrlForServer, mainFilePath },
|
|
244
|
-
) => {
|
|
245
|
-
const entryDirectoryRelativeUrl = urlToRelativeUrl(
|
|
246
|
-
entryDirectoryUrl,
|
|
247
|
-
rootDirectoryUrl,
|
|
248
|
-
);
|
|
249
|
-
const isDir =
|
|
250
|
-
entryDirectoryRelativeUrl === "" || entryDirectoryRelativeUrl.endsWith("/");
|
|
251
|
-
const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
|
|
252
|
-
const items = [];
|
|
253
|
-
let dirPartsHtml = "";
|
|
254
|
-
const parts = entryDirectoryRelativeUrl
|
|
255
|
-
? `${rootDirectoryUrlName}/${entryDirectoryRelativeUrl.slice(0, -1)}`.split(
|
|
256
|
-
"/",
|
|
257
|
-
)
|
|
258
|
-
: [rootDirectoryUrlName];
|
|
259
|
-
let i = 0;
|
|
260
|
-
while (i < parts.length) {
|
|
261
|
-
const part = parts[i];
|
|
262
|
-
const directoryRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
|
|
263
|
-
const directoryUrl =
|
|
264
|
-
directoryRelativeUrl === ""
|
|
265
|
-
? rootDirectoryUrl
|
|
266
|
-
: new URL(`${directoryRelativeUrl}/`, rootDirectoryUrl).href;
|
|
267
|
-
let href =
|
|
268
|
-
directoryUrl === rootDirectoryUrlForServer ||
|
|
269
|
-
urlIsInsideOf(directoryUrl, rootDirectoryUrlForServer)
|
|
270
|
-
? urlToRelativeUrl(directoryUrl, rootDirectoryUrlForServer)
|
|
271
|
-
: directoryUrl;
|
|
272
|
-
if (href === "") {
|
|
273
|
-
href = `/${directoryContentMagicName}`;
|
|
274
|
-
} else {
|
|
275
|
-
href = `/${href}`;
|
|
276
|
-
}
|
|
277
|
-
const text = part;
|
|
278
|
-
items.push({
|
|
279
|
-
href,
|
|
280
|
-
text,
|
|
281
|
-
});
|
|
282
|
-
i++;
|
|
283
|
-
}
|
|
284
|
-
i = 0;
|
|
285
|
-
|
|
286
|
-
const renderDirNavItem = ({ isCurrent, href, text }) => {
|
|
287
|
-
const isServerRootDir = href === `/${directoryContentMagicName}`;
|
|
288
|
-
if (isServerRootDir) {
|
|
289
|
-
if (isCurrent) {
|
|
290
|
-
return `
|
|
291
|
-
<span class="directory_nav_item" data-current>
|
|
292
|
-
<a class="directory_root_for_server" hot-decline href="/${mainFilePath}"></a>
|
|
293
|
-
<span class="directory_name">${text}</span>
|
|
294
|
-
</span>`;
|
|
295
|
-
}
|
|
296
|
-
return `
|
|
297
|
-
<span class="directory_nav_item">
|
|
298
|
-
<a class="directory_root_for_server" hot-decline href="/${mainFilePath}"></a>
|
|
299
|
-
<a class="directory_name" hot-decline href="${href}">${text}</a>
|
|
300
|
-
</span>`;
|
|
301
|
-
}
|
|
302
|
-
if (isCurrent) {
|
|
303
|
-
return `
|
|
304
|
-
<span class="directory_nav_item" data-current>
|
|
305
|
-
<span class="directory_text">${text}</span>
|
|
306
|
-
</span>`;
|
|
307
|
-
}
|
|
308
|
-
return `
|
|
309
|
-
<span class="directory_nav_item">
|
|
310
|
-
<a class="directory_text" hot-decline href="${href}">${text}</a>
|
|
311
|
-
</span>`;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
for (const { href, text } of items) {
|
|
315
|
-
const isLastPart = i === items.length - 1;
|
|
316
|
-
dirPartsHtml += renderDirNavItem({
|
|
317
|
-
isCurrent: isLastPart,
|
|
318
|
-
href,
|
|
319
|
-
text,
|
|
320
|
-
});
|
|
321
|
-
if (isLastPart) {
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
dirPartsHtml += `
|
|
325
|
-
<span class="directory_separator">/</span>`;
|
|
326
|
-
i++;
|
|
327
|
-
}
|
|
328
|
-
if (isDir) {
|
|
329
|
-
dirPartsHtml += `
|
|
330
|
-
<span class="directory_separator">/</span>`;
|
|
331
|
-
}
|
|
332
|
-
return dirPartsHtml;
|
|
333
|
-
};
|
|
334
|
-
const generateDirectoryContentItems = (
|
|
335
|
-
directoryUrl,
|
|
336
|
-
rootDirectoryUrlForServer,
|
|
337
|
-
) => {
|
|
338
|
-
let firstExistingDirectoryUrl = new URL("./", directoryUrl);
|
|
339
|
-
while (!existsSync(firstExistingDirectoryUrl)) {
|
|
340
|
-
firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
|
|
341
|
-
if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrlForServer)) {
|
|
342
|
-
firstExistingDirectoryUrl = new URL(rootDirectoryUrlForServer);
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
|
|
347
|
-
const fileUrls = [];
|
|
348
|
-
for (const filename of directoryContentArray) {
|
|
349
|
-
const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
|
|
350
|
-
fileUrls.push(fileUrlObject);
|
|
351
|
-
}
|
|
352
|
-
let rootDirectoryUrl = rootDirectoryUrlForServer;
|
|
353
|
-
package_workspaces: {
|
|
354
|
-
const packageDirectoryUrl = lookupPackageDirectory(
|
|
355
|
-
rootDirectoryUrlForServer,
|
|
356
|
-
);
|
|
357
|
-
if (!packageDirectoryUrl) {
|
|
358
|
-
break package_workspaces;
|
|
359
|
-
}
|
|
360
|
-
if (String(packageDirectoryUrl) === String(rootDirectoryUrlForServer)) {
|
|
361
|
-
break package_workspaces;
|
|
362
|
-
}
|
|
363
|
-
rootDirectoryUrl = packageDirectoryUrl;
|
|
364
|
-
if (
|
|
365
|
-
String(firstExistingDirectoryUrl) === String(rootDirectoryUrlForServer)
|
|
366
|
-
) {
|
|
367
|
-
let packageContent;
|
|
368
|
-
try {
|
|
369
|
-
packageContent = JSON.parse(
|
|
370
|
-
readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
|
|
371
|
-
);
|
|
372
|
-
} catch {
|
|
373
|
-
break package_workspaces;
|
|
374
|
-
}
|
|
375
|
-
const { workspaces } = packageContent;
|
|
376
|
-
if (Array.isArray(workspaces)) {
|
|
377
|
-
for (const workspace of workspaces) {
|
|
378
|
-
const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
|
|
379
|
-
const workspaceUrl = workspaceUrlObject.href;
|
|
380
|
-
if (workspaceUrl.endsWith("*")) {
|
|
381
|
-
const directoryUrl = ensurePathnameTrailingSlash(
|
|
382
|
-
workspaceUrl.slice(0, -1),
|
|
383
|
-
);
|
|
384
|
-
fileUrls.push(new URL(directoryUrl));
|
|
385
|
-
} else {
|
|
386
|
-
fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const sortedUrls = [];
|
|
394
|
-
for (let fileUrl of fileUrls) {
|
|
395
|
-
if (lstatSync(fileUrl).isDirectory()) {
|
|
396
|
-
sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
|
|
397
|
-
} else {
|
|
398
|
-
sortedUrls.push(fileUrl);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
sortedUrls.sort((a, b) => {
|
|
402
|
-
return comparePathnames(a.pathname, b.pathname);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
const items = [];
|
|
406
|
-
for (const sortedUrl of sortedUrls) {
|
|
407
|
-
const fileUrlRelativeToParent = urlToRelativeUrl(
|
|
408
|
-
sortedUrl,
|
|
409
|
-
firstExistingDirectoryUrl,
|
|
410
|
-
);
|
|
411
|
-
const fileUrlRelativeToServer = urlToRelativeUrl(
|
|
412
|
-
sortedUrl,
|
|
413
|
-
rootDirectoryUrlForServer,
|
|
414
|
-
);
|
|
415
|
-
const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
|
|
416
|
-
items.push({
|
|
417
|
-
type,
|
|
418
|
-
fileUrlRelativeToParent,
|
|
419
|
-
fileUrlRelativeToServer,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
items.rootDirectoryUrlForServer = rootDirectoryUrlForServer;
|
|
423
|
-
items.rootDirectoryUrl = rootDirectoryUrl;
|
|
424
|
-
items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
|
|
425
|
-
return items;
|
|
426
|
-
};
|
|
427
|
-
const generateDirectoryContent = (directoryContentItems, { mainFilePath }) => {
|
|
428
|
-
if (directoryContentItems.length === 0) {
|
|
429
|
-
return `<p class="directory_empty_message">Directory is empty</p>`;
|
|
430
|
-
}
|
|
431
|
-
let html = `<ul class="directory_content">`;
|
|
432
|
-
for (const directoryContentItem of directoryContentItems) {
|
|
433
|
-
const { type, fileUrlRelativeToParent, fileUrlRelativeToServer } =
|
|
434
|
-
directoryContentItem;
|
|
435
|
-
let href = fileUrlRelativeToServer;
|
|
436
|
-
if (href === "") {
|
|
437
|
-
href = `${directoryContentMagicName}`;
|
|
438
|
-
}
|
|
439
|
-
const isMainFile = href === mainFilePath;
|
|
440
|
-
const mainFileAttr = isMainFile ? ` data-main-file` : "";
|
|
441
|
-
html += `
|
|
442
|
-
<li class="directory_child" data-type="${type}"${mainFileAttr}>
|
|
443
|
-
<a href="/${href}" hot-decline>${fileUrlRelativeToParent}</a>
|
|
444
|
-
</li>`;
|
|
445
|
-
}
|
|
446
|
-
html += `\n </ul>`;
|
|
447
|
-
return html;
|
|
448
|
-
};
|
|
449
|
-
const replacePlaceholders = (html, replacers) => {
|
|
450
|
-
return html.replace(/\$\{(\w+)\}/g, (match, name) => {
|
|
451
|
-
const replacer = replacers[name];
|
|
452
|
-
if (replacer === undefined) {
|
|
453
|
-
return match;
|
|
454
|
-
}
|
|
455
|
-
if (typeof replacer === "function") {
|
|
456
|
-
return replacer();
|
|
457
|
-
}
|
|
458
|
-
return replacer;
|
|
459
|
-
});
|
|
460
|
-
};
|
|
@@ -55,10 +55,11 @@ export const jsenvPluginProtocolHttp = ({ include }) => {
|
|
|
55
55
|
return fileUrl;
|
|
56
56
|
},
|
|
57
57
|
fetchUrlContent: async (urlInfo) => {
|
|
58
|
-
|
|
58
|
+
const originalUrl = urlInfo.originalUrl;
|
|
59
|
+
if (!originalUrl.startsWith("http")) {
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
61
|
-
const response = await fetch(
|
|
62
|
+
const response = await fetch(originalUrl);
|
|
62
63
|
const responseStatus = response.status;
|
|
63
64
|
if (responseStatus < 200 || responseStatus > 299) {
|
|
64
65
|
throw new Error(`unexpected response status ${responseStatus}`);
|
|
@@ -256,9 +256,11 @@ export const jsenvPluginHtmlReferenceAnalysis = ({
|
|
|
256
256
|
const { line, column, isOriginal } = getHtmlNodePosition(node, {
|
|
257
257
|
preferOriginal: true,
|
|
258
258
|
});
|
|
259
|
-
const inlineContentUrl = getUrlForContentInsideHtml(
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
const inlineContentUrl = getUrlForContentInsideHtml(
|
|
260
|
+
node,
|
|
261
|
+
urlInfo,
|
|
262
|
+
null,
|
|
263
|
+
);
|
|
262
264
|
const debug =
|
|
263
265
|
getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
|
|
264
266
|
const inlineReference = urlInfo.dependencies.foundInline({
|
|
@@ -399,9 +401,8 @@ export const jsenvPluginHtmlReferenceAnalysis = ({
|
|
|
399
401
|
);
|
|
400
402
|
const importmapInlineUrl = getUrlForContentInsideHtml(
|
|
401
403
|
scriptNode,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
},
|
|
404
|
+
urlInfo,
|
|
405
|
+
importmapReference,
|
|
405
406
|
);
|
|
406
407
|
const importmapReferenceInlined = importmapReference.inline({
|
|
407
408
|
line,
|
|
@@ -41,9 +41,7 @@ const parseAndTransformJsReferences = async (
|
|
|
41
41
|
Object.keys(urlInfo.context.runtimeCompat).toString() === "node";
|
|
42
42
|
|
|
43
43
|
const onInlineReference = (inlineReferenceInfo) => {
|
|
44
|
-
const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo,
|
|
45
|
-
url: urlInfo.url,
|
|
46
|
-
});
|
|
44
|
+
const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, urlInfo);
|
|
47
45
|
let { quote } = inlineReferenceInfo;
|
|
48
46
|
if (quote === "`" && !canUseTemplateLiterals) {
|
|
49
47
|
// if quote is "`" and template literals are not supported
|
|
@@ -41,23 +41,7 @@ const jsenvPluginInlineContentFetcher = () => {
|
|
|
41
41
|
if (!urlInfo.isInline) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
if (urlInfo.context.request) {
|
|
46
|
-
let requestResource = urlInfo.context.request.resource;
|
|
47
|
-
let requestedUrl;
|
|
48
|
-
if (requestResource.startsWith("/@fs/")) {
|
|
49
|
-
const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
|
|
50
|
-
requestedUrl = `file:///${fsRootRelativeUrl}`;
|
|
51
|
-
} else {
|
|
52
|
-
const requestedUrlObject = new URL(
|
|
53
|
-
requestResource.slice(1),
|
|
54
|
-
urlInfo.context.rootDirectoryUrl,
|
|
55
|
-
);
|
|
56
|
-
requestedUrlObject.searchParams.delete("hot");
|
|
57
|
-
requestedUrl = requestedUrlObject.href;
|
|
58
|
-
}
|
|
59
|
-
isDirectRequestToFile = requestedUrl === urlInfo.url;
|
|
60
|
-
}
|
|
44
|
+
const { isDirectRequest } = urlInfo.lastReference;
|
|
61
45
|
/*
|
|
62
46
|
* We want to find inline content but it's not straightforward
|
|
63
47
|
*
|
|
@@ -86,7 +70,7 @@ const jsenvPluginInlineContentFetcher = () => {
|
|
|
86
70
|
originalContent = reference.content;
|
|
87
71
|
}
|
|
88
72
|
lastInlineReference = reference;
|
|
89
|
-
if (
|
|
73
|
+
if (isDirectRequest) {
|
|
90
74
|
break;
|
|
91
75
|
}
|
|
92
76
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This plugin is very special because it is here
|
|
3
|
+
* to provide "serverEvents" used by other plugins
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
|
|
7
|
+
import { createServerEventsDispatcher } from "./server_events_dispatcher.js";
|
|
8
|
+
|
|
9
|
+
const serverEventsClientFileUrl = new URL(
|
|
10
|
+
"./client/server_events_client.js",
|
|
11
|
+
import.meta.url,
|
|
12
|
+
).href;
|
|
13
|
+
|
|
14
|
+
export const jsenvPluginServerEvents = ({ clientAutoreload }) => {
|
|
15
|
+
let serverEventsDispatcher;
|
|
16
|
+
|
|
17
|
+
const { clientServerEventsConfig } = clientAutoreload;
|
|
18
|
+
const { logs = true } = clientServerEventsConfig;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name: "jsenv:server_events",
|
|
22
|
+
appliesDuring: "dev",
|
|
23
|
+
effect: ({ kitchenContext, otherPlugins }) => {
|
|
24
|
+
const allServerEvents = {};
|
|
25
|
+
for (const otherPlugin of otherPlugins) {
|
|
26
|
+
const { serverEvents } = otherPlugin;
|
|
27
|
+
if (!serverEvents) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const serverEventName of Object.keys(serverEvents)) {
|
|
31
|
+
// we could throw on serverEvent name conflict
|
|
32
|
+
// we could throw if serverEvents[serverEventName] is not a function
|
|
33
|
+
allServerEvents[serverEventName] = serverEvents[serverEventName];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const serverEventNames = Object.keys(allServerEvents);
|
|
37
|
+
if (serverEventNames.length === 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
serverEventsDispatcher = createServerEventsDispatcher();
|
|
41
|
+
const onabort = () => {
|
|
42
|
+
serverEventsDispatcher.destroy();
|
|
43
|
+
};
|
|
44
|
+
kitchenContext.signal.addEventListener("abort", onabort);
|
|
45
|
+
for (const serverEventName of Object.keys(allServerEvents)) {
|
|
46
|
+
const serverEventInfo = {
|
|
47
|
+
...kitchenContext,
|
|
48
|
+
// serverEventsDispatcher variable is safe, we can disable esling warning
|
|
49
|
+
// eslint-disable-next-line no-loop-func
|
|
50
|
+
sendServerEvent: (data) => {
|
|
51
|
+
if (!serverEventsDispatcher) {
|
|
52
|
+
// this can happen if a plugin wants to send a server event but
|
|
53
|
+
// server is closing or the plugin got destroyed but still wants to do things
|
|
54
|
+
// if plugin code is correctly written it is never supposed to happen
|
|
55
|
+
// because it means a plugin is still trying to do stuff after being destroyed
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
serverEventsDispatcher.dispatch({
|
|
59
|
+
type: serverEventName,
|
|
60
|
+
data,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const serverEventInit = allServerEvents[serverEventName];
|
|
65
|
+
serverEventInit(serverEventInfo);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
kitchenContext.signal.removeEventListener("abort", onabort);
|
|
69
|
+
serverEventsDispatcher.destroy();
|
|
70
|
+
serverEventsDispatcher = undefined;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
serveWebsocket: async ({ websocket, request }) => {
|
|
74
|
+
if (request.headers["sec-websocket-protocol"] !== "jsenv") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
serverEventsDispatcher.addWebsocket(websocket, request);
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
transformUrlContent: {
|
|
81
|
+
html: (urlInfo) => {
|
|
82
|
+
const htmlAst = parseHtml({
|
|
83
|
+
html: urlInfo.content,
|
|
84
|
+
url: urlInfo.url,
|
|
85
|
+
});
|
|
86
|
+
injectJsenvScript(htmlAst, {
|
|
87
|
+
src: serverEventsClientFileUrl,
|
|
88
|
+
initCall: {
|
|
89
|
+
callee: "window.__server_events__.setup",
|
|
90
|
+
params: {
|
|
91
|
+
logs,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
pluginName: "jsenv:server_events",
|
|
95
|
+
});
|
|
96
|
+
return stringifyHtmlAst(htmlAst);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|