@jsenv/core 38.3.4 → 38.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "38.3.4",
3
+ "version": "38.3.6",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -45,9 +45,9 @@
45
45
  "dev": "node --conditions=development ./scripts/dev/dev.mjs",
46
46
  "test": "node --conditions=development ./scripts/test/test.mjs",
47
47
  "test:workspace": "npm run test --workspaces --if-present -- --workspace",
48
- "snapshots:core": "npm run test -- --no-snapshot-assertion",
49
- "snapshots:packages": "npm run test --workspaces --if-present -- --workspace --no-snapshot-assertion",
50
- "snapshots:only_dev_server_errors": "node --conditions=development ./tests/dev_server/errors/generate_snapshot_files.mjs",
48
+ "test:snapshots_core": "npm run test -- --no-snapshot-assertion",
49
+ "test:snapshots_packages": "npm run test --workspaces --if-present -- --workspace --no-snapshot-assertion",
50
+ "test:snapshots_only_dev_server_errors": "node --conditions=development ./tests/dev_server/errors/generate_snapshot_files.mjs",
51
51
  "build": "node --conditions=development ./scripts/build/build.mjs",
52
52
  "build:file_size": "node ./scripts/build/build_file_size.mjs --log",
53
53
  "build:workspace": "npm run build --workspaces --if-present --conditions=developement",
@@ -68,14 +68,14 @@
68
68
  "@jsenv/importmap": "1.2.1",
69
69
  "@jsenv/integrity": "0.0.1",
70
70
  "@jsenv/js-module-fallback": "1.3.9",
71
- "@jsenv/log": "3.4.2",
71
+ "@jsenv/log": "3.4.3",
72
72
  "@jsenv/node-esm-resolution": "1.0.1",
73
- "@jsenv/plugin-bundling": "2.5.8",
73
+ "@jsenv/plugin-bundling": "2.5.9",
74
74
  "@jsenv/plugin-minification": "1.5.4",
75
75
  "@jsenv/plugin-supervisor": "1.3.9",
76
76
  "@jsenv/plugin-transpilation": "1.3.8",
77
77
  "@jsenv/runtime-compat": "1.2.0",
78
- "@jsenv/server": "15.1.5",
78
+ "@jsenv/server": "15.1.6",
79
79
  "@jsenv/sourcemap": "1.2.4",
80
80
  "@jsenv/url-meta": "8.1.0",
81
81
  "@jsenv/urls": "2.2.1",
@@ -99,7 +99,7 @@
99
99
  "eslint-plugin-import": "2.29.0",
100
100
  "eslint-plugin-react": "7.33.2",
101
101
  "open": "9.1.0",
102
- "playwright": "1.39.0",
102
+ "playwright": "1.40.0",
103
103
  "prettier": "3.1.0"
104
104
  }
105
105
  }
@@ -1,4 +1,9 @@
1
- import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
1
+ import { readFileSync } from "node:fs";
2
+ import { URL_META } from "@jsenv/url-meta";
3
+ import {
4
+ assertAndNormalizeDirectoryUrl,
5
+ bufferToEtag,
6
+ } from "@jsenv/filesystem";
2
7
  import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
3
8
  import { createLogger, createTaskLog } from "@jsenv/log";
4
9
  import {
@@ -6,13 +11,21 @@ import {
6
11
  startServer,
7
12
  jsenvServiceCORS,
8
13
  jsenvServiceErrorHandler,
14
+ serveDirectory,
15
+ composeTwoResponses,
9
16
  } from "@jsenv/server";
10
17
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
11
18
 
19
+ import { WEB_URL_CONVERTER } from "../helpers/web_url_converter.js";
20
+ import { watchSourceFiles } from "../helpers/watch_source_files.js";
21
+ import { createEventEmitter } from "../helpers/event_emitter.js";
12
22
  import { lookupPackageDirectory } from "../helpers/lookup_package_directory.js";
13
23
  import { createServerEventsDispatcher } from "../plugins/server_events/server_events_dispatcher.js";
14
24
  import { defaultRuntimeCompat } from "../build/build.js";
15
- import { createFileService } from "./file_service.js";
25
+ import { createKitchen } from "../kitchen/kitchen.js";
26
+ import { getCorePlugins } from "../plugins/plugins.js";
27
+ import { jsenvPluginServerEventsClientInjection } from "../plugins/server_events/jsenv_plugin_server_events_client_injection.js";
28
+ import { parseUserAgentHeader } from "./user_agent.js";
16
29
 
17
30
  /**
18
31
  * Start a server for source files:
@@ -99,6 +112,16 @@ export const startDevServer = async ({
99
112
  }
100
113
  }
101
114
 
115
+ // params normalization
116
+ {
117
+ if (clientAutoreload === true) {
118
+ clientAutoreload = {};
119
+ }
120
+ if (clientAutoreload === false) {
121
+ clientAutoreload = { enabled: false };
122
+ }
123
+ }
124
+
102
125
  const logger = createLogger({ logLevel });
103
126
  const operation = Abort.startOperation();
104
127
  operation.addAbortSignal(signal);
@@ -122,48 +145,38 @@ export const startDevServer = async ({
122
145
  serverEventsDispatcher.destroy();
123
146
  });
124
147
  const kitchenCache = new Map();
125
- const server = await startServer({
126
- signal,
127
- stopOnExit: false,
128
- stopOnSIGINT: handleSIGINT,
129
- stopOnInternalError: false,
130
- keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
131
- ? false
132
- : keepProcessAlive,
133
- logLevel: serverLogLevel,
134
- startLog: false,
135
148
 
136
- https,
137
- http2,
138
- acceptAnyIp,
139
- hostname,
140
- port,
141
- requestWaitingMs: 60_000,
142
- services: [
143
- {
144
- handleRequest: (request) => {
145
- if (request.headers["x-server-inspect"]) {
146
- return { status: 200 };
147
- }
148
- if (request.pathname === "/__params__.json") {
149
- const json = JSON.stringify({
150
- sourceDirectoryUrl,
151
- });
152
- return {
153
- status: 200,
154
- headers: {
155
- "content-type": "application/json",
156
- "content-length": Buffer.byteLength(json),
157
- },
158
- body: json,
159
- };
160
- }
161
- return null;
162
- },
163
- injectResponseHeaders: () => {
164
- return { server: "jsenv_dev_server/1" };
165
- },
149
+ const finalServices = [];
150
+ // x-server-inspect service
151
+ {
152
+ finalServices.push({
153
+ handleRequest: (request) => {
154
+ if (request.headers["x-server-inspect"]) {
155
+ return { status: 200 };
156
+ }
157
+ if (request.pathname === "/__params__.json") {
158
+ const json = JSON.stringify({
159
+ sourceDirectoryUrl,
160
+ });
161
+ return {
162
+ status: 200,
163
+ headers: {
164
+ "content-type": "application/json",
165
+ "content-length": Buffer.byteLength(json),
166
+ },
167
+ body: json,
168
+ };
169
+ }
170
+ return null;
171
+ },
172
+ injectResponseHeaders: () => {
173
+ return { server: "jsenv_dev_server/1" };
166
174
  },
175
+ });
176
+ }
177
+ // cors service
178
+ {
179
+ finalServices.push(
167
180
  jsenvServiceCORS({
168
181
  accessControlAllowRequestOrigin: true,
169
182
  accessControlAllowRequestMethod: true,
@@ -175,86 +188,453 @@ export const startDevServer = async ({
175
188
  accessControlAllowCredentials: true,
176
189
  timingAllowOrigin: true,
177
190
  }),
178
- ...services,
191
+ );
192
+ }
193
+ // custom services
194
+ {
195
+ finalServices.push(...services);
196
+ }
197
+ // file_service
198
+ {
199
+ const clientFileChangeEventEmitter = createEventEmitter();
200
+ const clientFileDereferencedEventEmitter = createEventEmitter();
201
+ clientAutoreload = {
202
+ enabled: true,
203
+ clientServerEventsConfig: {},
204
+ clientFileChangeEventEmitter,
205
+ clientFileDereferencedEventEmitter,
206
+ ...clientAutoreload,
207
+ };
208
+ const stopWatchingSourceFiles = watchSourceFiles(
209
+ sourceDirectoryUrl,
210
+ (fileInfo) => {
211
+ clientFileChangeEventEmitter.emit(fileInfo);
212
+ },
179
213
  {
180
- name: "jsenv:omega_file_service",
181
- handleRequest: createFileService({
182
- signal,
183
- logLevel,
184
- serverStopCallbacks,
185
- serverEventsDispatcher,
186
- kitchenCache,
187
- onKitchenCreated,
214
+ sourceFilesConfig,
215
+ keepProcessAlive: false,
216
+ cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
217
+ },
218
+ );
219
+ serverStopCallbacks.push(stopWatchingSourceFiles);
220
+
221
+ const getOrCreateKitchen = (request) => {
222
+ const { runtimeName, runtimeVersion } = parseUserAgentHeader(
223
+ request.headers["user-agent"] || "",
224
+ );
225
+ const runtimeId = `${runtimeName}@${runtimeVersion}`;
226
+ const existing = kitchenCache.get(runtimeId);
227
+ if (existing) {
228
+ return existing;
229
+ }
230
+ const watchAssociations = URL_META.resolveAssociations(
231
+ { watch: stopWatchingSourceFiles.watchPatterns },
232
+ sourceDirectoryUrl,
233
+ );
234
+ let kitchen;
235
+ clientFileChangeEventEmitter.on(({ url }) => {
236
+ const urlInfo = kitchen.graph.getUrlInfo(url);
237
+ if (urlInfo) {
238
+ urlInfo.onModified();
239
+ }
240
+ });
241
+ const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
188
242
 
189
- sourceDirectoryUrl,
190
- sourceMainFilePath,
191
- ignore,
192
- sourceFilesConfig,
193
- runtimeCompat,
243
+ kitchen = createKitchen({
244
+ name: runtimeId,
245
+ signal,
246
+ logLevel,
247
+ rootDirectoryUrl: sourceDirectoryUrl,
248
+ mainFilePath: sourceMainFilePath,
249
+ ignore,
250
+ dev: true,
251
+ runtimeCompat,
252
+ clientRuntimeCompat,
253
+ plugins: [
254
+ ...plugins,
255
+ ...getCorePlugins({
256
+ rootDirectoryUrl: sourceDirectoryUrl,
257
+ runtimeCompat,
194
258
 
195
- plugins,
196
- referenceAnalysis,
197
- nodeEsmResolution,
198
- magicExtensions,
199
- magicDirectoryIndex,
200
- supervisor,
201
- injections,
202
- transpilation,
203
- clientAutoreload,
204
- cacheControl,
205
- ribbon,
206
- sourcemaps,
207
- sourcemapsSourcesContent,
208
- outDirectoryUrl,
209
- }),
210
- handleWebsocket: (websocket, { request }) => {
211
- if (request.headers["sec-websocket-protocol"] === "jsenv") {
212
- serverEventsDispatcher.addWebsocket(websocket, request);
259
+ referenceAnalysis,
260
+ nodeEsmResolution,
261
+ magicExtensions,
262
+ magicDirectoryIndex,
263
+ supervisor,
264
+ injections,
265
+ transpilation,
266
+
267
+ clientAutoreload,
268
+ cacheControl,
269
+ ribbon,
270
+ }),
271
+ ],
272
+ supervisor,
273
+ minification: false,
274
+ sourcemaps,
275
+ sourcemapsSourcesContent,
276
+ outDirectoryUrl: outDirectoryUrl
277
+ ? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
278
+ : undefined,
279
+ });
280
+ kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
281
+ const { watch } = URL_META.applyAssociations({
282
+ url: urlInfoCreated.url,
283
+ associations: watchAssociations,
284
+ });
285
+ urlInfoCreated.isWatched = watch;
286
+ // when an url depends on many others, we check all these (like package.json)
287
+ urlInfoCreated.isValid = () => {
288
+ if (!urlInfoCreated.url.startsWith("file:")) {
289
+ return false;
213
290
  }
214
- },
215
- },
216
- {
217
- name: "jsenv:omega_error_handler",
218
- handleError: (error) => {
219
- const getResponseForError = () => {
220
- if (error && error.asResponse) {
221
- return error.asResponse();
291
+ if (urlInfoCreated.content === undefined) {
292
+ // urlInfo content is undefined when:
293
+ // - url info content never fetched
294
+ // - it is considered as modified because undelying file is watched and got saved
295
+ // - it is considered as modified because underlying file content
296
+ // was compared using etag and it has changed
297
+ return false;
298
+ }
299
+ if (!watch) {
300
+ // file is not watched, check the filesystem
301
+ let fileContentAsBuffer;
302
+ try {
303
+ fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
304
+ } catch (e) {
305
+ if (e.code === "ENOENT") {
306
+ urlInfoCreated.onModified();
307
+ return false;
308
+ }
309
+ return false;
310
+ }
311
+ const fileContentEtag = bufferToEtag(fileContentAsBuffer);
312
+ if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
313
+ urlInfoCreated.onModified();
314
+ // restore content to be able to compare it again later
315
+ urlInfoCreated.kitchen.urlInfoTransformer.setContent(
316
+ urlInfoCreated,
317
+ String(fileContentAsBuffer),
318
+ {
319
+ contentEtag: fileContentEtag,
320
+ },
321
+ );
322
+ return false;
222
323
  }
324
+ }
325
+ for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
326
+ const implicitUrlInfo =
327
+ urlInfoCreated.graph.getUrlInfo(implicitUrl);
328
+ if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
329
+ return false;
330
+ }
331
+ }
332
+ return true;
333
+ };
334
+ });
335
+ kitchen.graph.urlInfoDereferencedEventEmitter.on(
336
+ (urlInfoDereferenced, lastReferenceFromOther) => {
337
+ clientFileDereferencedEventEmitter.emit(
338
+ urlInfoDereferenced,
339
+ lastReferenceFromOther,
340
+ );
341
+ },
342
+ );
343
+
344
+ serverStopCallbacks.push(() => {
345
+ kitchen.pluginController.callHooks("destroy", kitchen.context);
346
+ });
347
+ server_events: {
348
+ const allServerEvents = {};
349
+ kitchen.pluginController.plugins.forEach((plugin) => {
350
+ const { serverEvents } = plugin;
351
+ if (serverEvents) {
352
+ Object.keys(serverEvents).forEach((serverEventName) => {
353
+ // we could throw on serverEvent name conflict
354
+ // we could throw if serverEvents[serverEventName] is not a function
355
+ allServerEvents[serverEventName] = serverEvents[serverEventName];
356
+ });
357
+ }
358
+ });
359
+ const serverEventNames = Object.keys(allServerEvents);
360
+ if (serverEventNames.length > 0) {
361
+ Object.keys(allServerEvents).forEach((serverEventName) => {
362
+ const serverEventInfo = {
363
+ ...kitchen.context,
364
+ sendServerEvent: (data) => {
365
+ serverEventsDispatcher.dispatch({
366
+ type: serverEventName,
367
+ data,
368
+ });
369
+ },
370
+ };
371
+ const serverEventInit = allServerEvents[serverEventName];
372
+ serverEventInit(serverEventInfo);
373
+ });
374
+ // "pushPlugin" so that event source client connection can be put as early as possible in html
375
+ kitchen.pluginController.pushPlugin(
376
+ jsenvPluginServerEventsClientInjection(
377
+ clientAutoreload.clientServerEventsConfig,
378
+ ),
379
+ );
380
+ }
381
+ }
382
+
383
+ kitchenCache.set(runtimeId, kitchen);
384
+ onKitchenCreated(kitchen);
385
+ return kitchen;
386
+ };
387
+
388
+ finalServices.push({
389
+ name: "jsenv:omega_file_service",
390
+ handleRequest: async (request) => {
391
+ const kitchen = getOrCreateKitchen(request);
392
+ const serveHookInfo = {
393
+ ...kitchen.context,
394
+ request,
395
+ };
396
+ const responseFromPlugin =
397
+ await kitchen.pluginController.callAsyncHooksUntil(
398
+ "serve",
399
+ serveHookInfo,
400
+ );
401
+ if (responseFromPlugin) {
402
+ return responseFromPlugin;
403
+ }
404
+ const { referer } = request.headers;
405
+ const parentUrl = referer
406
+ ? WEB_URL_CONVERTER.asFileUrl(referer, {
407
+ origin: request.origin,
408
+ rootDirectoryUrl: sourceDirectoryUrl,
409
+ })
410
+ : sourceDirectoryUrl;
411
+ let reference = kitchen.graph.inferReference(
412
+ request.resource,
413
+ parentUrl,
414
+ );
415
+ if (!reference) {
416
+ reference =
417
+ kitchen.graph.rootUrlInfo.dependencies.createResolveAndFinalize({
418
+ trace: { message: parentUrl },
419
+ type: "http_request",
420
+ specifier: request.resource,
421
+ });
422
+ }
423
+ const urlInfo = reference.urlInfo;
424
+ const ifNoneMatch = request.headers["if-none-match"];
425
+ const urlInfoTargetedByCache = urlInfo.findParentIfInline() || urlInfo;
426
+
427
+ try {
428
+ if (!urlInfo.error && ifNoneMatch) {
429
+ const [clientOriginalContentEtag, clientContentEtag] =
430
+ ifNoneMatch.split("_");
223
431
  if (
224
- error &&
225
- error.statusText === "Unexpected directory operation"
432
+ urlInfoTargetedByCache.originalContentEtag ===
433
+ clientOriginalContentEtag &&
434
+ urlInfoTargetedByCache.contentEtag === clientContentEtag &&
435
+ urlInfoTargetedByCache.isValid()
226
436
  ) {
437
+ const headers = {
438
+ "cache-control": `private,max-age=0,must-revalidate`,
439
+ };
440
+ Object.keys(urlInfo.headers).forEach((key) => {
441
+ if (key !== "content-length") {
442
+ headers[key] = urlInfo.headers[key];
443
+ }
444
+ });
227
445
  return {
228
- status: 403,
446
+ status: 304,
447
+ headers,
229
448
  };
230
449
  }
231
- return convertFileSystemErrorToResponseProperties(error);
232
- };
233
- const response = getResponseForError();
234
- if (!response) {
235
- return null;
236
450
  }
237
- const body = JSON.stringify({
238
- status: response.status,
239
- statusText: response.statusText,
240
- headers: response.headers,
241
- body: response.body,
242
- });
243
- return {
451
+
452
+ await urlInfo.cook({ request, reference });
453
+ let { response } = urlInfo;
454
+ if (response) {
455
+ return response;
456
+ }
457
+ response = {
458
+ url: reference.url,
244
459
  status: 200,
245
460
  headers: {
246
- "content-type": "application/json",
247
- "content-length": Buffer.byteLength(body),
461
+ // when we send eTag to the client the next request to the server
462
+ // will send etag in request headers.
463
+ // If they match jsenv bypass cooking and returns 304
464
+ // This must not happen when a plugin uses "no-store" or "no-cache" as it means
465
+ // plugin logic wants to happens for every request to this url
466
+ ...(urlInfo.headers["cache-control"] === "no-store" ||
467
+ urlInfo.headers["cache-control"] === "no-cache"
468
+ ? {}
469
+ : {
470
+ "cache-control": `private,max-age=0,must-revalidate`,
471
+ // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
472
+ "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
473
+ }),
474
+ ...urlInfo.headers,
475
+ "content-type": urlInfo.contentType,
476
+ "content-length": urlInfo.contentLength,
248
477
  },
249
- body,
478
+ body: urlInfo.content,
479
+ timing: urlInfo.timing,
250
480
  };
251
- },
481
+ const augmentResponseInfo = {
482
+ ...kitchen.context,
483
+ reference,
484
+ urlInfo,
485
+ };
486
+ kitchen.pluginController.callHooks(
487
+ "augmentResponse",
488
+ augmentResponseInfo,
489
+ (returnValue) => {
490
+ response = composeTwoResponses(response, returnValue);
491
+ },
492
+ );
493
+ return response;
494
+ } catch (e) {
495
+ urlInfo.error = e;
496
+ const originalError = e ? e.cause || e : e;
497
+ if (originalError.asResponse) {
498
+ return originalError.asResponse();
499
+ }
500
+ const code = originalError.code;
501
+ if (code === "PARSE_ERROR") {
502
+ // when possible let browser re-throw the syntax error
503
+ // it's not possible to do that when url info content is not available
504
+ // (happens for js_module_fallback for instance)
505
+ if (urlInfo.content !== undefined) {
506
+ kitchen.context.logger.error(`Error while handling ${request.url}:
507
+ ${originalError.reasonCode || originalError.code}
508
+ ${e.traceMessage}`);
509
+ return {
510
+ url: reference.url,
511
+ status: 200,
512
+ // reason becomes the http response statusText, it must not contain invalid chars
513
+ // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
514
+ statusText: e.reason,
515
+ statusMessage: originalError.message,
516
+ headers: {
517
+ "content-type": urlInfo.contentType,
518
+ "content-length": urlInfo.contentLength,
519
+ "cache-control": "no-store",
520
+ },
521
+ body: urlInfo.content,
522
+ };
523
+ }
524
+ return {
525
+ url: reference.url,
526
+ status: 500,
527
+ statusText: e.reason,
528
+ statusMessage: originalError.message,
529
+ headers: {
530
+ "cache-control": "no-store",
531
+ },
532
+ body: urlInfo.content,
533
+ };
534
+ }
535
+ if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
536
+ return serveDirectory(reference.url, {
537
+ headers: {
538
+ accept: "text/html",
539
+ },
540
+ canReadDirectory: true,
541
+ rootDirectoryUrl: sourceDirectoryUrl,
542
+ });
543
+ }
544
+ if (code === "NOT_ALLOWED") {
545
+ return {
546
+ url: reference.url,
547
+ status: 403,
548
+ statusText: originalError.reason,
549
+ };
550
+ }
551
+ if (code === "NOT_FOUND") {
552
+ return {
553
+ url: reference.url,
554
+ status: 404,
555
+ statusText: originalError.reason,
556
+ statusMessage: originalError.message,
557
+ };
558
+ }
559
+ return {
560
+ url: reference.url,
561
+ status: 500,
562
+ statusText: e.reason,
563
+ statusMessage: e.stack,
564
+ };
565
+ }
566
+ },
567
+ handleWebsocket: (websocket, { request }) => {
568
+ if (request.headers["sec-websocket-protocol"] === "jsenv") {
569
+ serverEventsDispatcher.addWebsocket(websocket, request);
570
+ }
252
571
  },
253
- // default error handling
572
+ });
573
+ }
574
+ // jsenv error handler service
575
+ {
576
+ finalServices.push({
577
+ name: "jsenv:omega_error_handler",
578
+ handleError: (error) => {
579
+ const getResponseForError = () => {
580
+ if (error && error.asResponse) {
581
+ return error.asResponse();
582
+ }
583
+ if (error && error.statusText === "Unexpected directory operation") {
584
+ return {
585
+ status: 403,
586
+ };
587
+ }
588
+ return convertFileSystemErrorToResponseProperties(error);
589
+ };
590
+ const response = getResponseForError();
591
+ if (!response) {
592
+ return null;
593
+ }
594
+ const body = JSON.stringify({
595
+ status: response.status,
596
+ statusText: response.statusText,
597
+ headers: response.headers,
598
+ body: response.body,
599
+ });
600
+ return {
601
+ status: 200,
602
+ headers: {
603
+ "content-type": "application/json",
604
+ "content-length": Buffer.byteLength(body),
605
+ },
606
+ body,
607
+ };
608
+ },
609
+ });
610
+ }
611
+ // default error handler
612
+ {
613
+ finalServices.push(
254
614
  jsenvServiceErrorHandler({
255
615
  sendErrorDetails: true,
256
616
  }),
257
- ],
617
+ );
618
+ }
619
+
620
+ const server = await startServer({
621
+ signal,
622
+ stopOnExit: false,
623
+ stopOnSIGINT: handleSIGINT,
624
+ stopOnInternalError: false,
625
+ keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
626
+ ? false
627
+ : keepProcessAlive,
628
+ logLevel: serverLogLevel,
629
+ startLog: false,
630
+
631
+ https,
632
+ http2,
633
+ acceptAnyIp,
634
+ hostname,
635
+ port,
636
+ requestWaitingMs: 60_000,
637
+ services: finalServices,
258
638
  });
259
639
  server.stoppedPromise.then((reason) => {
260
640
  onStop();