@jsenv/core 39.12.0 → 39.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) 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 +1035 -764
  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/build/build_urls_generator.js +0 -1
  12. package/src/dev/start_dev_server.js +66 -52
  13. package/src/kitchen/kitchen.js +1 -4
  14. package/src/kitchen/out_directory_url.js +2 -1
  15. package/src/kitchen/url_graph/references.js +1 -1
  16. package/src/kitchen/url_graph/url_graph.js +1 -0
  17. package/src/kitchen/url_graph/url_info_transformations.js +37 -4
  18. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
  19. package/src/plugins/plugin_controller.js +170 -114
  20. package/src/plugins/plugins.js +10 -6
  21. package/src/plugins/protocol_file/client/assets/home.svg +5 -5
  22. package/src/plugins/protocol_file/client/directory_listing.css +190 -0
  23. package/src/plugins/protocol_file/client/directory_listing.html +18 -0
  24. package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
  25. package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
  27. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +43 -370
  28. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
  29. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
  30. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
  31. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
  32. package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
  33. package/dist/html/directory.html +0 -184
  34. package/dist/html/html_404_and_ancestor_dir.html +0 -222
  35. package/src/plugins/protocol_file/client/assets/directory.css +0 -150
  36. package/src/plugins/protocol_file/client/directory.html +0 -17
  37. package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
  38. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +0 -37
@@ -1,36 +1,18 @@
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 = ({
30
12
  magicExtensions,
31
13
  magicDirectoryIndex,
32
14
  preserveSymlinks,
33
- directoryListingUrlMocks,
15
+ directoryListing,
34
16
  }) => {
35
17
  return [
36
18
  jsenvPluginFsRedirection({
@@ -61,8 +43,7 @@ export const jsenvPluginProtocolFile = ({
61
43
  appliesDuring: "dev",
62
44
  resolveReference: (reference) => {
63
45
  if (reference.specifier.startsWith("/@fs/")) {
64
- const fsRootRelativeUrl = reference.specifier.slice("/@fs/".length);
65
- return `file:///${fsRootRelativeUrl}`;
46
+ return FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(reference.specifier);
66
47
  }
67
48
  return null;
68
49
  },
@@ -81,12 +62,43 @@ export const jsenvPluginProtocolFile = ({
81
62
  }
82
63
  }
83
64
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
84
- if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
85
- const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
86
- return result;
65
+ return FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
66
+ generatedUrl,
67
+ rootDirectoryUrl,
68
+ );
69
+ },
70
+ },
71
+ ...(directoryListing
72
+ ? [
73
+ jsenvPluginDirectoryListing({
74
+ ...directoryListing,
75
+ directoryContentMagicName,
76
+ }),
77
+ ]
78
+ : []),
79
+ {
80
+ name: "jsenv:directory_as_json",
81
+ appliesDuring: "*",
82
+ fetchUrlContent: (urlInfo) => {
83
+ const { firstReference } = urlInfo;
84
+ let { fsStat } = firstReference;
85
+ if (!fsStat) {
86
+ fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
87
87
  }
88
- const result = `/@fs/${generatedUrl.slice("file:///".length)}`;
89
- return result;
88
+ if (!fsStat) {
89
+ return null;
90
+ }
91
+ const isDirectory = fsStat.isDirectory();
92
+ if (!isDirectory) {
93
+ return null;
94
+ }
95
+ const directoryContentArray = readdirSync(new URL(urlInfo.url));
96
+ const content = JSON.stringify(directoryContentArray, null, " ");
97
+ return {
98
+ type: "directory",
99
+ contentType: "application/json",
100
+ content,
101
+ };
90
102
  },
91
103
  },
92
104
  {
@@ -97,13 +109,10 @@ export const jsenvPluginProtocolFile = ({
97
109
  return null;
98
110
  }
99
111
  const { firstReference } = urlInfo;
100
- const { mainFilePath } = urlInfo.context;
101
112
  let { fsStat } = firstReference;
102
113
  if (!fsStat) {
103
114
  fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
104
115
  }
105
- const isDirectory = fsStat?.isDirectory();
106
- const { rootDirectoryUrl, request } = urlInfo.context;
107
116
  const serveFile = (url) => {
108
117
  const contentType = CONTENT_TYPE.fromUrlExtension(url);
109
118
  const fileBuffer = readFileSync(new URL(url));
@@ -117,344 +126,8 @@ export const jsenvPluginProtocolFile = ({
117
126
  };
118
127
  };
119
128
 
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
129
  return serveFile(urlInfo.url);
178
130
  },
179
131
  },
180
132
  ];
181
133
  };
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.context.requestedUrl === urlInfo.url;
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
+ };