@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.
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 -793
  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 +5 -5
  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 -369
  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 -184
  33. package/dist/html/html_404_and_ancestor_dir.html +0 -222
  34. package/src/plugins/protocol_file/client/assets/directory.css +0 -150
  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
@@ -1,32 +1,15 @@
1
- import {
2
- assertAndNormalizeDirectoryUrl,
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 { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
15
- import { lookupPackageDirectory } from "../../helpers/lookup_package_directory.js";
16
- import { jsenvCoreDirectoryUrl } from "../../jsenv_core_directory_url.js";
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
- const fsRootRelativeUrl = reference.specifier.slice("/@fs/".length);
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
- if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
85
- const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
86
- return result;
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 result = `/@fs/${generatedUrl.slice("file:///".length)}`;
89
- return result;
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
- if (!urlInfo.originalUrl.startsWith("http")) {
58
+ const originalUrl = urlInfo.originalUrl;
59
+ if (!originalUrl.startsWith("http")) {
59
60
  return null;
60
61
  }
61
- const response = await fetch(urlInfo.originalUrl);
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(node, {
260
- htmlUrl: urlInfo.url,
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
- htmlUrl: urlInfo.url,
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
- let isDirectRequestToFile;
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 (isDirectRequestToFile) {
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
+ };