@jsenv/core 39.14.3 → 40.0.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 (34) hide show
  1. package/dist/js/directory_listing.js +16 -9
  2. package/dist/js/server_events_client.js +2 -2
  3. package/dist/jsenv_core.js +7220 -11089
  4. package/package.json +22 -19
  5. package/src/build/build.js +122 -93
  6. package/src/build/build_specifier_manager.js +103 -94
  7. package/src/build/build_urls_generator.js +1 -1
  8. package/src/build/{version_mappings_injection.js → mappings_injection.js} +62 -21
  9. package/src/build/start_build_server.js +46 -36
  10. package/src/dev/start_dev_server.js +246 -248
  11. package/src/helpers/watch_source_files.js +50 -36
  12. package/src/kitchen/fetched_content_compliance.js +4 -2
  13. package/src/kitchen/kitchen.js +31 -24
  14. package/src/kitchen/url_graph/references.js +10 -2
  15. package/src/kitchen/url_graph/url_graph.js +3 -0
  16. package/src/kitchen/url_graph/url_graph_visitor.js +3 -0
  17. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +29 -16
  18. package/src/plugins/html_syntax_error_fallback/jsenv_plugin_html_syntax_error_fallback.js +1 -1
  19. package/src/plugins/plugin_controller.js +194 -200
  20. package/src/plugins/plugins.js +5 -0
  21. package/src/plugins/protocol_file/client/directory_listing.jsx +5 -0
  22. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +92 -67
  23. package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +17 -7
  24. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +6 -0
  25. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +33 -3
  26. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +15 -22
  27. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +53 -2
  28. package/src/plugins/resolution_node_esm/jsenv_plugin_node_esm_resolution.js +37 -30
  29. package/src/plugins/resolution_node_esm/node_esm_resolver.js +4 -8
  30. package/src/plugins/resolution_web/jsenv_plugin_web_resolution.js +8 -6
  31. package/src/plugins/server_events/client/server_events_client.js +2 -2
  32. package/src/plugins/server_events/jsenv_plugin_server_events.js +18 -16
  33. package/dist/js/ws.js +0 -6863
  34. package/src/helpers/lookup_package_directory.js +0 -9
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  assertAndNormalizeDirectoryUrl,
3
3
  bufferToEtag,
4
+ lookupPackageDirectory,
4
5
  } from "@jsenv/filesystem";
5
6
  import { createLogger, createTaskLog } from "@jsenv/humanize";
6
7
  import {
@@ -17,11 +18,14 @@ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
17
18
  import { existsSync, readFileSync } from "node:fs";
18
19
  import { defaultRuntimeCompat } from "../build/build.js";
19
20
  import { createEventEmitter } from "../helpers/event_emitter.js";
20
- import { lookupPackageDirectory } from "../helpers/lookup_package_directory.js";
21
21
  import { watchSourceFiles } from "../helpers/watch_source_files.js";
22
22
  import { WEB_URL_CONVERTER } from "../helpers/web_url_converter.js";
23
23
  import { jsenvCoreDirectoryUrl } from "../jsenv_core_directory_url.js";
24
24
  import { createKitchen } from "../kitchen/kitchen.js";
25
+ import {
26
+ createPluginController,
27
+ createPluginStore,
28
+ } from "../plugins/plugin_controller.js";
25
29
  import { getCorePlugins } from "../plugins/plugins.js";
26
30
  import { jsenvPluginServerEvents } from "../plugins/server_events/jsenv_plugin_server_events.js";
27
31
  import { parseUserAgentHeader } from "./user_agent.js";
@@ -56,7 +60,7 @@ export const startDevServer = async ({
56
60
  keepProcessAlive = true,
57
61
  onStop = () => {},
58
62
 
59
- sourceFilesConfig,
63
+ sourceFilesConfig = {},
60
64
  clientAutoreload = true,
61
65
 
62
66
  // runtimeCompat is the runtimeCompat for the build
@@ -154,27 +158,26 @@ export const startDevServer = async ({
154
158
  // x-server-inspect service
155
159
  {
156
160
  finalServices.push({
157
- handleRequest: (request) => {
158
- if (request.headers["x-server-inspect"]) {
159
- return { status: 200 };
160
- }
161
- if (request.pathname === "/__params__.json") {
162
- const json = JSON.stringify({
163
- sourceDirectoryUrl,
164
- });
165
- return {
166
- status: 200,
167
- headers: {
168
- "content-type": "application/json",
169
- "content-length": Buffer.byteLength(json),
170
- },
171
- body: json,
172
- };
173
- }
174
- return null;
175
- },
176
- injectResponseHeaders: () => {
177
- return { server: "jsenv_dev_server/1" };
161
+ name: "jsenv:server_header",
162
+ routes: [
163
+ {
164
+ endpoint: "GET /.internal/server.json",
165
+ description: "Get information about jsenv dev server",
166
+ availableMediaTypes: ["application/json"],
167
+ declarationSource: import.meta.url,
168
+ fetch: () =>
169
+ Response.json({
170
+ server: "jsenv_dev_server/1",
171
+ sourceDirectoryUrl,
172
+ }),
173
+ },
174
+ ],
175
+ injectResponseProperties: () => {
176
+ return {
177
+ headers: {
178
+ server: "jsenv_dev_server/1",
179
+ },
180
+ };
178
181
  },
179
182
  });
180
183
  }
@@ -222,6 +225,29 @@ export const startDevServer = async ({
222
225
  );
223
226
  serverStopCallbackSet.add(stopWatchingSourceFiles);
224
227
 
228
+ const devServerPluginStore = createPluginStore([
229
+ jsenvPluginServerEvents({ clientAutoreload }),
230
+ ...plugins,
231
+ ...getCorePlugins({
232
+ rootDirectoryUrl: sourceDirectoryUrl,
233
+ mainFilePath: sourceMainFilePath,
234
+ runtimeCompat,
235
+ sourceFilesConfig,
236
+
237
+ referenceAnalysis,
238
+ nodeEsmResolution,
239
+ magicExtensions,
240
+ magicDirectoryIndex,
241
+ directoryListing,
242
+ supervisor,
243
+ injections,
244
+ transpilation,
245
+
246
+ clientAutoreload,
247
+ cacheControl,
248
+ ribbon,
249
+ }),
250
+ ]);
225
251
  const getOrCreateKitchen = (request) => {
226
252
  const { runtimeName, runtimeVersion } = parseUserAgentHeader(
227
253
  request.headers["user-agent"] || "",
@@ -258,27 +284,6 @@ export const startDevServer = async ({
258
284
  dev: true,
259
285
  runtimeCompat,
260
286
  clientRuntimeCompat,
261
- plugins: [
262
- jsenvPluginServerEvents({ clientAutoreload }),
263
- ...plugins,
264
- ...getCorePlugins({
265
- rootDirectoryUrl: sourceDirectoryUrl,
266
- runtimeCompat,
267
-
268
- referenceAnalysis,
269
- nodeEsmResolution,
270
- magicExtensions,
271
- magicDirectoryIndex,
272
- directoryListing,
273
- supervisor,
274
- injections,
275
- transpilation,
276
-
277
- clientAutoreload,
278
- cacheControl,
279
- ribbon,
280
- }),
281
- ],
282
287
  supervisor,
283
288
  minification: false,
284
289
  sourcemaps,
@@ -365,9 +370,14 @@ export const startDevServer = async ({
365
370
  );
366
371
  },
367
372
  );
373
+ const devServerPluginController = createPluginController(
374
+ devServerPluginStore,
375
+ kitchen,
376
+ );
377
+ kitchen.setPluginController(devServerPluginController);
368
378
 
369
379
  serverStopCallbackSet.add(() => {
370
- kitchen.pluginController.callHooks("destroy", kitchen.context);
380
+ devServerPluginController.callHooks("destroy", kitchen.context);
371
381
  });
372
382
  kitchenCache.set(runtimeId, kitchen);
373
383
  onKitchenCreated(kitchen);
@@ -375,222 +385,210 @@ export const startDevServer = async ({
375
385
  };
376
386
 
377
387
  finalServices.push({
378
- name: "jsenv:omega_file_service",
379
- handleRequest: async (request) => {
388
+ name: "jsenv:dev_server_routes",
389
+ augmentRouteFetchSecondArg: (request) => {
380
390
  const kitchen = getOrCreateKitchen(request);
381
- const serveHookInfo = {
382
- ...kitchen.context,
383
- request,
384
- };
385
- const responseFromPlugin =
386
- await kitchen.pluginController.callAsyncHooksUntil(
387
- "serve",
388
- serveHookInfo,
389
- );
390
- if (responseFromPlugin) {
391
- return responseFromPlugin;
392
- }
393
- const { rootDirectoryUrl, mainFilePath } = kitchen.context;
394
- let requestResource = request.resource;
395
- let requestedUrl;
396
- if (requestResource.startsWith("/@fs/")) {
397
- const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
398
- requestedUrl = `file:///${fsRootRelativeUrl}`;
399
- } else {
400
- const requestedUrlObject = new URL(
401
- requestResource === "/" ? mainFilePath : requestResource.slice(1),
402
- rootDirectoryUrl,
403
- );
404
- requestedUrlObject.searchParams.delete("hot");
405
- requestedUrl = requestedUrlObject.href;
406
- }
407
- const { referer } = request.headers;
408
- const parentUrl = referer
409
- ? WEB_URL_CONVERTER.asFileUrl(referer, {
410
- origin: request.origin,
411
- rootDirectoryUrl: sourceDirectoryUrl,
412
- })
413
- : sourceDirectoryUrl;
414
- let reference = kitchen.graph.inferReference(
415
- request.resource,
416
- parentUrl,
417
- );
418
- if (reference) {
419
- reference.urlInfo.context.request = request;
420
- reference.urlInfo.context.requestedUrl = requestedUrl;
421
- } else {
422
- const rootUrlInfo = kitchen.graph.rootUrlInfo;
423
- rootUrlInfo.context.request = request;
424
- rootUrlInfo.context.requestedUrl = requestedUrl;
425
- reference = rootUrlInfo.dependencies.createResolveAndFinalize({
426
- trace: { message: parentUrl },
427
- type: "http_request",
428
- specifier: request.resource,
429
- });
430
- rootUrlInfo.context.request = null;
431
- rootUrlInfo.context.requestedUrl = null;
432
- }
433
- const urlInfo = reference.urlInfo;
434
- const ifNoneMatch = request.headers["if-none-match"];
435
- const urlInfoTargetedByCache = urlInfo.findParentIfInline() || urlInfo;
436
-
437
- try {
438
- if (!urlInfo.error && ifNoneMatch) {
439
- const [clientOriginalContentEtag, clientContentEtag] =
440
- ifNoneMatch.split("_");
441
- if (
442
- urlInfoTargetedByCache.originalContentEtag ===
443
- clientOriginalContentEtag &&
444
- urlInfoTargetedByCache.contentEtag === clientContentEtag &&
445
- urlInfoTargetedByCache.isValid()
446
- ) {
447
- const headers = {
448
- "cache-control": `private,max-age=0,must-revalidate`,
449
- };
450
- Object.keys(urlInfo.headers).forEach((key) => {
451
- if (key !== "content-length") {
452
- headers[key] = urlInfo.headers[key];
453
- }
391
+ return { kitchen };
392
+ },
393
+ routes: [
394
+ ...devServerPluginStore.allDevServerRoutes,
395
+ {
396
+ endpoint: "GET *",
397
+ description: "Serve project files.",
398
+ declarationSource: import.meta.url,
399
+ fetch: async (request, { kitchen }) => {
400
+ const { rootDirectoryUrl, mainFilePath } = kitchen.context;
401
+ let requestResource = request.resource;
402
+ let requestedUrl;
403
+ if (requestResource.startsWith("/@fs/")) {
404
+ const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
405
+ requestedUrl = `file:///${fsRootRelativeUrl}`;
406
+ } else {
407
+ const requestedUrlObject = new URL(
408
+ requestResource === "/"
409
+ ? mainFilePath
410
+ : requestResource.slice(1),
411
+ rootDirectoryUrl,
412
+ );
413
+ requestedUrlObject.searchParams.delete("hot");
414
+ requestedUrl = requestedUrlObject.href;
415
+ }
416
+ const { referer } = request.headers;
417
+ const parentUrl = referer
418
+ ? WEB_URL_CONVERTER.asFileUrl(referer, {
419
+ origin: request.origin,
420
+ rootDirectoryUrl: sourceDirectoryUrl,
421
+ })
422
+ : sourceDirectoryUrl;
423
+ let reference = kitchen.graph.inferReference(
424
+ request.resource,
425
+ parentUrl,
426
+ );
427
+ if (reference) {
428
+ reference.urlInfo.context.request = request;
429
+ reference.urlInfo.context.requestedUrl = requestedUrl;
430
+ } else {
431
+ const rootUrlInfo = kitchen.graph.rootUrlInfo;
432
+ rootUrlInfo.context.request = request;
433
+ rootUrlInfo.context.requestedUrl = requestedUrl;
434
+ reference = rootUrlInfo.dependencies.createResolveAndFinalize({
435
+ trace: { message: parentUrl },
436
+ type: "http_request",
437
+ specifier: request.resource,
454
438
  });
455
- return {
456
- status: 304,
457
- headers,
458
- };
439
+ rootUrlInfo.context.request = null;
440
+ rootUrlInfo.context.requestedUrl = null;
459
441
  }
460
- }
461
- await urlInfo.cook({ request, reference });
462
- let { response } = urlInfo;
463
- if (response) {
464
- return response;
465
- }
466
- response = {
467
- url: reference.url,
468
- status: 200,
469
- headers: {
470
- // when we send eTag to the client the next request to the server
471
- // will send etag in request headers.
472
- // If they match jsenv bypass cooking and returns 304
473
- // This must not happen when a plugin uses "no-store" or "no-cache" as it means
474
- // plugin logic wants to happens for every request to this url
475
- ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
476
- ? {
477
- "cache-control": "no-store", // for inline file we force no-store when parent is no-store
478
- }
479
- : {
442
+ const urlInfo = reference.urlInfo;
443
+ const ifNoneMatch = request.headers["if-none-match"];
444
+ const urlInfoTargetedByCache =
445
+ urlInfo.findParentIfInline() || urlInfo;
446
+
447
+ try {
448
+ if (!urlInfo.error && ifNoneMatch) {
449
+ const [clientOriginalContentEtag, clientContentEtag] =
450
+ ifNoneMatch.split("_");
451
+ if (
452
+ urlInfoTargetedByCache.originalContentEtag ===
453
+ clientOriginalContentEtag &&
454
+ urlInfoTargetedByCache.contentEtag === clientContentEtag &&
455
+ urlInfoTargetedByCache.isValid()
456
+ ) {
457
+ const headers = {
480
458
  "cache-control": `private,max-age=0,must-revalidate`,
481
- // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
482
- "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
483
- }),
484
- ...urlInfo.headers,
485
- "content-type": urlInfo.contentType,
486
- "content-length": urlInfo.contentLength,
487
- },
488
- body: urlInfo.content,
489
- timing: urlInfo.timing,
490
- };
491
- const augmentResponseInfo = {
492
- ...kitchen.context,
493
- reference,
494
- urlInfo,
495
- };
496
- kitchen.pluginController.callHooks(
497
- "augmentResponse",
498
- augmentResponseInfo,
499
- (returnValue) => {
500
- response = composeTwoResponses(response, returnValue);
501
- },
502
- );
503
- return response;
504
- } catch (error) {
505
- const originalError = error ? error.cause || error : error;
506
- if (originalError.asResponse) {
507
- return originalError.asResponse();
508
- }
509
- const code = originalError.code;
510
- if (code === "PARSE_ERROR") {
511
- // when possible let browser re-throw the syntax error
512
- // it's not possible to do that when url info content is not available
513
- // (happens for js_module_fallback for instance)
514
- if (urlInfo.content !== undefined) {
515
- kitchen.context.logger.error(`Error while handling ${request.url}:
516
- ${originalError.reasonCode || originalError.code}
517
- ${error.trace?.message}`);
518
- return {
459
+ };
460
+ Object.keys(urlInfo.headers).forEach((key) => {
461
+ if (key !== "content-length") {
462
+ headers[key] = urlInfo.headers[key];
463
+ }
464
+ });
465
+ return {
466
+ status: 304,
467
+ headers,
468
+ };
469
+ }
470
+ }
471
+ await urlInfo.cook({ request, reference });
472
+ let { response } = urlInfo;
473
+ if (response) {
474
+ return response;
475
+ }
476
+ response = {
519
477
  url: reference.url,
520
478
  status: 200,
521
- // reason becomes the http response statusText, it must not contain invalid chars
522
- // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
523
- statusText: error.reason,
524
- statusMessage: originalError.message,
525
479
  headers: {
480
+ // when we send eTag to the client the next request to the server
481
+ // will send etag in request headers.
482
+ // If they match jsenv bypass cooking and returns 304
483
+ // This must not happen when a plugin uses "no-store" or "no-cache" as it means
484
+ // plugin logic wants to happens for every request to this url
485
+ ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
486
+ ? {
487
+ "cache-control": "no-store", // for inline file we force no-store when parent is no-store
488
+ }
489
+ : {
490
+ "cache-control": `private,max-age=0,must-revalidate`,
491
+ // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
492
+ "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
493
+ }),
494
+ ...urlInfo.headers,
526
495
  "content-type": urlInfo.contentType,
527
496
  "content-length": urlInfo.contentLength,
528
- "cache-control": "no-store",
529
497
  },
530
498
  body: urlInfo.content,
499
+ timing: urlInfo.timing, // TODO: use something else
500
+ };
501
+ const augmentResponseInfo = {
502
+ ...kitchen.context,
503
+ reference,
504
+ urlInfo,
505
+ };
506
+ kitchen.pluginController.callHooks(
507
+ "augmentResponse",
508
+ augmentResponseInfo,
509
+ (returnValue) => {
510
+ response = composeTwoResponses(response, returnValue);
511
+ },
512
+ );
513
+ return response;
514
+ } catch (error) {
515
+ const originalError = error ? error.cause || error : error;
516
+ if (originalError.asResponse) {
517
+ return originalError.asResponse();
518
+ }
519
+ const code = originalError.code;
520
+ if (code === "PARSE_ERROR") {
521
+ // when possible let browser re-throw the syntax error
522
+ // it's not possible to do that when url info content is not available
523
+ // (happens for js_module_fallback for instance)
524
+ if (urlInfo.content !== undefined) {
525
+ kitchen.context.logger
526
+ .error(`Error while handling ${request.url}:
527
+ ${originalError.reasonCode || originalError.code}
528
+ ${error.trace?.message}`);
529
+ return {
530
+ url: reference.url,
531
+ status: 200,
532
+ // reason becomes the http response statusText, it must not contain invalid chars
533
+ // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
534
+ statusText: error.reason,
535
+ statusMessage: originalError.message,
536
+ headers: {
537
+ "content-type": urlInfo.contentType,
538
+ "content-length": urlInfo.contentLength,
539
+ "cache-control": "no-store",
540
+ },
541
+ body: urlInfo.content,
542
+ };
543
+ }
544
+ return {
545
+ url: reference.url,
546
+ status: 500,
547
+ statusText: error.reason,
548
+ statusMessage: originalError.message,
549
+ headers: {
550
+ "cache-control": "no-store",
551
+ },
552
+ body: urlInfo.content,
553
+ };
554
+ }
555
+ if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
556
+ return serveDirectory(reference.url, {
557
+ headers: {
558
+ accept: "text/html",
559
+ },
560
+ canReadDirectory: true,
561
+ rootDirectoryUrl: sourceDirectoryUrl,
562
+ });
563
+ }
564
+ if (code === "NOT_ALLOWED") {
565
+ return {
566
+ url: reference.url,
567
+ status: 403,
568
+ statusText: originalError.reason,
569
+ };
570
+ }
571
+ if (code === "NOT_FOUND") {
572
+ return {
573
+ url: reference.url,
574
+ status: 404,
575
+ statusText: originalError.reason,
576
+ statusMessage: originalError.message,
577
+ };
578
+ }
579
+ return {
580
+ url: reference.url,
581
+ status: 500,
582
+ statusText: error.reason,
583
+ statusMessage: error.stack,
584
+ headers: {
585
+ "cache-control": "no-store",
586
+ },
531
587
  };
532
588
  }
533
- return {
534
- url: reference.url,
535
- status: 500,
536
- statusText: error.reason,
537
- statusMessage: originalError.message,
538
- headers: {
539
- "cache-control": "no-store",
540
- },
541
- body: urlInfo.content,
542
- };
543
- }
544
- if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
545
- return serveDirectory(reference.url, {
546
- headers: {
547
- accept: "text/html",
548
- },
549
- canReadDirectory: true,
550
- rootDirectoryUrl: sourceDirectoryUrl,
551
- });
552
- }
553
- if (code === "NOT_ALLOWED") {
554
- return {
555
- url: reference.url,
556
- status: 403,
557
- statusText: originalError.reason,
558
- };
559
- }
560
- if (code === "NOT_FOUND") {
561
- return {
562
- url: reference.url,
563
- status: 404,
564
- statusText: originalError.reason,
565
- statusMessage: originalError.message,
566
- };
567
- }
568
- return {
569
- url: reference.url,
570
- status: 500,
571
- statusText: error.reason,
572
- statusMessage: error.stack,
573
- headers: {
574
- "cache-control": "no-store",
575
- },
576
- };
577
- }
578
- },
579
- handleWebsocket: async (websocket, { request }) => {
580
- // if (true || logLevel === "debug") {
581
- // console.log("handleWebsocket", websocket, request.headers);
582
- // }
583
- const kitchen = getOrCreateKitchen(request);
584
- const serveWebsocketHookInfo = {
585
- request,
586
- websocket,
587
- context: kitchen.context,
588
- };
589
- await kitchen.pluginController.callAsyncHooksUntil(
590
- "serveWebsocket",
591
- serveWebsocketHookInfo,
592
- );
593
- },
589
+ },
590
+ },
591
+ ],
594
592
  });
595
593
  }
596
594
  // jsenv error handler service