@jsenv/core 40.0.7 → 40.0.9

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.
@@ -1,840 +0,0 @@
1
- import { moveUrl, urlIsInsideOf, ensureWindowsDriveLetter, memoizeByFirstArgument, assertAndNormalizeDirectoryUrl, urlToRelativeUrl, lookupPackageDirectory, createLogger, createTaskLog, URL_META, bufferToEtag } from "../jsenv_core_packages.js";
2
- import { ServerEvents, jsenvServiceCORS, jsenvAccessControlAllowedHeaders, composeTwoResponses, serveDirectory, jsenvServiceErrorHandler, startServer } from "@jsenv/server";
3
- import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { createEventEmitter, watchSourceFiles, createPluginStore, getCorePlugins, defaultRuntimeCompat, createKitchen, createPluginController } from "../plugins.js";
6
- import { parseHtml, injectJsenvScript, stringifyHtmlAst } from "@jsenv/ast";
7
- import { createRequire } from "node:module";
8
- import "node:path";
9
- import "node:crypto";
10
- import "@jsenv/sourcemap";
11
- import "node:url";
12
- import "@jsenv/js-module-fallback";
13
- import "node:process";
14
- import "node:os";
15
- import "node:tty";
16
- import "string-width";
17
- import "@jsenv/runtime-compat";
18
- import "node:perf_hooks";
19
- import "@jsenv/plugin-supervisor";
20
- import "../main.js";
21
-
22
- const WEB_URL_CONVERTER = {
23
- asWebUrl: (fileUrl, webServer) => {
24
- if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
25
- return moveUrl({
26
- url: fileUrl,
27
- from: webServer.rootDirectoryUrl,
28
- to: `${webServer.origin}/`,
29
- });
30
- }
31
- const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl);
32
- return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`;
33
- },
34
- asFileUrl: (webUrl, webServer) => {
35
- const { pathname, search } = new URL(webUrl);
36
- if (pathname.startsWith("/@fs/")) {
37
- const fsRootRelativeUrl = pathname.slice("/@fs/".length);
38
- return `file:///${fsRootRelativeUrl}${search}`;
39
- }
40
- return moveUrl({
41
- url: webUrl,
42
- from: `${webServer.origin}/`,
43
- to: webServer.rootDirectoryUrl,
44
- });
45
- },
46
- };
47
-
48
- /*
49
- * This plugin is very special because it is here
50
- * to provide "serverEvents" used by other plugins
51
- */
52
-
53
-
54
- const serverEventsClientFileUrl = new URL(
55
- "../client/server_events_client/server_events_client.js",
56
- import.meta.url,
57
- ).href;
58
-
59
- const jsenvPluginServerEvents = ({ clientAutoreload }) => {
60
- let serverEvents = new ServerEvents({
61
- actionOnClientLimitReached: "kick-oldest",
62
- });
63
- const { clientServerEventsConfig } = clientAutoreload;
64
- const { logs = true } = clientServerEventsConfig;
65
-
66
- return {
67
- name: "jsenv:server_events",
68
- appliesDuring: "dev",
69
- effect: ({ kitchenContext, otherPlugins }) => {
70
- const allServerEvents = {};
71
- for (const otherPlugin of otherPlugins) {
72
- const { serverEvents } = otherPlugin;
73
- if (!serverEvents) {
74
- continue;
75
- }
76
- for (const serverEventName of Object.keys(serverEvents)) {
77
- // we could throw on serverEvent name conflict
78
- // we could throw if serverEvents[serverEventName] is not a function
79
- allServerEvents[serverEventName] = serverEvents[serverEventName];
80
- }
81
- }
82
- const serverEventNames = Object.keys(allServerEvents);
83
- if (serverEventNames.length === 0) {
84
- return false;
85
- }
86
-
87
- const onabort = () => {
88
- serverEvents.close();
89
- };
90
- kitchenContext.signal.addEventListener("abort", onabort);
91
- for (const serverEventName of Object.keys(allServerEvents)) {
92
- const serverEventInfo = {
93
- ...kitchenContext,
94
- // serverEventsDispatcher variable is safe, we can disable esling warning
95
- // eslint-disable-next-line no-loop-func
96
- sendServerEvent: (data) => {
97
- if (!serverEvents) {
98
- // this can happen if a plugin wants to send a server event but
99
- // server is closing or the plugin got destroyed but still wants to do things
100
- // if plugin code is correctly written it is never supposed to happen
101
- // because it means a plugin is still trying to do stuff after being destroyed
102
- return;
103
- }
104
- serverEvents.sendEventToAllClients({
105
- type: serverEventName,
106
- data,
107
- });
108
- },
109
- };
110
- const serverEventInit = allServerEvents[serverEventName];
111
- serverEventInit(serverEventInfo);
112
- }
113
- return () => {
114
- kitchenContext.signal.removeEventListener("abort", onabort);
115
- serverEvents.close();
116
- serverEvents = undefined;
117
- };
118
- },
119
- transformUrlContent: {
120
- html: (urlInfo) => {
121
- const htmlAst = parseHtml({
122
- html: urlInfo.content,
123
- url: urlInfo.url,
124
- });
125
- injectJsenvScript(htmlAst, {
126
- src: serverEventsClientFileUrl,
127
- initCall: {
128
- callee: "window.__server_events__.setup",
129
- params: {
130
- logs,
131
- },
132
- },
133
- pluginName: "jsenv:server_events",
134
- });
135
- return stringifyHtmlAst(htmlAst);
136
- },
137
- },
138
- devServerRoutes: [
139
- {
140
- endpoint: "GET /.internal/events.websocket",
141
- description: `Jsenv dev server emit server events on this endpoint. When a file is saved the "reload" event is sent here.`,
142
- fetch: serverEvents.fetch,
143
- declarationSource: import.meta.url,
144
- },
145
- ],
146
- };
147
- };
148
-
149
- const requireFromJsenv = createRequire(import.meta.url);
150
-
151
- const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
152
- if (userAgent.includes("node-fetch/")) {
153
- // it's not really node and conceptually we can't assume the node version
154
- // but good enough for now
155
- return {
156
- runtimeName: "node",
157
- runtimeVersion: process.version.slice(1),
158
- };
159
- }
160
- const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
161
- const { ua } = new UA(userAgent);
162
- const { family, major, minor, patch } = ua;
163
- return {
164
- runtimeName: family.toLowerCase(),
165
- runtimeVersion:
166
- family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
167
- };
168
- });
169
-
170
- const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
171
-
172
- /**
173
- * Starts the development server.
174
- *
175
- * @param {Object} [params={}] - Configuration params for the dev server.
176
- * @param {number} [params.port=3456] - Port number the server should listen on.
177
- * @param {string} [params.hostname="localhost"] - Hostname to bind the server to.
178
- * @param {boolean} [params.https=false] - Whether to use HTTPS.
179
- *
180
- * @returns {Promise<Object>} A promise that resolves to the server instance.
181
- * @throws {Error} Will throw an error if the server fails to start or is called with unexpected params.
182
- *
183
- * @example
184
- * // Start a basic dev server
185
- * const server = await startDevServer();
186
- * console.log(`Server started at ${server.origin}`);
187
- *
188
- * @example
189
- * // Start a server with custom params
190
- * const server = await startDevServer({
191
- * port: 8080,
192
- * });
193
- */
194
- const startDevServer = async ({
195
- sourceDirectoryUrl,
196
- sourceMainFilePath = "./index.html",
197
- ignore,
198
- port = 3456,
199
- hostname,
200
- acceptAnyIp,
201
- https,
202
- // it's better to use http1 by default because it allows to get statusText in devtools
203
- // which gives valuable information when there is errors
204
- http2 = false,
205
- logLevel = EXECUTED_BY_TEST_PLAN ? "warn" : "info",
206
- serverLogLevel = "warn",
207
- services = [],
208
-
209
- signal = new AbortController().signal,
210
- handleSIGINT = true,
211
- keepProcessAlive = true,
212
- onStop = () => {},
213
-
214
- sourceFilesConfig = {},
215
- clientAutoreload = true,
216
-
217
- // runtimeCompat is the runtimeCompat for the build
218
- // when specified, dev server use it to warn in case
219
- // code would be supported during dev but not after build
220
- runtimeCompat = defaultRuntimeCompat,
221
- plugins = [],
222
- referenceAnalysis = {},
223
- nodeEsmResolution,
224
- supervisor = true,
225
- magicExtensions,
226
- magicDirectoryIndex,
227
- directoryListing,
228
- injections,
229
- transpilation,
230
- cacheControl = true,
231
- ribbon = true,
232
- // toolbar = false,
233
- onKitchenCreated = () => {},
234
-
235
- sourcemaps = "inline",
236
- sourcemapsSourcesContent,
237
- outDirectoryUrl,
238
- ...rest
239
- }) => {
240
- // params type checking
241
- {
242
- const unexpectedParamNames = Object.keys(rest);
243
- if (unexpectedParamNames.length > 0) {
244
- throw new TypeError(
245
- `${unexpectedParamNames.join(",")}: there is no such param`,
246
- );
247
- }
248
- sourceDirectoryUrl = assertAndNormalizeDirectoryUrl(
249
- sourceDirectoryUrl,
250
- "sourceDirectoryUrl",
251
- );
252
- if (!existsSync(new URL(sourceDirectoryUrl))) {
253
- throw new Error(`ENOENT on sourceDirectoryUrl at ${sourceDirectoryUrl}`);
254
- }
255
- if (typeof sourceMainFilePath !== "string") {
256
- throw new TypeError(
257
- `sourceMainFilePath must be a string, got ${sourceMainFilePath}`,
258
- );
259
- }
260
- sourceMainFilePath = urlToRelativeUrl(
261
- new URL(sourceMainFilePath, sourceDirectoryUrl),
262
- sourceDirectoryUrl,
263
- );
264
- if (outDirectoryUrl === undefined) {
265
- if (
266
- process.env.CAPTURING_SIDE_EFFECTS ||
267
- (false)
268
- ) {
269
- outDirectoryUrl = new URL("../.jsenv/", sourceDirectoryUrl);
270
- } else {
271
- const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
272
- if (packageDirectoryUrl) {
273
- outDirectoryUrl = `${packageDirectoryUrl}.jsenv/`;
274
- }
275
- }
276
- } else if (outDirectoryUrl !== null && outDirectoryUrl !== false) {
277
- outDirectoryUrl = assertAndNormalizeDirectoryUrl(
278
- outDirectoryUrl,
279
- "outDirectoryUrl",
280
- );
281
- }
282
- }
283
-
284
- // params normalization
285
- {
286
- if (clientAutoreload === true) {
287
- clientAutoreload = {};
288
- }
289
- if (clientAutoreload === false) {
290
- clientAutoreload = { enabled: false };
291
- }
292
- }
293
-
294
- const logger = createLogger({ logLevel });
295
- const startDevServerTask = createTaskLog("start dev server", {
296
- disabled: !logger.levels.info,
297
- });
298
-
299
- const serverStopCallbackSet = new Set();
300
- const serverStopAbortController = new AbortController();
301
- serverStopCallbackSet.add(() => {
302
- serverStopAbortController.abort();
303
- });
304
- const serverStopAbortSignal = serverStopAbortController.signal;
305
- const kitchenCache = new Map();
306
-
307
- const finalServices = [];
308
- // x-server-inspect service
309
- {
310
- finalServices.push({
311
- name: "jsenv:server_header",
312
- routes: [
313
- {
314
- endpoint: "GET /.internal/server.json",
315
- description: "Get information about jsenv dev server",
316
- availableMediaTypes: ["application/json"],
317
- declarationSource: import.meta.url,
318
- fetch: () =>
319
- Response.json({
320
- server: "jsenv_dev_server/1",
321
- sourceDirectoryUrl,
322
- }),
323
- },
324
- ],
325
- injectResponseProperties: () => {
326
- return {
327
- headers: {
328
- server: "jsenv_dev_server/1",
329
- },
330
- };
331
- },
332
- });
333
- }
334
- // cors service
335
- {
336
- finalServices.push(
337
- jsenvServiceCORS({
338
- accessControlAllowRequestOrigin: true,
339
- accessControlAllowRequestMethod: true,
340
- accessControlAllowRequestHeaders: true,
341
- accessControlAllowedRequestHeaders: [
342
- ...jsenvAccessControlAllowedHeaders,
343
- "x-jsenv-execution-id",
344
- ],
345
- accessControlAllowCredentials: true,
346
- timingAllowOrigin: true,
347
- }),
348
- );
349
- }
350
- // custom services
351
- {
352
- finalServices.push(...services);
353
- }
354
- // file_service
355
- {
356
- const clientFileChangeEventEmitter = createEventEmitter();
357
- const clientFileDereferencedEventEmitter = createEventEmitter();
358
- clientAutoreload = {
359
- enabled: true,
360
- clientServerEventsConfig: {},
361
- clientFileChangeEventEmitter,
362
- clientFileDereferencedEventEmitter,
363
- ...clientAutoreload,
364
- };
365
- const stopWatchingSourceFiles = watchSourceFiles(
366
- sourceDirectoryUrl,
367
- (fileInfo) => {
368
- clientFileChangeEventEmitter.emit(fileInfo);
369
- },
370
- {
371
- sourceFilesConfig,
372
- keepProcessAlive: false,
373
- cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
374
- },
375
- );
376
- serverStopCallbackSet.add(stopWatchingSourceFiles);
377
-
378
- const devServerPluginStore = createPluginStore([
379
- jsenvPluginServerEvents({ clientAutoreload }),
380
- ...plugins,
381
- ...getCorePlugins({
382
- rootDirectoryUrl: sourceDirectoryUrl,
383
- mainFilePath: sourceMainFilePath,
384
- runtimeCompat,
385
- sourceFilesConfig,
386
-
387
- referenceAnalysis,
388
- nodeEsmResolution,
389
- magicExtensions,
390
- magicDirectoryIndex,
391
- directoryListing,
392
- supervisor,
393
- injections,
394
- transpilation,
395
-
396
- clientAutoreload,
397
- cacheControl,
398
- ribbon,
399
- }),
400
- ]);
401
- const getOrCreateKitchen = (request) => {
402
- const { runtimeName, runtimeVersion } = parseUserAgentHeader(
403
- request.headers["user-agent"] || "",
404
- );
405
- const runtimeId = `${runtimeName}@${runtimeVersion}`;
406
- const existing = kitchenCache.get(runtimeId);
407
- if (existing) {
408
- return existing;
409
- }
410
- const watchAssociations = URL_META.resolveAssociations(
411
- { watch: stopWatchingSourceFiles.watchPatterns },
412
- sourceDirectoryUrl,
413
- );
414
- let kitchen;
415
- clientFileChangeEventEmitter.on(({ url, event }) => {
416
- const urlInfo = kitchen.graph.getUrlInfo(url);
417
- if (urlInfo) {
418
- if (event === "removed") {
419
- urlInfo.onRemoved();
420
- } else {
421
- urlInfo.onModified();
422
- }
423
- }
424
- });
425
- const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
426
-
427
- kitchen = createKitchen({
428
- name: runtimeId,
429
- signal: serverStopAbortSignal,
430
- logLevel,
431
- rootDirectoryUrl: sourceDirectoryUrl,
432
- mainFilePath: sourceMainFilePath,
433
- ignore,
434
- dev: true,
435
- runtimeCompat,
436
- clientRuntimeCompat,
437
- supervisor,
438
- sourcemaps,
439
- sourcemapsSourcesContent,
440
- outDirectoryUrl: outDirectoryUrl
441
- ? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
442
- : undefined,
443
- });
444
- kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
445
- const { watch } = URL_META.applyAssociations({
446
- url: urlInfoCreated.url,
447
- associations: watchAssociations,
448
- });
449
- urlInfoCreated.isWatched = watch;
450
- // when an url depends on many others, we check all these (like package.json)
451
- urlInfoCreated.isValid = () => {
452
- if (!urlInfoCreated.url.startsWith("file:")) {
453
- return false;
454
- }
455
- if (urlInfoCreated.content === undefined) {
456
- // urlInfo content is undefined when:
457
- // - url info content never fetched
458
- // - it is considered as modified because undelying file is watched and got saved
459
- // - it is considered as modified because underlying file content
460
- // was compared using etag and it has changed
461
- return false;
462
- }
463
- if (!watch) {
464
- // file is not watched, check the filesystem
465
- let fileContentAsBuffer;
466
- try {
467
- fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
468
- } catch (e) {
469
- if (e.code === "ENOENT") {
470
- urlInfoCreated.onModified();
471
- return false;
472
- }
473
- return false;
474
- }
475
- const fileContentEtag = bufferToEtag(fileContentAsBuffer);
476
- if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
477
- urlInfoCreated.onModified();
478
- // restore content to be able to compare it again later
479
- urlInfoCreated.kitchen.urlInfoTransformer.setContent(
480
- urlInfoCreated,
481
- String(fileContentAsBuffer),
482
- {
483
- contentEtag: fileContentEtag,
484
- },
485
- );
486
- return false;
487
- }
488
- }
489
- for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
490
- const implicitUrlInfo =
491
- urlInfoCreated.graph.getUrlInfo(implicitUrl);
492
- if (!implicitUrlInfo) {
493
- continue;
494
- }
495
- if (implicitUrlInfo.content === undefined) {
496
- // happens when we explicitely load an url with a search param
497
- // - it creates an implicit url info to the url without params
498
- // - we never explicitely request the url without search param so it has no content
499
- // in that case the underlying urlInfo cannot be invalidate by the implicit
500
- // we use modifiedTimestamp to detect if the url was loaded once
501
- // or is just here to be used later
502
- if (implicitUrlInfo.modifiedTimestamp) {
503
- return false;
504
- }
505
- continue;
506
- }
507
- if (!implicitUrlInfo.isValid()) {
508
- return false;
509
- }
510
- }
511
- return true;
512
- };
513
- });
514
- kitchen.graph.urlInfoDereferencedEventEmitter.on(
515
- (urlInfoDereferenced, lastReferenceFromOther) => {
516
- clientFileDereferencedEventEmitter.emit(
517
- urlInfoDereferenced,
518
- lastReferenceFromOther,
519
- );
520
- },
521
- );
522
- const devServerPluginController = createPluginController(
523
- devServerPluginStore,
524
- kitchen,
525
- );
526
- kitchen.setPluginController(devServerPluginController);
527
-
528
- serverStopCallbackSet.add(() => {
529
- devServerPluginController.callHooks("destroy", kitchen.context);
530
- });
531
- kitchenCache.set(runtimeId, kitchen);
532
- onKitchenCreated(kitchen);
533
- return kitchen;
534
- };
535
-
536
- finalServices.push({
537
- name: "jsenv:dev_server_routes",
538
- augmentRouteFetchSecondArg: (request) => {
539
- const kitchen = getOrCreateKitchen(request);
540
- return { kitchen };
541
- },
542
- routes: [
543
- ...devServerPluginStore.allDevServerRoutes,
544
- {
545
- endpoint: "GET *",
546
- description: "Serve project files.",
547
- declarationSource: import.meta.url,
548
- fetch: async (request, { kitchen }) => {
549
- const { rootDirectoryUrl, mainFilePath } = kitchen.context;
550
- let requestResource = request.resource;
551
- let requestedUrl;
552
- if (requestResource.startsWith("/@fs/")) {
553
- const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
554
- requestedUrl = `file:///${fsRootRelativeUrl}`;
555
- } else {
556
- const requestedUrlObject = new URL(
557
- requestResource === "/"
558
- ? mainFilePath
559
- : requestResource.slice(1),
560
- rootDirectoryUrl,
561
- );
562
- requestedUrlObject.searchParams.delete("hot");
563
- requestedUrl = requestedUrlObject.href;
564
- }
565
- const { referer } = request.headers;
566
- const parentUrl = referer
567
- ? WEB_URL_CONVERTER.asFileUrl(referer, {
568
- origin: request.origin,
569
- rootDirectoryUrl: sourceDirectoryUrl,
570
- })
571
- : sourceDirectoryUrl;
572
- let reference = kitchen.graph.inferReference(
573
- request.resource,
574
- parentUrl,
575
- );
576
- if (reference) {
577
- reference.urlInfo.context.request = request;
578
- reference.urlInfo.context.requestedUrl = requestedUrl;
579
- } else {
580
- const rootUrlInfo = kitchen.graph.rootUrlInfo;
581
- rootUrlInfo.context.request = request;
582
- rootUrlInfo.context.requestedUrl = requestedUrl;
583
- reference = rootUrlInfo.dependencies.createResolveAndFinalize({
584
- trace: { message: parentUrl },
585
- type: "http_request",
586
- specifier: request.resource,
587
- });
588
- rootUrlInfo.context.request = null;
589
- rootUrlInfo.context.requestedUrl = null;
590
- }
591
- const urlInfo = reference.urlInfo;
592
- const ifNoneMatch = request.headers["if-none-match"];
593
- const urlInfoTargetedByCache =
594
- urlInfo.findParentIfInline() || urlInfo;
595
-
596
- try {
597
- if (!urlInfo.error && ifNoneMatch) {
598
- const [clientOriginalContentEtag, clientContentEtag] =
599
- ifNoneMatch.split("_");
600
- if (
601
- urlInfoTargetedByCache.originalContentEtag ===
602
- clientOriginalContentEtag &&
603
- urlInfoTargetedByCache.contentEtag === clientContentEtag &&
604
- urlInfoTargetedByCache.isValid()
605
- ) {
606
- const headers = {
607
- "cache-control": `private,max-age=0,must-revalidate`,
608
- };
609
- Object.keys(urlInfo.headers).forEach((key) => {
610
- if (key !== "content-length") {
611
- headers[key] = urlInfo.headers[key];
612
- }
613
- });
614
- return {
615
- status: 304,
616
- headers,
617
- };
618
- }
619
- }
620
- await urlInfo.cook({ request, reference });
621
- let { response } = urlInfo;
622
- if (response) {
623
- return response;
624
- }
625
- response = {
626
- url: reference.url,
627
- status: 200,
628
- headers: {
629
- // when we send eTag to the client the next request to the server
630
- // will send etag in request headers.
631
- // If they match jsenv bypass cooking and returns 304
632
- // This must not happen when a plugin uses "no-store" or "no-cache" as it means
633
- // plugin logic wants to happens for every request to this url
634
- ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
635
- ? {
636
- "cache-control": "no-store", // for inline file we force no-store when parent is no-store
637
- }
638
- : {
639
- "cache-control": `private,max-age=0,must-revalidate`,
640
- // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
641
- "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
642
- }),
643
- ...urlInfo.headers,
644
- "content-type": urlInfo.contentType,
645
- "content-length": urlInfo.contentLength,
646
- },
647
- body: urlInfo.content,
648
- timing: urlInfo.timing, // TODO: use something else
649
- };
650
- const augmentResponseInfo = {
651
- ...kitchen.context,
652
- reference,
653
- urlInfo,
654
- };
655
- kitchen.pluginController.callHooks(
656
- "augmentResponse",
657
- augmentResponseInfo,
658
- (returnValue) => {
659
- response = composeTwoResponses(response, returnValue);
660
- },
661
- );
662
- return response;
663
- } catch (error) {
664
- const originalError = error ? error.cause || error : error;
665
- if (originalError.asResponse) {
666
- return originalError.asResponse();
667
- }
668
- const code = originalError.code;
669
- if (code === "PARSE_ERROR") {
670
- // when possible let browser re-throw the syntax error
671
- // it's not possible to do that when url info content is not available
672
- // (happens for js_module_fallback for instance)
673
- if (urlInfo.content !== undefined) {
674
- kitchen.context.logger
675
- .error(`Error while handling ${request.url}:
676
- ${originalError.reasonCode || originalError.code}
677
- ${error.trace?.message}`);
678
- return {
679
- url: reference.url,
680
- status: 200,
681
- // reason becomes the http response statusText, it must not contain invalid chars
682
- // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
683
- statusText: error.reason,
684
- statusMessage: originalError.message,
685
- headers: {
686
- "content-type": urlInfo.contentType,
687
- "content-length": urlInfo.contentLength,
688
- "cache-control": "no-store",
689
- },
690
- body: urlInfo.content,
691
- };
692
- }
693
- return {
694
- url: reference.url,
695
- status: 500,
696
- statusText: error.reason,
697
- statusMessage: originalError.message,
698
- headers: {
699
- "cache-control": "no-store",
700
- },
701
- body: urlInfo.content,
702
- };
703
- }
704
- if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
705
- return serveDirectory(reference.url, {
706
- headers: {
707
- accept: "text/html",
708
- },
709
- canReadDirectory: true,
710
- rootDirectoryUrl: sourceDirectoryUrl,
711
- });
712
- }
713
- if (code === "NOT_ALLOWED") {
714
- return {
715
- url: reference.url,
716
- status: 403,
717
- statusText: originalError.reason,
718
- };
719
- }
720
- if (code === "NOT_FOUND") {
721
- return {
722
- url: reference.url,
723
- status: 404,
724
- statusText: originalError.reason,
725
- statusMessage: originalError.message,
726
- };
727
- }
728
- return {
729
- url: reference.url,
730
- status: 500,
731
- statusText: error.reason,
732
- statusMessage: error.stack,
733
- headers: {
734
- "cache-control": "no-store",
735
- },
736
- };
737
- }
738
- },
739
- },
740
- ],
741
- });
742
- }
743
- // jsenv error handler service
744
- {
745
- finalServices.push({
746
- name: "jsenv:omega_error_handler",
747
- handleError: (error) => {
748
- const getResponseForError = () => {
749
- if (error && error.asResponse) {
750
- return error.asResponse();
751
- }
752
- if (error && error.statusText === "Unexpected directory operation") {
753
- return {
754
- status: 403,
755
- };
756
- }
757
- return convertFileSystemErrorToResponseProperties(error);
758
- };
759
- const response = getResponseForError();
760
- if (!response) {
761
- return null;
762
- }
763
- const body = JSON.stringify({
764
- status: response.status,
765
- statusText: response.statusText,
766
- headers: response.headers,
767
- body: response.body,
768
- });
769
- return {
770
- status: 200,
771
- headers: {
772
- "content-type": "application/json",
773
- "content-length": Buffer.byteLength(body),
774
- },
775
- body,
776
- };
777
- },
778
- });
779
- }
780
- // default error handler
781
- {
782
- finalServices.push(
783
- jsenvServiceErrorHandler({
784
- sendErrorDetails: true,
785
- }),
786
- );
787
- }
788
-
789
- const server = await startServer({
790
- signal,
791
- stopOnExit: false,
792
- stopOnSIGINT: handleSIGINT,
793
- stopOnInternalError: false,
794
- keepProcessAlive,
795
- logLevel: serverLogLevel,
796
- startLog: false,
797
-
798
- https,
799
- http2,
800
- acceptAnyIp,
801
- hostname,
802
- port,
803
- requestWaitingMs: 60_000,
804
- services: finalServices,
805
- });
806
- server.stoppedPromise.then((reason) => {
807
- onStop();
808
- for (const serverStopCallback of serverStopCallbackSet) {
809
- serverStopCallback(reason);
810
- }
811
- serverStopCallbackSet.clear();
812
- });
813
- startDevServerTask.done();
814
- if (hostname) {
815
- delete server.origins.localip;
816
- delete server.origins.externalip;
817
- }
818
- logger.info(``);
819
- Object.keys(server.origins).forEach((key) => {
820
- logger.info(`- ${server.origins[key]}`);
821
- });
822
- logger.info(``);
823
- return {
824
- origin: server.origin,
825
- sourceDirectoryUrl,
826
- stop: () => {
827
- server.stop();
828
- },
829
- kitchenCache,
830
- };
831
- };
832
-
833
- const cacheIsDisabledInResponseHeader = (urlInfo) => {
834
- return (
835
- urlInfo.headers["cache-control"] === "no-store" ||
836
- urlInfo.headers["cache-control"] === "no-cache"
837
- );
838
- };
839
-
840
- export { startDevServer };