@chidchanun/bcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/docs/caching.md +76 -0
  5. package/docs/configuration.md +97 -0
  6. package/docs/deployment.md +74 -0
  7. package/docs/getting-started.md +82 -0
  8. package/docs/middleware.md +58 -0
  9. package/docs/releasing.md +299 -0
  10. package/docs/routing.md +103 -0
  11. package/docs/security.md +57 -0
  12. package/package.json +68 -0
  13. package/packages/bundler/src/client-islands.ts +1457 -0
  14. package/packages/bundler/src/incremental-context.ts +206 -0
  15. package/packages/bundler/src/index.ts +1991 -0
  16. package/packages/bundler/src/module-graph.ts +317 -0
  17. package/packages/bundler/src/partial-hydration.ts +414 -0
  18. package/packages/bundler/src/production.ts +974 -0
  19. package/packages/bundler/src/server-production-middleware.ts +447 -0
  20. package/packages/bundler/src/server-production.ts +1193 -0
  21. package/packages/bundler/src/special-files.ts +131 -0
  22. package/packages/cache/src/index.ts +761 -0
  23. package/packages/cli/bin/bcp.mjs +93 -0
  24. package/packages/cli/src/args.ts +305 -0
  25. package/packages/cli/src/bootstrap.ts +514 -0
  26. package/packages/cli/src/index.ts +504 -0
  27. package/packages/cli/src/version.ts +45 -0
  28. package/packages/client/src/cache.ts +11 -0
  29. package/packages/client/src/config.ts +18 -0
  30. package/packages/client/src/error-boundary.tsx +149 -0
  31. package/packages/client/src/hydration.ts +3 -0
  32. package/packages/client/src/index.tsx +57 -0
  33. package/packages/client/src/islands.tsx +315 -0
  34. package/packages/client/src/metadata.ts +281 -0
  35. package/packages/client/src/navigation-loading.ts +52 -0
  36. package/packages/client/src/navigation-state.ts +80 -0
  37. package/packages/client/src/not-found.ts +34 -0
  38. package/packages/client/src/persistent-layout-runtime.ts +273 -0
  39. package/packages/client/src/router-v2.tsx +969 -0
  40. package/packages/client/src/router.tsx +1 -0
  41. package/packages/config/src/index.ts +1038 -0
  42. package/packages/env/src/index.ts +593 -0
  43. package/packages/router/src/advanced-router.ts +1032 -0
  44. package/packages/router/src/index.ts +1 -0
  45. package/packages/server/src/compression.ts +249 -0
  46. package/packages/server/src/dev-document-metadata.ts +154 -0
  47. package/packages/server/src/dev-hmr.ts +225 -0
  48. package/packages/server/src/index.ts +2265 -0
  49. package/packages/server/src/metadata.ts +478 -0
  50. package/packages/server/src/middleware-dev-server.ts +260 -0
  51. package/packages/server/src/middleware-loader.ts +140 -0
  52. package/packages/server/src/middleware-proxy.ts +516 -0
  53. package/packages/server/src/middleware.ts +704 -0
  54. package/packages/server/src/navigation-payload.ts +471 -0
  55. package/packages/server/src/production-server.ts +1746 -0
  56. package/packages/server/src/response-cache-proxy.ts +828 -0
  57. package/packages/server/src/security-proxy.ts +406 -0
  58. package/packages/server/src/security.ts +451 -0
  59. package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
  60. package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
  61. package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
  62. package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
  63. package/packages/server/src/standalone-production-runtime.ts +1951 -0
  64. package/packages/server/src/standalone-production-server.ts +6 -0
  65. package/packages/server/src/static-assets.ts +262 -0
  66. package/packages/server/src/static-dev-server.ts +854 -0
@@ -0,0 +1,2047 @@
1
+ import {
2
+ createHash,
3
+ } from "node:crypto";
4
+ import fs from "node:fs/promises";
5
+ import * as http from "node:http";
6
+ import path from "node:path";
7
+
8
+ import {
9
+ createElement,
10
+ type ComponentType,
11
+ type ReactElement,
12
+ } from "react";
13
+
14
+ import {
15
+ renderToStaticMarkup,
16
+ renderToString,
17
+ } from "react-dom/server";
18
+
19
+ import type {
20
+ ProductionRouteAsset,
21
+ } from "../../bundler/src/production.js";
22
+
23
+ import {
24
+ createMetadataHeadEntries,
25
+ type ResolvedMetadata,
26
+ } from "../../client/src/metadata.js";
27
+
28
+ import {
29
+ isNotFoundError,
30
+ } from "../../client/src/not-found.js";
31
+
32
+ import {
33
+ matchApiRoute,
34
+ matchRoute,
35
+ type ApiRoute,
36
+ type Route,
37
+ type RouteSegment,
38
+ } from "../../router/src/index.js";
39
+
40
+ import {
41
+ appendVary,
42
+ compressDynamicContent,
43
+ isCompressibleContentType,
44
+ selectContentEncoding,
45
+ } from "./compression.js";
46
+
47
+ import {
48
+ resolveRouteMetadata,
49
+ type MetadataSource,
50
+ } from "./metadata.js";
51
+
52
+ import {
53
+ getContentType,
54
+ readPublicAsset,
55
+ type StaticAsset,
56
+ } from "./static-assets.js";
57
+
58
+ export interface StandalonePageRoute {
59
+ pathname: string;
60
+ segments: RouteSegment[];
61
+ page: ComponentType<any>;
62
+ layouts: ComponentType<any>[];
63
+ loading: ComponentType<any> | null;
64
+ error: ComponentType<any> | null;
65
+ notFound: ComponentType<any> | null;
66
+ metadataSources: MetadataSource[];
67
+ client: ProductionRouteAsset;
68
+ }
69
+
70
+ export interface StandaloneApiRoute {
71
+ pathname: string;
72
+ segments: RouteSegment[];
73
+ handlers: Record<string, unknown>;
74
+ }
75
+
76
+ export interface StandaloneProductionServerOptions {
77
+ port: number;
78
+ hostname: string;
79
+ buildDirectory: string;
80
+ buildId: string;
81
+ version: number;
82
+ routes: StandalonePageRoute[];
83
+ apiRoutes: StandaloneApiRoute[];
84
+ globalNotFound?: ComponentType<any> | null;
85
+ globalNotFoundLayouts?: ComponentType<any>[];
86
+ }
87
+
88
+ interface ApiContext {
89
+ params: Record<string, string>;
90
+ }
91
+
92
+ type ApiHandler = (
93
+ request: Request,
94
+ context: ApiContext
95
+ ) => Response | Promise<Response>;
96
+
97
+ const HTTP_METHODS = [
98
+ "GET",
99
+ "POST",
100
+ "PUT",
101
+ "PATCH",
102
+ "DELETE",
103
+ "HEAD",
104
+ "OPTIONS",
105
+ ] as const;
106
+
107
+ type HttpMethod =
108
+ typeof HTTP_METHODS[number];
109
+
110
+ export function createStandaloneProductionServer(
111
+ options: StandaloneProductionServerOptions
112
+ ) {
113
+ const port =
114
+ options.port;
115
+ const hostname =
116
+ options.hostname;
117
+ const buildDirectory =
118
+ path.resolve(
119
+ options.buildDirectory
120
+ );
121
+ const clientDirectory =
122
+ path.join(
123
+ buildDirectory,
124
+ "client"
125
+ );
126
+
127
+ const routeByPathname =
128
+ new Map(
129
+ options.routes.map(
130
+ (route) => [
131
+ route.pathname,
132
+ route,
133
+ ]
134
+ )
135
+ );
136
+
137
+ const apiByPathname =
138
+ new Map(
139
+ options.apiRoutes.map(
140
+ (route) => [
141
+ route.pathname,
142
+ route,
143
+ ]
144
+ )
145
+ );
146
+
147
+ const routePatterns:
148
+ Route[] =
149
+ options.routes.map(
150
+ (route) => ({
151
+ pathname:
152
+ route.pathname,
153
+ segments:
154
+ route.segments,
155
+ filePath:
156
+ route.pathname,
157
+ layouts: [],
158
+ })
159
+ );
160
+
161
+ const apiPatterns:
162
+ ApiRoute[] =
163
+ options.apiRoutes.map(
164
+ (route) => ({
165
+ pathname:
166
+ route.pathname,
167
+ segments:
168
+ route.segments,
169
+ filePath:
170
+ route.pathname,
171
+ })
172
+ );
173
+
174
+ const server =
175
+ http.createServer(
176
+ async (
177
+ req,
178
+ res
179
+ ) => {
180
+ const startedAt =
181
+ performance.now();
182
+ let pathname =
183
+ "/";
184
+
185
+ try {
186
+ const requestUrl =
187
+ new URL(
188
+ req.url ?? "/",
189
+ `http://${
190
+ req.headers.host ??
191
+ `${hostname}:${port}`
192
+ }`
193
+ );
194
+
195
+ pathname =
196
+ normalizePathname(
197
+ requestUrl.pathname
198
+ );
199
+
200
+ res.once(
201
+ "finish",
202
+ () => {
203
+ const duration =
204
+ performance.now() -
205
+ startedAt;
206
+
207
+ console.log(
208
+ `${req.method ?? "GET"} ${pathname} ${res.statusCode} ${duration.toFixed(2)}ms`
209
+ );
210
+ }
211
+ );
212
+
213
+ if (
214
+ pathname ===
215
+ "/_bcp/navigation"
216
+ ) {
217
+ await handleNavigation(
218
+ req,
219
+ res,
220
+ requestUrl,
221
+ routePatterns,
222
+ routeByPathname,
223
+ options.version
224
+ );
225
+ return;
226
+ }
227
+
228
+ if (
229
+ pathname.startsWith(
230
+ "/_bcp/client/"
231
+ )
232
+ ) {
233
+ const served =
234
+ await serveBuildAsset(
235
+ req,
236
+ res,
237
+ clientDirectory,
238
+ pathname
239
+ );
240
+
241
+ if (!served) {
242
+ sendSimpleNotFound(
243
+ req,
244
+ res
245
+ );
246
+ }
247
+ return;
248
+ }
249
+
250
+ const apiMatch =
251
+ matchApiRoute(
252
+ apiPatterns,
253
+ pathname
254
+ );
255
+
256
+ if (apiMatch) {
257
+ const definition =
258
+ apiByPathname.get(
259
+ apiMatch.route.pathname
260
+ );
261
+
262
+ if (!definition) {
263
+ throw new Error(
264
+ `BCP Framework: standalone API definition missing for "${apiMatch.route.pathname}".`
265
+ );
266
+ }
267
+
268
+ try {
269
+ await handleApiRoute(
270
+ req,
271
+ res,
272
+ requestUrl,
273
+ definition,
274
+ apiMatch.params
275
+ );
276
+ } catch (error) {
277
+ console.error(
278
+ `[BCP API Error] ${req.method ?? "GET"} ${pathname}:`,
279
+ error
280
+ );
281
+
282
+ if (!res.headersSent) {
283
+ sendJson(
284
+ req,
285
+ res,
286
+ 500,
287
+ {
288
+ error:
289
+ "Internal Server Error",
290
+ }
291
+ );
292
+ } else {
293
+ res.end();
294
+ }
295
+ }
296
+ return;
297
+ }
298
+
299
+ const publicAsset =
300
+ await readPublicAsset(
301
+ buildDirectory,
302
+ pathname
303
+ );
304
+
305
+ if (publicAsset) {
306
+ sendPublicAsset(
307
+ req,
308
+ res,
309
+ publicAsset
310
+ );
311
+ return;
312
+ }
313
+
314
+ const match =
315
+ matchRoute(
316
+ routePatterns,
317
+ pathname
318
+ );
319
+
320
+ if (!match) {
321
+ sendGlobalNotFound(
322
+ req,
323
+ res,
324
+ options
325
+ );
326
+ return;
327
+ }
328
+
329
+ const definition =
330
+ routeByPathname.get(
331
+ match.route.pathname
332
+ );
333
+
334
+ if (!definition) {
335
+ throw new Error(
336
+ `BCP Framework: standalone page definition missing for "${match.route.pathname}".`
337
+ );
338
+ }
339
+
340
+ await sendPage(
341
+ req,
342
+ res,
343
+ definition,
344
+ match.params,
345
+ options
346
+ );
347
+ } catch (error) {
348
+ console.error(
349
+ "BCP Standalone Production Error:",
350
+ error
351
+ );
352
+
353
+ if (res.headersSent) {
354
+ res.end();
355
+ return;
356
+ }
357
+
358
+ sendInternalServerError(
359
+ req,
360
+ res,
361
+ error
362
+ );
363
+ }
364
+ }
365
+ );
366
+
367
+ return {
368
+ async start() {
369
+ process.env.NODE_ENV =
370
+ "production";
371
+
372
+ await new Promise<void>(
373
+ (
374
+ resolve,
375
+ reject
376
+ ) => {
377
+ const onError =
378
+ (error: Error) => {
379
+ server.off(
380
+ "listening",
381
+ onListening
382
+ );
383
+ reject(error);
384
+ };
385
+
386
+ const onListening =
387
+ () => {
388
+ server.off(
389
+ "error",
390
+ onError
391
+ );
392
+ resolve();
393
+ };
394
+
395
+ server.once(
396
+ "error",
397
+ onError
398
+ );
399
+ server.once(
400
+ "listening",
401
+ onListening
402
+ );
403
+ server.listen(
404
+ port,
405
+ hostname
406
+ );
407
+ }
408
+ );
409
+
410
+ console.log("");
411
+ console.log(
412
+ "BCP Framework Standalone Production"
413
+ );
414
+ console.log(
415
+ `Local: http://${hostname}:${port}`
416
+ );
417
+ console.log(
418
+ `Build: ${options.buildId}`
419
+ );
420
+ console.log("");
421
+ },
422
+
423
+ async stop() {
424
+ if (!server.listening) {
425
+ return;
426
+ }
427
+
428
+ await new Promise<void>(
429
+ (
430
+ resolve,
431
+ reject
432
+ ) => {
433
+ server.close(
434
+ (error) => {
435
+ if (error) {
436
+ reject(error);
437
+ return;
438
+ }
439
+ resolve();
440
+ }
441
+ );
442
+ }
443
+ );
444
+ },
445
+ };
446
+ }
447
+
448
+ async function sendPage(
449
+ req: http.IncomingMessage,
450
+ res: http.ServerResponse,
451
+ definition: StandalonePageRoute,
452
+ params: Record<string, string>,
453
+ options: StandaloneProductionServerOptions
454
+ ): Promise<void> {
455
+ try {
456
+ const metadata =
457
+ await resolveRouteMetadata(
458
+ definition.metadataSources,
459
+ params
460
+ );
461
+
462
+ const html =
463
+ renderPageDocument(
464
+ definition,
465
+ params,
466
+ metadata
467
+ );
468
+
469
+ sendHtml(
470
+ req,
471
+ res,
472
+ 200,
473
+ html
474
+ );
475
+ } catch (error) {
476
+ if (isNotFoundError(error)) {
477
+ const NotFoundComponent =
478
+ definition.notFound ??
479
+ options.globalNotFound ??
480
+ null;
481
+
482
+ const html =
483
+ renderFallbackDocument(
484
+ NotFoundComponent,
485
+ definition.layouts,
486
+ params,
487
+ {
488
+ kind:
489
+ "not-found",
490
+ }
491
+ );
492
+
493
+ sendHtml(
494
+ req,
495
+ res,
496
+ 404,
497
+ html
498
+ );
499
+ return;
500
+ }
501
+
502
+ console.error(
503
+ `[BCP Page Error] ${definition.pathname}:`,
504
+ error
505
+ );
506
+
507
+ const html =
508
+ renderFallbackDocument(
509
+ definition.error,
510
+ definition.layouts,
511
+ params,
512
+ {
513
+ kind:
514
+ "error",
515
+ error:
516
+ createPublicServerError(
517
+ error
518
+ ),
519
+ }
520
+ );
521
+
522
+ sendHtml(
523
+ req,
524
+ res,
525
+ 500,
526
+ html
527
+ );
528
+ }
529
+ }
530
+
531
+ function sendGlobalNotFound(
532
+ req: http.IncomingMessage,
533
+ res: http.ServerResponse,
534
+ options: StandaloneProductionServerOptions
535
+ ): void {
536
+ const html =
537
+ renderFallbackDocument(
538
+ options.globalNotFound ??
539
+ null,
540
+ options.globalNotFoundLayouts ??
541
+ [],
542
+ {},
543
+ {
544
+ kind:
545
+ "not-found",
546
+ }
547
+ );
548
+
549
+ sendHtml(
550
+ req,
551
+ res,
552
+ 404,
553
+ html
554
+ );
555
+ }
556
+
557
+ function renderPageDocument(
558
+ definition: StandalonePageRoute,
559
+ params: Record<string, string>,
560
+ metadata: ResolvedMetadata
561
+ ): string {
562
+ const element =
563
+ wrapWithLayouts(
564
+ createElement(
565
+ definition.page,
566
+ {
567
+ params,
568
+ }
569
+ ),
570
+ definition.layouts,
571
+ params
572
+ );
573
+
574
+ const content =
575
+ renderToString(
576
+ element
577
+ );
578
+
579
+ return renderDocument({
580
+ content,
581
+ route:
582
+ definition.pathname,
583
+ params,
584
+ client:
585
+ definition.client,
586
+ metadata,
587
+ });
588
+ }
589
+
590
+ function renderFallbackDocument(
591
+ Component: ComponentType<any> | null,
592
+ layouts: ComponentType<any>[],
593
+ params: Record<string, string>,
594
+ fallback:
595
+ | {
596
+ kind: "not-found";
597
+ }
598
+ | {
599
+ kind: "error";
600
+ error: Error;
601
+ }
602
+ ): string {
603
+ const metadata =
604
+ fallback.kind ===
605
+ "not-found"
606
+ ? createFallbackMetadata(
607
+ "Page Not Found"
608
+ )
609
+ : createFallbackMetadata(
610
+ "Internal Server Error"
611
+ );
612
+
613
+ if (!Component) {
614
+ return fallback.kind ===
615
+ "not-found"
616
+ ? genericNotFoundDocument()
617
+ : genericErrorDocument(
618
+ getErrorDigest(
619
+ fallback.error
620
+ )
621
+ );
622
+ }
623
+
624
+ const props =
625
+ fallback.kind ===
626
+ "error"
627
+ ? {
628
+ error:
629
+ fallback.error,
630
+ reset:
631
+ () => undefined,
632
+ }
633
+ : {};
634
+
635
+ try {
636
+ const content =
637
+ renderToString(
638
+ wrapWithLayouts(
639
+ createElement(
640
+ Component,
641
+ props
642
+ ),
643
+ layouts,
644
+ params
645
+ )
646
+ );
647
+
648
+ return renderDocument({
649
+ content,
650
+ route: null,
651
+ params: null,
652
+ client: null,
653
+ metadata,
654
+ });
655
+ } catch (fallbackError) {
656
+ console.error(
657
+ `[BCP ${fallback.kind} fallback error]:`,
658
+ fallbackError
659
+ );
660
+
661
+ try {
662
+ const content =
663
+ renderToString(
664
+ createElement(
665
+ Component,
666
+ props
667
+ )
668
+ );
669
+
670
+ return renderDocument({
671
+ content,
672
+ route: null,
673
+ params: null,
674
+ client: null,
675
+ metadata,
676
+ });
677
+ } catch (componentError) {
678
+ console.error(
679
+ `[BCP ${fallback.kind} component error]:`,
680
+ componentError
681
+ );
682
+
683
+ return fallback.kind ===
684
+ "not-found"
685
+ ? genericNotFoundDocument()
686
+ : genericErrorDocument(
687
+ getErrorDigest(
688
+ fallback.error
689
+ )
690
+ );
691
+ }
692
+ }
693
+ }
694
+
695
+ function wrapWithLayouts(
696
+ child: ReactElement,
697
+ layouts: ComponentType<any>[],
698
+ params: Record<string, string>
699
+ ): ReactElement {
700
+ let element =
701
+ child;
702
+
703
+ for (
704
+ let index =
705
+ layouts.length - 1;
706
+ index >= 0;
707
+ index--
708
+ ) {
709
+ element =
710
+ createElement(
711
+ layouts[index],
712
+ {
713
+ params,
714
+ },
715
+ element
716
+ );
717
+ }
718
+
719
+ return element;
720
+ }
721
+
722
+ function renderDocument(
723
+ input: {
724
+ content: string;
725
+ route: string | null;
726
+ params: Record<string, string> | null;
727
+ client: ProductionRouteAsset | null;
728
+ metadata: ResolvedMetadata;
729
+ }
730
+ ): string {
731
+ const modulePreloads =
732
+ input.client
733
+ ? input.client.imports
734
+ .map(
735
+ (importPath) =>
736
+ ` <link rel="modulepreload" href="${escapeHtmlAttribute(
737
+ importPath
738
+ )}" />`
739
+ )
740
+ .join("\n")
741
+ : "";
742
+
743
+ const metadataTags =
744
+ renderMetadataTags(
745
+ input.metadata
746
+ );
747
+
748
+ const frameworkData =
749
+ input.route !== null &&
750
+ input.params !== null
751
+ ? serializeFrameworkData({
752
+ route:
753
+ input.route,
754
+ params:
755
+ input.params,
756
+ metadata:
757
+ input.metadata,
758
+ })
759
+ : null;
760
+
761
+ const clientRuntime =
762
+ input.client &&
763
+ frameworkData !== null
764
+ ? `
765
+ <script
766
+ id="__BCP_DATA__"
767
+ type="application/json"
768
+ >${frameworkData}</script>
769
+
770
+ <script
771
+ data-bcp-client
772
+ type="module"
773
+ src="${escapeHtmlAttribute(
774
+ input.client.entry
775
+ )}"
776
+ ></script>`
777
+ : "";
778
+
779
+ return `
780
+ <!DOCTYPE html>
781
+ <html lang="en">
782
+ <head>
783
+ <meta charset="UTF-8" />
784
+ <meta
785
+ name="viewport"
786
+ content="width=device-width, initial-scale=1.0"
787
+ />
788
+ <title>${escapeHtml(
789
+ input.metadata.title
790
+ )}</title>
791
+ ${metadataTags}${metadataTags ? "\n" : ""}${modulePreloads}
792
+ </head>
793
+ <body>
794
+ <div id="root">${input.content}</div>
795
+ ${clientRuntime}
796
+ </body>
797
+ </html>
798
+ `.trim();
799
+ }
800
+
801
+ function renderMetadataTags(
802
+ metadata: ResolvedMetadata
803
+ ): string {
804
+ return createMetadataHeadEntries(
805
+ metadata
806
+ )
807
+ .map(
808
+ (entry) => {
809
+ const attributes = {
810
+ "data-bcp-metadata":
811
+ entry.key,
812
+ ...entry.attributes,
813
+ };
814
+
815
+ const serialized =
816
+ Object.entries(
817
+ attributes
818
+ )
819
+ .map(
820
+ ([name, value]) =>
821
+ `${name}="${escapeHtmlAttribute(
822
+ value
823
+ )}"`
824
+ )
825
+ .join(" ");
826
+
827
+ return ` <${entry.tag} ${serialized} />`;
828
+ }
829
+ )
830
+ .join("\n");
831
+ }
832
+
833
+ function createFallbackMetadata(
834
+ title: string
835
+ ): ResolvedMetadata {
836
+ return {
837
+ title,
838
+ description: null,
839
+ robots:
840
+ "noindex, nofollow",
841
+ canonical: null,
842
+ openGraph: null,
843
+ icons: {
844
+ icon: null,
845
+ shortcut: null,
846
+ apple: null,
847
+ },
848
+ };
849
+ }
850
+
851
+ function createPublicServerError(
852
+ error: unknown
853
+ ): Error {
854
+ const digest =
855
+ createErrorDigest(
856
+ error
857
+ );
858
+
859
+ const publicError =
860
+ new Error(
861
+ "An unexpected server error occurred."
862
+ ) as Error & {
863
+ digest?: string;
864
+ };
865
+
866
+ publicError.name =
867
+ "BcpServerError";
868
+ publicError.digest =
869
+ digest;
870
+
871
+ return publicError;
872
+ }
873
+
874
+ function createErrorDigest(
875
+ error: unknown
876
+ ): string {
877
+ const source =
878
+ error instanceof Error
879
+ ? `${error.name}\n${error.message}\n${error.stack ?? ""}`
880
+ : String(error);
881
+
882
+ return createHash(
883
+ "sha256"
884
+ )
885
+ .update(
886
+ source
887
+ )
888
+ .digest(
889
+ "hex"
890
+ )
891
+ .slice(
892
+ 0,
893
+ 12
894
+ );
895
+ }
896
+
897
+ function getErrorDigest(
898
+ error: Error
899
+ ): string {
900
+ const value =
901
+ error as Error & {
902
+ digest?: string;
903
+ };
904
+
905
+ return value.digest ??
906
+ "unknown";
907
+ }
908
+
909
+ async function handleNavigation(
910
+ req: http.IncomingMessage,
911
+ res: http.ServerResponse,
912
+ requestUrl: URL,
913
+ routes: Route[],
914
+ routeByPathname: Map<
915
+ string,
916
+ StandalonePageRoute
917
+ >,
918
+ version: number
919
+ ): Promise<void> {
920
+ if (req.method !== "GET") {
921
+ sendJson(
922
+ req,
923
+ res,
924
+ 405,
925
+ {
926
+ error:
927
+ "Method Not Allowed",
928
+ }
929
+ );
930
+ return;
931
+ }
932
+
933
+ const targetPath =
934
+ requestUrl.searchParams.get(
935
+ "url"
936
+ );
937
+
938
+ if (!targetPath) {
939
+ sendJson(
940
+ req,
941
+ res,
942
+ 400,
943
+ {
944
+ error:
945
+ "Missing navigation target.",
946
+ }
947
+ );
948
+ return;
949
+ }
950
+
951
+ let target: URL;
952
+
953
+ try {
954
+ target =
955
+ new URL(
956
+ targetPath,
957
+ requestUrl.origin
958
+ );
959
+ } catch {
960
+ sendJson(
961
+ req,
962
+ res,
963
+ 400,
964
+ {
965
+ error:
966
+ "Invalid navigation target.",
967
+ }
968
+ );
969
+ return;
970
+ }
971
+
972
+ if (
973
+ target.origin !==
974
+ requestUrl.origin
975
+ ) {
976
+ sendJson(
977
+ req,
978
+ res,
979
+ 400,
980
+ {
981
+ error:
982
+ "Cross-origin navigation is not allowed.",
983
+ }
984
+ );
985
+ return;
986
+ }
987
+
988
+ const match =
989
+ matchRoute(
990
+ routes,
991
+ normalizePathname(
992
+ target.pathname
993
+ )
994
+ );
995
+
996
+ if (!match) {
997
+ sendJson(
998
+ req,
999
+ res,
1000
+ 404,
1001
+ {
1002
+ error:
1003
+ "Route not found.",
1004
+ }
1005
+ );
1006
+ return;
1007
+ }
1008
+
1009
+ const definition =
1010
+ routeByPathname.get(
1011
+ match.route.pathname
1012
+ );
1013
+
1014
+ if (!definition) {
1015
+ sendJson(
1016
+ req,
1017
+ res,
1018
+ 404,
1019
+ {
1020
+ error:
1021
+ "Route not found.",
1022
+ }
1023
+ );
1024
+ return;
1025
+ }
1026
+
1027
+ let metadata:
1028
+ ResolvedMetadata;
1029
+
1030
+ try {
1031
+ metadata =
1032
+ await resolveRouteMetadata(
1033
+ definition.metadataSources,
1034
+ match.params
1035
+ );
1036
+ } catch (error) {
1037
+ if (isNotFoundError(error)) {
1038
+ sendJson(
1039
+ req,
1040
+ res,
1041
+ 404,
1042
+ {
1043
+ error:
1044
+ "Route not found.",
1045
+ }
1046
+ );
1047
+ return;
1048
+ }
1049
+
1050
+ console.error(
1051
+ `[BCP Metadata Error] ${definition.pathname}:`,
1052
+ error
1053
+ );
1054
+
1055
+ sendJson(
1056
+ req,
1057
+ res,
1058
+ 500,
1059
+ {
1060
+ error:
1061
+ "Internal Server Error",
1062
+ }
1063
+ );
1064
+ return;
1065
+ }
1066
+
1067
+ const loadingHtml =
1068
+ definition.loading
1069
+ ? renderToStaticMarkup(
1070
+ createElement(
1071
+ definition.loading,
1072
+ {
1073
+ params:
1074
+ match.params,
1075
+ }
1076
+ )
1077
+ )
1078
+ : null;
1079
+
1080
+ sendJson(
1081
+ req,
1082
+ res,
1083
+ 200,
1084
+ {
1085
+ route:
1086
+ definition.pathname,
1087
+ params:
1088
+ match.params,
1089
+ clientScript:
1090
+ definition.client.entry,
1091
+ clientImports:
1092
+ definition.client.imports,
1093
+ title:
1094
+ metadata.title,
1095
+ metadata,
1096
+ version,
1097
+ loadingHtml,
1098
+ }
1099
+ );
1100
+ }
1101
+
1102
+ async function serveBuildAsset(
1103
+ req: http.IncomingMessage,
1104
+ res: http.ServerResponse,
1105
+ clientDirectory: string,
1106
+ pathname: string
1107
+ ): Promise<boolean> {
1108
+ const encodedRelative =
1109
+ pathname.slice(
1110
+ "/_bcp/client/".length
1111
+ );
1112
+
1113
+ let relativePath: string;
1114
+
1115
+ try {
1116
+ relativePath =
1117
+ decodeURIComponent(
1118
+ encodedRelative
1119
+ );
1120
+ } catch {
1121
+ return false;
1122
+ }
1123
+
1124
+ if (
1125
+ !relativePath ||
1126
+ relativePath.includes(
1127
+ "\0"
1128
+ )
1129
+ ) {
1130
+ return false;
1131
+ }
1132
+
1133
+ const root =
1134
+ path.resolve(
1135
+ clientDirectory
1136
+ );
1137
+ const candidate =
1138
+ path.resolve(
1139
+ root,
1140
+ relativePath.replaceAll(
1141
+ "/",
1142
+ path.sep
1143
+ )
1144
+ );
1145
+
1146
+ if (!isPathInside(root, candidate)) {
1147
+ return false;
1148
+ }
1149
+
1150
+ let stat:
1151
+ Awaited<
1152
+ ReturnType<typeof fs.stat>
1153
+ >;
1154
+
1155
+ try {
1156
+ stat =
1157
+ await fs.stat(
1158
+ candidate
1159
+ );
1160
+ } catch {
1161
+ return false;
1162
+ }
1163
+
1164
+ if (!stat.isFile()) {
1165
+ return false;
1166
+ }
1167
+
1168
+ const contentType =
1169
+ getContentType(
1170
+ candidate
1171
+ );
1172
+
1173
+ const encoding =
1174
+ selectContentEncoding(
1175
+ req,
1176
+ contentType,
1177
+ stat.size,
1178
+ {
1179
+ br:
1180
+ await fileExists(
1181
+ `${candidate}.br`
1182
+ ),
1183
+ gzip:
1184
+ await fileExists(
1185
+ `${candidate}.gz`
1186
+ ),
1187
+ }
1188
+ );
1189
+
1190
+ const sourceFile =
1191
+ encoding === "br"
1192
+ ? `${candidate}.br`
1193
+ : encoding === "gzip"
1194
+ ? `${candidate}.gz`
1195
+ : candidate;
1196
+
1197
+ const content =
1198
+ await fs.readFile(
1199
+ sourceFile
1200
+ );
1201
+
1202
+ const headers:
1203
+ Record<string, string | number> = {
1204
+ "Content-Type":
1205
+ contentType,
1206
+ "Cache-Control":
1207
+ "public, max-age=31536000, immutable",
1208
+ "X-Content-Type-Options":
1209
+ "nosniff",
1210
+ "Content-Length":
1211
+ content.length,
1212
+ Vary:
1213
+ "Accept-Encoding",
1214
+ };
1215
+
1216
+ if (encoding) {
1217
+ headers[
1218
+ "Content-Encoding"
1219
+ ] =
1220
+ encoding;
1221
+ }
1222
+
1223
+ res.writeHead(
1224
+ 200,
1225
+ headers
1226
+ );
1227
+
1228
+ if (req.method === "HEAD") {
1229
+ res.end();
1230
+ return true;
1231
+ }
1232
+
1233
+ res.end(
1234
+ content
1235
+ );
1236
+ return true;
1237
+ }
1238
+
1239
+ async function fileExists(
1240
+ filePath: string
1241
+ ): Promise<boolean> {
1242
+ try {
1243
+ const stat =
1244
+ await fs.stat(
1245
+ filePath
1246
+ );
1247
+ return stat.isFile();
1248
+ } catch {
1249
+ return false;
1250
+ }
1251
+ }
1252
+
1253
+ function sendPublicAsset(
1254
+ req: http.IncomingMessage,
1255
+ res: http.ServerResponse,
1256
+ asset: StaticAsset
1257
+ ): void {
1258
+ const encoding =
1259
+ selectContentEncoding(
1260
+ req,
1261
+ asset.contentType,
1262
+ asset.content.length
1263
+ );
1264
+
1265
+ const content =
1266
+ compressDynamicContent(
1267
+ asset.content,
1268
+ encoding
1269
+ );
1270
+
1271
+ const etag =
1272
+ createVariantEtag(
1273
+ asset.etag,
1274
+ encoding
1275
+ );
1276
+
1277
+ const ifNoneMatch =
1278
+ req.headers[
1279
+ "if-none-match"
1280
+ ];
1281
+
1282
+ const values =
1283
+ Array.isArray(
1284
+ ifNoneMatch
1285
+ )
1286
+ ? ifNoneMatch
1287
+ : ifNoneMatch
1288
+ ? ifNoneMatch.split(",")
1289
+ : [];
1290
+
1291
+ if (
1292
+ values
1293
+ .map(
1294
+ (value) =>
1295
+ value.trim()
1296
+ )
1297
+ .includes(
1298
+ etag
1299
+ )
1300
+ ) {
1301
+ res.writeHead(
1302
+ 304,
1303
+ {
1304
+ ETag:
1305
+ etag,
1306
+ "Cache-Control":
1307
+ "public, max-age=0, must-revalidate",
1308
+ Vary:
1309
+ "Accept-Encoding",
1310
+ }
1311
+ );
1312
+ res.end();
1313
+ return;
1314
+ }
1315
+
1316
+ const headers:
1317
+ Record<string, string | number> = {
1318
+ "Content-Type":
1319
+ asset.contentType,
1320
+ "Content-Length":
1321
+ content.length,
1322
+ ETag:
1323
+ etag,
1324
+ "Last-Modified":
1325
+ asset.lastModified,
1326
+ "Cache-Control":
1327
+ "public, max-age=0, must-revalidate",
1328
+ "X-Content-Type-Options":
1329
+ "nosniff",
1330
+ };
1331
+
1332
+ if (
1333
+ isCompressibleContentType(
1334
+ asset.contentType
1335
+ )
1336
+ ) {
1337
+ headers.Vary =
1338
+ "Accept-Encoding";
1339
+ }
1340
+
1341
+ if (encoding) {
1342
+ headers[
1343
+ "Content-Encoding"
1344
+ ] =
1345
+ encoding;
1346
+ }
1347
+
1348
+ res.writeHead(
1349
+ 200,
1350
+ headers
1351
+ );
1352
+
1353
+ if (req.method === "HEAD") {
1354
+ res.end();
1355
+ return;
1356
+ }
1357
+
1358
+ res.end(
1359
+ content
1360
+ );
1361
+ }
1362
+
1363
+ async function handleApiRoute(
1364
+ req: http.IncomingMessage,
1365
+ res: http.ServerResponse,
1366
+ requestUrl: URL,
1367
+ definition: StandaloneApiRoute,
1368
+ params: Record<string, string>
1369
+ ): Promise<void> {
1370
+ const method =
1371
+ normalizeHttpMethod(
1372
+ req.method
1373
+ );
1374
+
1375
+ const allowedMethods =
1376
+ getAllowedMethods(
1377
+ definition.handlers
1378
+ );
1379
+
1380
+ if (
1381
+ method === "OPTIONS" &&
1382
+ !getApiHandler(
1383
+ definition.handlers,
1384
+ "OPTIONS"
1385
+ )
1386
+ ) {
1387
+ res.writeHead(
1388
+ 204,
1389
+ {
1390
+ Allow:
1391
+ allowedMethods.join(
1392
+ ", "
1393
+ ),
1394
+ }
1395
+ );
1396
+ res.end();
1397
+ return;
1398
+ }
1399
+
1400
+ let handler =
1401
+ getApiHandler(
1402
+ definition.handlers,
1403
+ method
1404
+ );
1405
+
1406
+ if (
1407
+ !handler &&
1408
+ method === "HEAD"
1409
+ ) {
1410
+ handler =
1411
+ getApiHandler(
1412
+ definition.handlers,
1413
+ "GET"
1414
+ );
1415
+ }
1416
+
1417
+ if (!handler) {
1418
+ sendJson(
1419
+ req,
1420
+ res,
1421
+ 405,
1422
+ {
1423
+ error:
1424
+ "Method Not Allowed",
1425
+ allowedMethods,
1426
+ },
1427
+ {
1428
+ Allow:
1429
+ allowedMethods.join(
1430
+ ", "
1431
+ ),
1432
+ }
1433
+ );
1434
+ return;
1435
+ }
1436
+
1437
+ const response =
1438
+ await handler(
1439
+ await createWebRequest(
1440
+ req,
1441
+ requestUrl
1442
+ ),
1443
+ {
1444
+ params,
1445
+ }
1446
+ );
1447
+
1448
+ if (!(response instanceof Response)) {
1449
+ throw new Error(
1450
+ `API handler "${method} ${definition.pathname}" must return a Response object.`
1451
+ );
1452
+ }
1453
+
1454
+ await sendWebResponse(
1455
+ req,
1456
+ res,
1457
+ response,
1458
+ method === "HEAD"
1459
+ );
1460
+ }
1461
+
1462
+ function normalizeHttpMethod(
1463
+ method: string | undefined
1464
+ ): HttpMethod {
1465
+ return (
1466
+ (
1467
+ method ??
1468
+ "GET"
1469
+ ).toUpperCase() as
1470
+ HttpMethod
1471
+ );
1472
+ }
1473
+
1474
+ function getApiHandler(
1475
+ module: Record<string, unknown>,
1476
+ method: string
1477
+ ): ApiHandler | undefined {
1478
+ const value =
1479
+ module[
1480
+ method
1481
+ ];
1482
+
1483
+ return typeof value ===
1484
+ "function"
1485
+ ? value as ApiHandler
1486
+ : undefined;
1487
+ }
1488
+
1489
+ function getAllowedMethods(
1490
+ module: Record<string, unknown>
1491
+ ): string[] {
1492
+ const allowed:
1493
+ string[] = [];
1494
+
1495
+ for (const method of HTTP_METHODS) {
1496
+ if (method === "OPTIONS") {
1497
+ continue;
1498
+ }
1499
+
1500
+ if (method === "HEAD") {
1501
+ if (
1502
+ getApiHandler(
1503
+ module,
1504
+ "HEAD"
1505
+ ) ||
1506
+ getApiHandler(
1507
+ module,
1508
+ "GET"
1509
+ )
1510
+ ) {
1511
+ allowed.push(
1512
+ "HEAD"
1513
+ );
1514
+ }
1515
+ continue;
1516
+ }
1517
+
1518
+ if (
1519
+ getApiHandler(
1520
+ module,
1521
+ method
1522
+ )
1523
+ ) {
1524
+ allowed.push(
1525
+ method
1526
+ );
1527
+ }
1528
+ }
1529
+
1530
+ allowed.push(
1531
+ "OPTIONS"
1532
+ );
1533
+ return allowed;
1534
+ }
1535
+
1536
+ async function createWebRequest(
1537
+ req: http.IncomingMessage,
1538
+ url: URL
1539
+ ): Promise<Request> {
1540
+ const headers =
1541
+ new Headers();
1542
+
1543
+ for (
1544
+ const [key, value]
1545
+ of Object.entries(
1546
+ req.headers
1547
+ )
1548
+ ) {
1549
+ if (value === undefined) {
1550
+ continue;
1551
+ }
1552
+
1553
+ if (Array.isArray(value)) {
1554
+ for (const item of value) {
1555
+ headers.append(
1556
+ key,
1557
+ item
1558
+ );
1559
+ }
1560
+ } else {
1561
+ headers.set(
1562
+ key,
1563
+ value
1564
+ );
1565
+ }
1566
+ }
1567
+
1568
+ const method =
1569
+ (
1570
+ req.method ??
1571
+ "GET"
1572
+ ).toUpperCase();
1573
+
1574
+ const init:
1575
+ RequestInit = {
1576
+ method,
1577
+ headers,
1578
+ };
1579
+
1580
+ if (
1581
+ method !== "GET" &&
1582
+ method !== "HEAD"
1583
+ ) {
1584
+ const body =
1585
+ await readRequestBody(
1586
+ req
1587
+ );
1588
+
1589
+ if (body.length > 0) {
1590
+ init.body =
1591
+ body as unknown as
1592
+ BodyInit;
1593
+ }
1594
+ }
1595
+
1596
+ return new Request(
1597
+ url,
1598
+ init
1599
+ );
1600
+ }
1601
+
1602
+ async function readRequestBody(
1603
+ req: http.IncomingMessage
1604
+ ): Promise<Buffer> {
1605
+ const chunks:
1606
+ Buffer[] = [];
1607
+
1608
+ for await (const chunk of req) {
1609
+ chunks.push(
1610
+ Buffer.isBuffer(
1611
+ chunk
1612
+ )
1613
+ ? chunk
1614
+ : Buffer.from(
1615
+ chunk
1616
+ )
1617
+ );
1618
+ }
1619
+
1620
+ return Buffer.concat(
1621
+ chunks
1622
+ );
1623
+ }
1624
+
1625
+ async function sendWebResponse(
1626
+ req: http.IncomingMessage,
1627
+ res: http.ServerResponse,
1628
+ response: Response,
1629
+ isHead: boolean
1630
+ ): Promise<void> {
1631
+ res.statusCode =
1632
+ response.status;
1633
+
1634
+ response.headers.forEach(
1635
+ (
1636
+ value,
1637
+ key
1638
+ ) => {
1639
+ const normalized =
1640
+ key.toLowerCase();
1641
+
1642
+ if (
1643
+ normalized ===
1644
+ "set-cookie" ||
1645
+ normalized ===
1646
+ "content-length"
1647
+ ) {
1648
+ return;
1649
+ }
1650
+
1651
+ res.setHeader(
1652
+ key,
1653
+ value
1654
+ );
1655
+ }
1656
+ );
1657
+
1658
+ const responseHeaders =
1659
+ response.headers as
1660
+ Headers & {
1661
+ getSetCookie?: () =>
1662
+ string[];
1663
+ };
1664
+
1665
+ const cookies =
1666
+ responseHeaders
1667
+ .getSetCookie?.();
1668
+
1669
+ if (
1670
+ cookies &&
1671
+ cookies.length > 0
1672
+ ) {
1673
+ res.setHeader(
1674
+ "Set-Cookie",
1675
+ cookies
1676
+ );
1677
+ }
1678
+
1679
+ if (
1680
+ response.status === 204 ||
1681
+ response.status === 304
1682
+ ) {
1683
+ res.end();
1684
+ return;
1685
+ }
1686
+
1687
+ const rawBody =
1688
+ Buffer.from(
1689
+ await response.arrayBuffer()
1690
+ );
1691
+
1692
+ const contentType =
1693
+ response.headers.get(
1694
+ "content-type"
1695
+ ) ??
1696
+ "application/octet-stream";
1697
+
1698
+ const existingEncoding =
1699
+ response.headers.get(
1700
+ "content-encoding"
1701
+ );
1702
+
1703
+ const encoding =
1704
+ existingEncoding
1705
+ ? null
1706
+ : selectContentEncoding(
1707
+ req,
1708
+ contentType,
1709
+ rawBody.length
1710
+ );
1711
+
1712
+ const body =
1713
+ existingEncoding
1714
+ ? rawBody
1715
+ : compressDynamicContent(
1716
+ rawBody,
1717
+ encoding
1718
+ );
1719
+
1720
+ if (encoding) {
1721
+ res.setHeader(
1722
+ "Content-Encoding",
1723
+ encoding
1724
+ );
1725
+ res.setHeader(
1726
+ "Vary",
1727
+ appendVary(
1728
+ res.getHeader(
1729
+ "Vary"
1730
+ )?.toString(),
1731
+ "Accept-Encoding"
1732
+ )
1733
+ );
1734
+ }
1735
+
1736
+ res.setHeader(
1737
+ "Content-Length",
1738
+ body.length
1739
+ );
1740
+
1741
+ if (isHead) {
1742
+ res.end();
1743
+ return;
1744
+ }
1745
+
1746
+ res.end(
1747
+ body
1748
+ );
1749
+ }
1750
+
1751
+ function sendJson(
1752
+ req: http.IncomingMessage,
1753
+ res: http.ServerResponse,
1754
+ statusCode: number,
1755
+ value: unknown,
1756
+ extraHeaders: Record<string, string> = {}
1757
+ ): void {
1758
+ sendCompressedBody(
1759
+ req,
1760
+ res,
1761
+ statusCode,
1762
+ "application/json; charset=utf-8",
1763
+ Buffer.from(
1764
+ JSON.stringify(
1765
+ value
1766
+ )
1767
+ ),
1768
+ {
1769
+ "Cache-Control":
1770
+ "no-store",
1771
+ "X-Content-Type-Options":
1772
+ "nosniff",
1773
+ ...extraHeaders,
1774
+ }
1775
+ );
1776
+ }
1777
+
1778
+ function sendHtml(
1779
+ req: http.IncomingMessage,
1780
+ res: http.ServerResponse,
1781
+ statusCode: number,
1782
+ html: string
1783
+ ): void {
1784
+ sendCompressedBody(
1785
+ req,
1786
+ res,
1787
+ statusCode,
1788
+ "text/html; charset=utf-8",
1789
+ Buffer.from(
1790
+ html
1791
+ ),
1792
+ {
1793
+ "Cache-Control":
1794
+ "no-store",
1795
+ }
1796
+ );
1797
+ }
1798
+
1799
+ function sendCompressedBody(
1800
+ req: http.IncomingMessage,
1801
+ res: http.ServerResponse,
1802
+ statusCode: number,
1803
+ contentType: string,
1804
+ rawBody: Buffer,
1805
+ extraHeaders: Record<string, string> = {}
1806
+ ): void {
1807
+ const encoding =
1808
+ selectContentEncoding(
1809
+ req,
1810
+ contentType,
1811
+ rawBody.length
1812
+ );
1813
+
1814
+ const body =
1815
+ compressDynamicContent(
1816
+ rawBody,
1817
+ encoding
1818
+ );
1819
+
1820
+ const headers:
1821
+ Record<string, string | number> = {
1822
+ "Content-Type":
1823
+ contentType,
1824
+ "Content-Length":
1825
+ body.length,
1826
+ ...extraHeaders,
1827
+ };
1828
+
1829
+ if (
1830
+ isCompressibleContentType(
1831
+ contentType
1832
+ )
1833
+ ) {
1834
+ headers.Vary =
1835
+ appendVary(
1836
+ typeof headers.Vary ===
1837
+ "string"
1838
+ ? headers.Vary
1839
+ : undefined,
1840
+ "Accept-Encoding"
1841
+ );
1842
+ }
1843
+
1844
+ if (encoding) {
1845
+ headers[
1846
+ "Content-Encoding"
1847
+ ] =
1848
+ encoding;
1849
+ }
1850
+
1851
+ res.writeHead(
1852
+ statusCode,
1853
+ headers
1854
+ );
1855
+
1856
+ if (req.method === "HEAD") {
1857
+ res.end();
1858
+ return;
1859
+ }
1860
+
1861
+ res.end(
1862
+ body
1863
+ );
1864
+ }
1865
+
1866
+ function sendSimpleNotFound(
1867
+ req: http.IncomingMessage,
1868
+ res: http.ServerResponse
1869
+ ): void {
1870
+ sendHtml(
1871
+ req,
1872
+ res,
1873
+ 404,
1874
+ genericNotFoundDocument()
1875
+ );
1876
+ }
1877
+
1878
+ function sendInternalServerError(
1879
+ req: http.IncomingMessage,
1880
+ res: http.ServerResponse,
1881
+ error: unknown
1882
+ ): void {
1883
+ sendHtml(
1884
+ req,
1885
+ res,
1886
+ 500,
1887
+ genericErrorDocument(
1888
+ createErrorDigest(
1889
+ error
1890
+ )
1891
+ )
1892
+ );
1893
+ }
1894
+
1895
+ function genericNotFoundDocument(): string {
1896
+ return renderDocument({
1897
+ content:
1898
+ "<main><h1>404</h1><p>Page Not Found</p></main>",
1899
+ route: null,
1900
+ params: null,
1901
+ client: null,
1902
+ metadata:
1903
+ createFallbackMetadata(
1904
+ "Page Not Found"
1905
+ ),
1906
+ });
1907
+ }
1908
+
1909
+ function genericErrorDocument(
1910
+ digest: string
1911
+ ): string {
1912
+ return renderDocument({
1913
+ content:
1914
+ `<main><h1>500</h1><p>Internal Server Error</p><p>Error ID: ${escapeHtml(
1915
+ digest
1916
+ )}</p></main>`,
1917
+ route: null,
1918
+ params: null,
1919
+ client: null,
1920
+ metadata:
1921
+ createFallbackMetadata(
1922
+ "Internal Server Error"
1923
+ ),
1924
+ });
1925
+ }
1926
+
1927
+ function createVariantEtag(
1928
+ etag: string,
1929
+ encoding: "br" | "gzip" | null
1930
+ ): string {
1931
+ if (!encoding) {
1932
+ return etag;
1933
+ }
1934
+
1935
+ const value =
1936
+ etag.replace(
1937
+ /^W\//,
1938
+ ""
1939
+ );
1940
+
1941
+ const unquoted =
1942
+ value.startsWith('"') &&
1943
+ value.endsWith('"')
1944
+ ? value.slice(
1945
+ 1,
1946
+ -1
1947
+ )
1948
+ : value;
1949
+
1950
+ return `"${unquoted}-${encoding}"`;
1951
+ }
1952
+
1953
+ function serializeFrameworkData(
1954
+ value: unknown
1955
+ ): string {
1956
+ return JSON.stringify(
1957
+ value
1958
+ )
1959
+ .replace(
1960
+ /</g,
1961
+ "\\u003c"
1962
+ )
1963
+ .replace(
1964
+ /\u2028/g,
1965
+ "\\u2028"
1966
+ )
1967
+ .replace(
1968
+ /\u2029/g,
1969
+ "\\u2029"
1970
+ );
1971
+ }
1972
+
1973
+ function normalizePathname(
1974
+ pathname: string
1975
+ ): string {
1976
+ if (
1977
+ pathname.length > 1 &&
1978
+ pathname.endsWith(
1979
+ "/"
1980
+ )
1981
+ ) {
1982
+ return pathname.slice(
1983
+ 0,
1984
+ -1
1985
+ );
1986
+ }
1987
+
1988
+ return pathname || "/";
1989
+ }
1990
+
1991
+ function isPathInside(
1992
+ root: string,
1993
+ candidate: string
1994
+ ): boolean {
1995
+ const relative =
1996
+ path.relative(
1997
+ root,
1998
+ candidate
1999
+ );
2000
+
2001
+ return (
2002
+ relative === "" ||
2003
+ (
2004
+ relative !== ".." &&
2005
+ !relative.startsWith(
2006
+ `..${path.sep}`
2007
+ ) &&
2008
+ !path.isAbsolute(
2009
+ relative
2010
+ )
2011
+ )
2012
+ );
2013
+ }
2014
+
2015
+ function escapeHtml(
2016
+ value: string
2017
+ ): string {
2018
+ return value
2019
+ .replaceAll(
2020
+ "&",
2021
+ "&amp;"
2022
+ )
2023
+ .replaceAll(
2024
+ "<",
2025
+ "&lt;"
2026
+ )
2027
+ .replaceAll(
2028
+ ">",
2029
+ "&gt;"
2030
+ )
2031
+ .replaceAll(
2032
+ '"',
2033
+ "&quot;"
2034
+ )
2035
+ .replaceAll(
2036
+ "'",
2037
+ "&#039;"
2038
+ );
2039
+ }
2040
+
2041
+ function escapeHtmlAttribute(
2042
+ value: string
2043
+ ): string {
2044
+ return escapeHtml(
2045
+ value
2046
+ );
2047
+ }