@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,1746 @@
1
+ import fs from "node:fs/promises";
2
+ import * as http from "node:http";
3
+ import path from "node:path";
4
+ import {
5
+ pathToFileURL,
6
+ } from "node:url";
7
+
8
+ import {
9
+ createElement,
10
+ type ComponentType,
11
+ type ReactElement,
12
+ } from "react";
13
+
14
+ import {
15
+ renderToString,
16
+ } from "react-dom/server";
17
+
18
+ import type {
19
+ ProductionBuildManifest,
20
+ ProductionRouteAsset,
21
+ } from "../../bundler/src/production.js";
22
+
23
+ import {
24
+ assertNoPageApiConflicts,
25
+ matchApiRoute,
26
+ matchRoute,
27
+ scanApiRoutes,
28
+ scanRoutes,
29
+ type ApiRoute,
30
+ } from "../../router/src/index.js";
31
+
32
+ import {
33
+ compressDynamicContent,
34
+ isCompressibleContentType,
35
+ selectContentEncoding,
36
+ type ContentEncoding,
37
+ } from "./compression.js";
38
+
39
+ import {
40
+ resolveNavigationPayload,
41
+ sendNavigationError,
42
+ sendNavigationPayload,
43
+ } from "./navigation-payload.js";
44
+
45
+ import {
46
+ getContentType,
47
+ readPublicAsset,
48
+ type StaticAsset,
49
+ } from "./static-assets.js";
50
+
51
+ export interface ProductionServerOptions {
52
+ port?: number;
53
+ hostname?: string;
54
+ rootDirectory?: string;
55
+ }
56
+
57
+ interface FrameworkData {
58
+ route: string;
59
+ params: Record<string, string>;
60
+ }
61
+
62
+ interface ApiContext {
63
+ params: Record<string, string>;
64
+ }
65
+
66
+ type ApiHandler = (
67
+ request: Request,
68
+ context: ApiContext
69
+ ) => Response | Promise<Response>;
70
+
71
+ const HTTP_METHODS = [
72
+ "GET",
73
+ "POST",
74
+ "PUT",
75
+ "PATCH",
76
+ "DELETE",
77
+ "HEAD",
78
+ "OPTIONS",
79
+ ] as const;
80
+
81
+ type HttpMethod =
82
+ typeof HTTP_METHODS[number];
83
+
84
+ export function createProductionServer(
85
+ options: ProductionServerOptions = {}
86
+ ) {
87
+ const port =
88
+ options.port ??
89
+ 3000;
90
+
91
+ const hostname =
92
+ options.hostname ??
93
+ "localhost";
94
+
95
+ const rootDirectory =
96
+ path.resolve(
97
+ options.rootDirectory ??
98
+ process.cwd()
99
+ );
100
+
101
+ const appDirectory =
102
+ path.join(
103
+ rootDirectory,
104
+ "app"
105
+ );
106
+
107
+ const buildDirectory =
108
+ path.join(
109
+ rootDirectory,
110
+ ".bcp-framework",
111
+ "build"
112
+ );
113
+
114
+ const clientDirectory =
115
+ path.join(
116
+ buildDirectory,
117
+ "client"
118
+ );
119
+
120
+ const manifestPath =
121
+ path.join(
122
+ buildDirectory,
123
+ "manifest.json"
124
+ );
125
+
126
+ let manifest:
127
+ ProductionBuildManifest | null =
128
+ null;
129
+
130
+ const routes =
131
+ scanRoutes(
132
+ appDirectory
133
+ );
134
+
135
+ const apiRoutes =
136
+ scanApiRoutes(
137
+ appDirectory
138
+ );
139
+
140
+ assertNoPageApiConflicts(
141
+ routes,
142
+ apiRoutes
143
+ );
144
+
145
+ const server =
146
+ http.createServer(
147
+ async (
148
+ req,
149
+ res
150
+ ) => {
151
+ const startedAt =
152
+ performance.now();
153
+
154
+ let pathname =
155
+ "/";
156
+
157
+ try {
158
+ const requestUrl =
159
+ new URL(
160
+ req.url ?? "/",
161
+ `http://${
162
+ req.headers.host ??
163
+ `${hostname}:${port}`
164
+ }`
165
+ );
166
+
167
+ pathname =
168
+ normalizePathname(
169
+ requestUrl.pathname
170
+ );
171
+
172
+ res.once(
173
+ "finish",
174
+ () => {
175
+ const duration =
176
+ performance.now() -
177
+ startedAt;
178
+
179
+ console.log(
180
+ `${req.method ?? "GET"} ${pathname} ${res.statusCode} ${duration.toFixed(2)}ms`
181
+ );
182
+ }
183
+ );
184
+
185
+ if (
186
+ pathname ===
187
+ "/_bcp/navigation"
188
+ ) {
189
+ await handleNavigationRequest(
190
+ req,
191
+ res,
192
+ requestUrl,
193
+ rootDirectory,
194
+ requireManifest(
195
+ manifest
196
+ )
197
+ );
198
+
199
+ return;
200
+ }
201
+
202
+ if (
203
+ pathname ===
204
+ "/_bcp/dev-runtime.js"
205
+ ) {
206
+ sendJavaScript(
207
+ res,
208
+ Buffer.from(
209
+ "/* BCP production: HMR disabled */"
210
+ ),
211
+ false
212
+ );
213
+
214
+ return;
215
+ }
216
+
217
+ if (
218
+ pathname.startsWith(
219
+ "/_bcp/client/"
220
+ )
221
+ ) {
222
+ const served =
223
+ await serveBuildAsset(
224
+ req,
225
+ res,
226
+ clientDirectory,
227
+ pathname
228
+ );
229
+
230
+ if (!served) {
231
+ sendNotFound(
232
+ res
233
+ );
234
+ }
235
+
236
+ return;
237
+ }
238
+
239
+ const apiMatch =
240
+ matchApiRoute(
241
+ apiRoutes,
242
+ pathname
243
+ );
244
+
245
+ if (apiMatch) {
246
+ await handleApiRoute(
247
+ req,
248
+ res,
249
+ requestUrl,
250
+ apiMatch.route,
251
+ apiMatch.params,
252
+ requireManifest(
253
+ manifest
254
+ ).buildId
255
+ );
256
+
257
+ return;
258
+ }
259
+
260
+ const publicAsset =
261
+ await readPublicAsset(
262
+ rootDirectory,
263
+ pathname
264
+ );
265
+
266
+ if (publicAsset) {
267
+ sendPublicAsset(
268
+ req,
269
+ res,
270
+ publicAsset
271
+ );
272
+
273
+ return;
274
+ }
275
+
276
+ const match =
277
+ matchRoute(
278
+ routes,
279
+ pathname
280
+ );
281
+
282
+ if (!match) {
283
+ sendNotFound(
284
+ res
285
+ );
286
+
287
+ return;
288
+ }
289
+
290
+ const activeManifest =
291
+ requireManifest(
292
+ manifest
293
+ );
294
+
295
+ const routeAsset =
296
+ activeManifest.routes[
297
+ match.route.pathname
298
+ ];
299
+
300
+ if (!routeAsset) {
301
+ throw new Error(
302
+ `BCP Framework: production bundle was not found for route "${match.route.pathname}". Run bcp build again.`
303
+ );
304
+ }
305
+
306
+ const Page =
307
+ await importComponent(
308
+ match.route.filePath,
309
+ "Page",
310
+ activeManifest.buildId
311
+ );
312
+
313
+ const Layouts:
314
+ ComponentType<any>[] =
315
+ [];
316
+
317
+ for (
318
+ const layoutFile
319
+ of match.route.layouts
320
+ ) {
321
+ Layouts.push(
322
+ await importComponent(
323
+ layoutFile,
324
+ "Layout",
325
+ activeManifest.buildId
326
+ )
327
+ );
328
+ }
329
+
330
+ let element:
331
+ ReactElement =
332
+ createElement(
333
+ Page,
334
+ {
335
+ params:
336
+ match.params,
337
+ }
338
+ );
339
+
340
+ for (
341
+ let index =
342
+ Layouts.length - 1;
343
+ index >= 0;
344
+ index--
345
+ ) {
346
+ element =
347
+ createElement(
348
+ Layouts[index],
349
+ {
350
+ params:
351
+ match.params,
352
+ },
353
+ element
354
+ );
355
+ }
356
+
357
+ const content =
358
+ renderToString(
359
+ element
360
+ );
361
+
362
+ const frameworkData:
363
+ FrameworkData = {
364
+ route:
365
+ match.route.pathname,
366
+ params:
367
+ match.params,
368
+ };
369
+
370
+ sendHtml(
371
+ req,
372
+ res,
373
+ renderDocument(
374
+ content,
375
+ routeAsset,
376
+ frameworkData
377
+ )
378
+ );
379
+ } catch (error) {
380
+ console.error(
381
+ "BCP Production Error:",
382
+ error
383
+ );
384
+
385
+ if (
386
+ res.headersSent
387
+ ) {
388
+ res.end();
389
+ return;
390
+ }
391
+
392
+ sendInternalServerError(
393
+ res,
394
+ error
395
+ );
396
+ }
397
+ }
398
+ );
399
+
400
+ return {
401
+ async start() {
402
+ process.env.NODE_ENV =
403
+ "production";
404
+
405
+ manifest =
406
+ await readManifest(
407
+ manifestPath
408
+ );
409
+
410
+ await new Promise<void>(
411
+ (
412
+ resolve,
413
+ reject
414
+ ) => {
415
+ const onError =
416
+ (error: Error) => {
417
+ server.off(
418
+ "listening",
419
+ onListening
420
+ );
421
+ reject(error);
422
+ };
423
+
424
+ const onListening =
425
+ () => {
426
+ server.off(
427
+ "error",
428
+ onError
429
+ );
430
+ resolve();
431
+ };
432
+
433
+ server.once(
434
+ "error",
435
+ onError
436
+ );
437
+ server.once(
438
+ "listening",
439
+ onListening
440
+ );
441
+ server.listen(
442
+ port,
443
+ hostname
444
+ );
445
+ }
446
+ );
447
+
448
+ console.log("");
449
+ console.log(
450
+ "BCP Framework Production"
451
+ );
452
+ console.log(
453
+ `Local: http://${hostname}:${port}`
454
+ );
455
+ console.log(
456
+ `Build: ${manifest.buildId}`
457
+ );
458
+ console.log("");
459
+ },
460
+
461
+ async stop() {
462
+ if (!server.listening) {
463
+ return;
464
+ }
465
+
466
+ await new Promise<void>(
467
+ (
468
+ resolve,
469
+ reject
470
+ ) => {
471
+ server.close(
472
+ (error) => {
473
+ if (error) {
474
+ reject(error);
475
+ return;
476
+ }
477
+
478
+ resolve();
479
+ }
480
+ );
481
+ }
482
+ );
483
+ },
484
+ };
485
+ }
486
+
487
+ async function readManifest(
488
+ manifestPath: string
489
+ ): Promise<ProductionBuildManifest> {
490
+ let content: string;
491
+
492
+ try {
493
+ content =
494
+ await fs.readFile(
495
+ manifestPath,
496
+ "utf8"
497
+ );
498
+ } catch {
499
+ throw new Error(
500
+ "BCP Framework: production build was not found. Run `bcp build` before `bcp start`."
501
+ );
502
+ }
503
+
504
+ const manifest =
505
+ JSON.parse(
506
+ content
507
+ ) as
508
+ Partial<ProductionBuildManifest>;
509
+
510
+ if (
511
+ typeof manifest.version !==
512
+ "number" ||
513
+ typeof manifest.buildId !==
514
+ "string" ||
515
+ typeof manifest.builtAt !==
516
+ "string" ||
517
+ !manifest.routes ||
518
+ typeof manifest.routes !==
519
+ "object" ||
520
+ !Array.isArray(
521
+ manifest.assets
522
+ ) ||
523
+ !Object.values(
524
+ manifest.routes
525
+ ).every(
526
+ isValidRouteAsset
527
+ )
528
+ ) {
529
+ throw new Error(
530
+ "BCP Framework: production manifest is invalid. Run `bcp build` again."
531
+ );
532
+ }
533
+
534
+ return manifest as
535
+ ProductionBuildManifest;
536
+ }
537
+
538
+ function isValidRouteAsset(
539
+ value: unknown
540
+ ): value is ProductionRouteAsset {
541
+ if (
542
+ !value ||
543
+ typeof value !== "object"
544
+ ) {
545
+ return false;
546
+ }
547
+
548
+ const route =
549
+ value as
550
+ Partial<ProductionRouteAsset>;
551
+
552
+ return (
553
+ typeof route.entry ===
554
+ "string" &&
555
+ Array.isArray(
556
+ route.imports
557
+ ) &&
558
+ route.imports.every(
559
+ (item) =>
560
+ typeof item ===
561
+ "string"
562
+ ) &&
563
+ Boolean(
564
+ route.size
565
+ ) &&
566
+ typeof route.size?.raw ===
567
+ "number" &&
568
+ typeof route.size?.gzip ===
569
+ "number" &&
570
+ typeof route.size?.brotli ===
571
+ "number"
572
+ );
573
+ }
574
+
575
+ function requireManifest(
576
+ manifest:
577
+ ProductionBuildManifest | null
578
+ ): ProductionBuildManifest {
579
+ if (!manifest) {
580
+ throw new Error(
581
+ "BCP Framework: production server has not loaded the build manifest."
582
+ );
583
+ }
584
+
585
+ return manifest;
586
+ }
587
+
588
+ async function handleNavigationRequest(
589
+ req: http.IncomingMessage,
590
+ res: http.ServerResponse,
591
+ requestUrl: URL,
592
+ rootDirectory: string,
593
+ manifest: ProductionBuildManifest
594
+ ): Promise<void> {
595
+ if (
596
+ req.method !== "GET"
597
+ ) {
598
+ sendNavigationError(
599
+ res,
600
+ 405,
601
+ "Method Not Allowed"
602
+ );
603
+ return;
604
+ }
605
+
606
+ const targetPath =
607
+ requestUrl.searchParams.get(
608
+ "url"
609
+ );
610
+
611
+ if (!targetPath) {
612
+ sendNavigationError(
613
+ res,
614
+ 400,
615
+ "Missing navigation target."
616
+ );
617
+ return;
618
+ }
619
+
620
+ let target: URL;
621
+
622
+ try {
623
+ target =
624
+ new URL(
625
+ targetPath,
626
+ requestUrl.origin
627
+ );
628
+ } catch {
629
+ sendNavigationError(
630
+ res,
631
+ 400,
632
+ "Invalid navigation target."
633
+ );
634
+ return;
635
+ }
636
+
637
+ if (
638
+ target.origin !==
639
+ requestUrl.origin
640
+ ) {
641
+ sendNavigationError(
642
+ res,
643
+ 400,
644
+ "Cross-origin navigation is not allowed."
645
+ );
646
+ return;
647
+ }
648
+
649
+ const payload =
650
+ await resolveNavigationPayload(
651
+ rootDirectory,
652
+ target.pathname,
653
+ {
654
+ resolveClientScript:
655
+ (pathname) =>
656
+ manifest.routes[
657
+ pathname
658
+ ]?.entry ??
659
+ null,
660
+ resolveClientImports:
661
+ (pathname) =>
662
+ manifest.routes[
663
+ pathname
664
+ ]?.imports ??
665
+ [],
666
+ version:
667
+ manifest.version,
668
+ }
669
+ );
670
+
671
+ if (!payload) {
672
+ sendNavigationError(
673
+ res,
674
+ 404,
675
+ "Route not found."
676
+ );
677
+ return;
678
+ }
679
+
680
+ sendNavigationPayload(
681
+ res,
682
+ payload
683
+ );
684
+ }
685
+
686
+ async function serveBuildAsset(
687
+ req: http.IncomingMessage,
688
+ res: http.ServerResponse,
689
+ clientDirectory: string,
690
+ pathname: string
691
+ ): Promise<boolean> {
692
+ const encodedRelative =
693
+ pathname.slice(
694
+ "/_bcp/client/".length
695
+ );
696
+
697
+ let relativePath: string;
698
+
699
+ try {
700
+ relativePath =
701
+ decodeURIComponent(
702
+ encodedRelative
703
+ );
704
+ } catch {
705
+ return false;
706
+ }
707
+
708
+ if (
709
+ !relativePath ||
710
+ relativePath.includes("\0")
711
+ ) {
712
+ return false;
713
+ }
714
+
715
+ const root =
716
+ path.resolve(
717
+ clientDirectory
718
+ );
719
+
720
+ const candidate =
721
+ path.resolve(
722
+ root,
723
+ relativePath.replaceAll(
724
+ "/",
725
+ path.sep
726
+ )
727
+ );
728
+
729
+ if (
730
+ !isPathInside(
731
+ root,
732
+ candidate
733
+ )
734
+ ) {
735
+ return false;
736
+ }
737
+
738
+ let stat:
739
+ Awaited<
740
+ ReturnType<typeof fs.stat>
741
+ >;
742
+
743
+ try {
744
+ stat =
745
+ await fs.stat(
746
+ candidate
747
+ );
748
+ } catch {
749
+ return false;
750
+ }
751
+
752
+ if (!stat.isFile()) {
753
+ return false;
754
+ }
755
+
756
+ const contentType =
757
+ getContentType(
758
+ candidate
759
+ );
760
+
761
+ const encoding =
762
+ selectContentEncoding(
763
+ req,
764
+ contentType,
765
+ stat.size,
766
+ {
767
+ br:
768
+ await fileExists(
769
+ `${candidate}.br`
770
+ ),
771
+ gzip:
772
+ await fileExists(
773
+ `${candidate}.gz`
774
+ ),
775
+ }
776
+ );
777
+
778
+ const sourceFile =
779
+ encoding === "br"
780
+ ? `${candidate}.br`
781
+ : encoding === "gzip"
782
+ ? `${candidate}.gz`
783
+ : candidate;
784
+
785
+ const content =
786
+ await fs.readFile(
787
+ sourceFile
788
+ );
789
+
790
+ const headers:
791
+ Record<string, string | number> = {
792
+ "Content-Type":
793
+ contentType,
794
+ "Cache-Control":
795
+ "public, max-age=31536000, immutable",
796
+ "X-Content-Type-Options":
797
+ "nosniff",
798
+ "Content-Length":
799
+ content.length,
800
+ Vary:
801
+ "Accept-Encoding",
802
+ };
803
+
804
+ if (encoding) {
805
+ headers[
806
+ "Content-Encoding"
807
+ ] =
808
+ encoding;
809
+ }
810
+
811
+ res.writeHead(
812
+ 200,
813
+ headers
814
+ );
815
+
816
+ if (
817
+ req.method === "HEAD"
818
+ ) {
819
+ res.end();
820
+ return true;
821
+ }
822
+
823
+ res.end(
824
+ content
825
+ );
826
+
827
+ return true;
828
+ }
829
+
830
+ async function fileExists(
831
+ filePath: string
832
+ ): Promise<boolean> {
833
+ try {
834
+ const stat =
835
+ await fs.stat(
836
+ filePath
837
+ );
838
+
839
+ return stat.isFile();
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+
845
+ function sendPublicAsset(
846
+ req: http.IncomingMessage,
847
+ res: http.ServerResponse,
848
+ asset: StaticAsset
849
+ ): void {
850
+ const encoding =
851
+ selectContentEncoding(
852
+ req,
853
+ asset.contentType,
854
+ asset.content.length
855
+ );
856
+
857
+ const content =
858
+ compressDynamicContent(
859
+ asset.content,
860
+ encoding
861
+ );
862
+
863
+ const etag =
864
+ createVariantEtag(
865
+ asset.etag,
866
+ encoding
867
+ );
868
+
869
+ const ifNoneMatch =
870
+ req.headers[
871
+ "if-none-match"
872
+ ];
873
+
874
+ const values =
875
+ Array.isArray(
876
+ ifNoneMatch
877
+ )
878
+ ? ifNoneMatch
879
+ : ifNoneMatch
880
+ ? ifNoneMatch.split(",")
881
+ : [];
882
+
883
+ if (
884
+ values
885
+ .map(
886
+ (value) =>
887
+ value.trim()
888
+ )
889
+ .includes(
890
+ etag
891
+ )
892
+ ) {
893
+ res.writeHead(
894
+ 304,
895
+ {
896
+ ETag:
897
+ etag,
898
+ "Cache-Control":
899
+ "public, max-age=0, must-revalidate",
900
+ Vary:
901
+ "Accept-Encoding",
902
+ }
903
+ );
904
+ res.end();
905
+ return;
906
+ }
907
+
908
+ const headers:
909
+ Record<string, string | number> = {
910
+ "Content-Type":
911
+ asset.contentType,
912
+ "Content-Length":
913
+ content.length,
914
+ ETag:
915
+ etag,
916
+ "Last-Modified":
917
+ asset.lastModified,
918
+ "Cache-Control":
919
+ "public, max-age=0, must-revalidate",
920
+ "X-Content-Type-Options":
921
+ "nosniff",
922
+ };
923
+
924
+ if (
925
+ isCompressibleContentType(
926
+ asset.contentType
927
+ )
928
+ ) {
929
+ headers.Vary =
930
+ "Accept-Encoding";
931
+ }
932
+
933
+ if (encoding) {
934
+ headers[
935
+ "Content-Encoding"
936
+ ] =
937
+ encoding;
938
+ }
939
+
940
+ res.writeHead(
941
+ 200,
942
+ headers
943
+ );
944
+
945
+ if (
946
+ req.method === "HEAD"
947
+ ) {
948
+ res.end();
949
+ return;
950
+ }
951
+
952
+ res.end(
953
+ content
954
+ );
955
+ }
956
+
957
+ async function handleApiRoute(
958
+ req: http.IncomingMessage,
959
+ res: http.ServerResponse,
960
+ requestUrl: URL,
961
+ route: ApiRoute,
962
+ params: Record<string, string>,
963
+ buildId: string
964
+ ): Promise<void> {
965
+ const moduleUrl =
966
+ createVersionedModuleUrl(
967
+ route.filePath,
968
+ buildId
969
+ );
970
+
971
+ const apiModule =
972
+ await import(
973
+ moduleUrl
974
+ );
975
+
976
+ const method =
977
+ normalizeHttpMethod(
978
+ req.method
979
+ );
980
+
981
+ const allowedMethods =
982
+ getAllowedMethods(
983
+ apiModule
984
+ );
985
+
986
+ if (
987
+ method === "OPTIONS" &&
988
+ !getApiHandler(
989
+ apiModule,
990
+ "OPTIONS"
991
+ )
992
+ ) {
993
+ res.writeHead(
994
+ 204,
995
+ {
996
+ Allow:
997
+ allowedMethods.join(
998
+ ", "
999
+ ),
1000
+ }
1001
+ );
1002
+ res.end();
1003
+ return;
1004
+ }
1005
+
1006
+ let handler =
1007
+ getApiHandler(
1008
+ apiModule,
1009
+ method
1010
+ );
1011
+
1012
+ if (
1013
+ !handler &&
1014
+ method === "HEAD"
1015
+ ) {
1016
+ handler =
1017
+ getApiHandler(
1018
+ apiModule,
1019
+ "GET"
1020
+ );
1021
+ }
1022
+
1023
+ if (!handler) {
1024
+ sendMethodNotAllowed(
1025
+ res,
1026
+ allowedMethods
1027
+ );
1028
+ return;
1029
+ }
1030
+
1031
+ const response =
1032
+ await handler(
1033
+ await createWebRequest(
1034
+ req,
1035
+ requestUrl
1036
+ ),
1037
+ {
1038
+ params,
1039
+ }
1040
+ );
1041
+
1042
+ if (
1043
+ !(response instanceof Response)
1044
+ ) {
1045
+ throw new Error(
1046
+ `API handler "${method} ${route.pathname}" must return a Response object.`
1047
+ );
1048
+ }
1049
+
1050
+ await sendWebResponse(
1051
+ req,
1052
+ res,
1053
+ response,
1054
+ method === "HEAD"
1055
+ );
1056
+ }
1057
+
1058
+ function normalizeHttpMethod(
1059
+ method:
1060
+ string | undefined
1061
+ ): HttpMethod {
1062
+ return (
1063
+ (
1064
+ method ??
1065
+ "GET"
1066
+ ).toUpperCase() as
1067
+ HttpMethod
1068
+ );
1069
+ }
1070
+
1071
+ function getApiHandler(
1072
+ module:
1073
+ Record<string, unknown>,
1074
+ method: string
1075
+ ): ApiHandler | undefined {
1076
+ const value =
1077
+ module[
1078
+ method
1079
+ ];
1080
+
1081
+ return typeof value ===
1082
+ "function"
1083
+ ? value as ApiHandler
1084
+ : undefined;
1085
+ }
1086
+
1087
+ function getAllowedMethods(
1088
+ module:
1089
+ Record<string, unknown>
1090
+ ): string[] {
1091
+ const allowed:
1092
+ string[] = [];
1093
+
1094
+ for (
1095
+ const method
1096
+ of HTTP_METHODS
1097
+ ) {
1098
+ if (
1099
+ method === "OPTIONS"
1100
+ ) {
1101
+ continue;
1102
+ }
1103
+
1104
+ if (
1105
+ method === "HEAD"
1106
+ ) {
1107
+ if (
1108
+ getApiHandler(
1109
+ module,
1110
+ "HEAD"
1111
+ ) ||
1112
+ getApiHandler(
1113
+ module,
1114
+ "GET"
1115
+ )
1116
+ ) {
1117
+ allowed.push(
1118
+ "HEAD"
1119
+ );
1120
+ }
1121
+ continue;
1122
+ }
1123
+
1124
+ if (
1125
+ getApiHandler(
1126
+ module,
1127
+ method
1128
+ )
1129
+ ) {
1130
+ allowed.push(
1131
+ method
1132
+ );
1133
+ }
1134
+ }
1135
+
1136
+ allowed.push(
1137
+ "OPTIONS"
1138
+ );
1139
+
1140
+ return allowed;
1141
+ }
1142
+
1143
+ async function createWebRequest(
1144
+ req: http.IncomingMessage,
1145
+ url: URL
1146
+ ): Promise<Request> {
1147
+ const headers =
1148
+ new Headers();
1149
+
1150
+ for (
1151
+ const [
1152
+ key,
1153
+ value,
1154
+ ]
1155
+ of Object.entries(
1156
+ req.headers
1157
+ )
1158
+ ) {
1159
+ if (
1160
+ value ===
1161
+ undefined
1162
+ ) {
1163
+ continue;
1164
+ }
1165
+
1166
+ if (
1167
+ Array.isArray(
1168
+ value
1169
+ )
1170
+ ) {
1171
+ for (
1172
+ const item
1173
+ of value
1174
+ ) {
1175
+ headers.append(
1176
+ key,
1177
+ item
1178
+ );
1179
+ }
1180
+ continue;
1181
+ }
1182
+
1183
+ headers.set(
1184
+ key,
1185
+ value
1186
+ );
1187
+ }
1188
+
1189
+ const method =
1190
+ (
1191
+ req.method ??
1192
+ "GET"
1193
+ ).toUpperCase();
1194
+
1195
+ const init:
1196
+ RequestInit = {
1197
+ method,
1198
+ headers,
1199
+ };
1200
+
1201
+ if (
1202
+ method !== "GET" &&
1203
+ method !== "HEAD"
1204
+ ) {
1205
+ const body =
1206
+ await readRequestBody(
1207
+ req
1208
+ );
1209
+
1210
+ if (
1211
+ body.length > 0
1212
+ ) {
1213
+ init.body =
1214
+ body as unknown as
1215
+ BodyInit;
1216
+ }
1217
+ }
1218
+
1219
+ return new Request(
1220
+ url,
1221
+ init
1222
+ );
1223
+ }
1224
+
1225
+ async function readRequestBody(
1226
+ req: http.IncomingMessage
1227
+ ): Promise<Buffer> {
1228
+ const chunks:
1229
+ Buffer[] = [];
1230
+
1231
+ for await (
1232
+ const chunk
1233
+ of req
1234
+ ) {
1235
+ chunks.push(
1236
+ Buffer.isBuffer(
1237
+ chunk
1238
+ )
1239
+ ? chunk
1240
+ : Buffer.from(
1241
+ chunk
1242
+ )
1243
+ );
1244
+ }
1245
+
1246
+ return Buffer.concat(
1247
+ chunks
1248
+ );
1249
+ }
1250
+
1251
+ async function sendWebResponse(
1252
+ req: http.IncomingMessage,
1253
+ res: http.ServerResponse,
1254
+ response: Response,
1255
+ isHead: boolean
1256
+ ): Promise<void> {
1257
+ res.statusCode =
1258
+ response.status;
1259
+
1260
+ response.headers.forEach(
1261
+ (
1262
+ value,
1263
+ key
1264
+ ) => {
1265
+ if (
1266
+ key.toLowerCase() ===
1267
+ "set-cookie" ||
1268
+ key.toLowerCase() ===
1269
+ "content-length"
1270
+ ) {
1271
+ return;
1272
+ }
1273
+
1274
+ res.setHeader(
1275
+ key,
1276
+ value
1277
+ );
1278
+ }
1279
+ );
1280
+
1281
+ const responseHeaders =
1282
+ response.headers as
1283
+ Headers & {
1284
+ getSetCookie?: () =>
1285
+ string[];
1286
+ };
1287
+
1288
+ const cookies =
1289
+ responseHeaders
1290
+ .getSetCookie?.();
1291
+
1292
+ if (
1293
+ cookies &&
1294
+ cookies.length > 0
1295
+ ) {
1296
+ res.setHeader(
1297
+ "Set-Cookie",
1298
+ cookies
1299
+ );
1300
+ }
1301
+
1302
+ if (
1303
+ response.status === 204 ||
1304
+ response.status === 304
1305
+ ) {
1306
+ res.end();
1307
+ return;
1308
+ }
1309
+
1310
+ const rawBody =
1311
+ Buffer.from(
1312
+ await response.arrayBuffer()
1313
+ );
1314
+
1315
+ const contentType =
1316
+ response.headers.get(
1317
+ "content-type"
1318
+ ) ??
1319
+ "application/octet-stream";
1320
+
1321
+ const existingEncoding =
1322
+ response.headers.get(
1323
+ "content-encoding"
1324
+ );
1325
+
1326
+ const encoding =
1327
+ existingEncoding
1328
+ ? null
1329
+ : selectContentEncoding(
1330
+ req,
1331
+ contentType,
1332
+ rawBody.length
1333
+ );
1334
+
1335
+ const body =
1336
+ existingEncoding
1337
+ ? rawBody
1338
+ : compressDynamicContent(
1339
+ rawBody,
1340
+ encoding
1341
+ );
1342
+
1343
+ if (
1344
+ encoding
1345
+ ) {
1346
+ res.setHeader(
1347
+ "Content-Encoding",
1348
+ encoding
1349
+ );
1350
+ res.setHeader(
1351
+ "Vary",
1352
+ "Accept-Encoding"
1353
+ );
1354
+ }
1355
+
1356
+ res.setHeader(
1357
+ "Content-Length",
1358
+ body.length
1359
+ );
1360
+
1361
+ if (isHead) {
1362
+ res.end();
1363
+ return;
1364
+ }
1365
+
1366
+ res.end(
1367
+ body
1368
+ );
1369
+ }
1370
+
1371
+ async function importComponent(
1372
+ filePath: string,
1373
+ componentType: string,
1374
+ buildId: string
1375
+ ): Promise<ComponentType<any>> {
1376
+ const module =
1377
+ await import(
1378
+ createVersionedModuleUrl(
1379
+ filePath,
1380
+ buildId
1381
+ )
1382
+ );
1383
+
1384
+ const Component =
1385
+ module.default;
1386
+
1387
+ if (
1388
+ typeof Component !==
1389
+ "function"
1390
+ ) {
1391
+ throw new Error(
1392
+ `${componentType} "${filePath}" does not export a default React component.`
1393
+ );
1394
+ }
1395
+
1396
+ return Component;
1397
+ }
1398
+
1399
+ function createVersionedModuleUrl(
1400
+ filePath: string,
1401
+ buildId: string
1402
+ ): string {
1403
+ const url =
1404
+ pathToFileURL(
1405
+ filePath
1406
+ );
1407
+
1408
+ url.searchParams.set(
1409
+ "bcp-build",
1410
+ buildId
1411
+ );
1412
+
1413
+ return url.href;
1414
+ }
1415
+
1416
+ function renderDocument(
1417
+ content: string,
1418
+ routeAsset: ProductionRouteAsset,
1419
+ frameworkData: FrameworkData
1420
+ ): string {
1421
+ const modulePreloads =
1422
+ routeAsset.imports
1423
+ .map(
1424
+ (importPath) =>
1425
+ ` <link rel="modulepreload" href="${escapeHtmlAttribute(
1426
+ importPath
1427
+ )}" />`
1428
+ )
1429
+ .join("\n");
1430
+
1431
+ return `
1432
+ <!DOCTYPE html>
1433
+ <html lang="en">
1434
+ <head>
1435
+ <meta charset="UTF-8" />
1436
+ <meta
1437
+ name="viewport"
1438
+ content="width=device-width, initial-scale=1.0"
1439
+ />
1440
+ <title>BCP Framework</title>
1441
+ ${modulePreloads}
1442
+ </head>
1443
+ <body>
1444
+ <div id="root">${content}</div>
1445
+
1446
+ <script
1447
+ id="__BCP_DATA__"
1448
+ type="application/json"
1449
+ >${serializeFrameworkData(frameworkData)}</script>
1450
+
1451
+ <script
1452
+ data-bcp-client
1453
+ type="module"
1454
+ src="${escapeHtmlAttribute(
1455
+ routeAsset.entry
1456
+ )}"
1457
+ ></script>
1458
+ </body>
1459
+ </html>
1460
+ `.trim();
1461
+ }
1462
+
1463
+ function serializeFrameworkData(
1464
+ value: unknown
1465
+ ): string {
1466
+ return JSON.stringify(
1467
+ value
1468
+ )
1469
+ .replace(
1470
+ /</g,
1471
+ "\\u003c"
1472
+ )
1473
+ .replace(
1474
+ /\u2028/g,
1475
+ "\\u2028"
1476
+ )
1477
+ .replace(
1478
+ /\u2029/g,
1479
+ "\\u2029"
1480
+ );
1481
+ }
1482
+
1483
+ function sendHtml(
1484
+ req: http.IncomingMessage,
1485
+ res: http.ServerResponse,
1486
+ html: string
1487
+ ): void {
1488
+ const contentType =
1489
+ "text/html; charset=utf-8";
1490
+
1491
+ const rawBody =
1492
+ Buffer.from(
1493
+ html
1494
+ );
1495
+
1496
+ const encoding =
1497
+ selectContentEncoding(
1498
+ req,
1499
+ contentType,
1500
+ rawBody.length
1501
+ );
1502
+
1503
+ const body =
1504
+ compressDynamicContent(
1505
+ rawBody,
1506
+ encoding
1507
+ );
1508
+
1509
+ const headers:
1510
+ Record<string, string | number> = {
1511
+ "Content-Type":
1512
+ contentType,
1513
+ "Cache-Control":
1514
+ "no-store",
1515
+ "Content-Length":
1516
+ body.length,
1517
+ Vary:
1518
+ "Accept-Encoding",
1519
+ };
1520
+
1521
+ if (encoding) {
1522
+ headers[
1523
+ "Content-Encoding"
1524
+ ] =
1525
+ encoding;
1526
+ }
1527
+
1528
+ res.writeHead(
1529
+ 200,
1530
+ headers
1531
+ );
1532
+
1533
+ if (
1534
+ req.method === "HEAD"
1535
+ ) {
1536
+ res.end();
1537
+ return;
1538
+ }
1539
+
1540
+ res.end(
1541
+ body
1542
+ );
1543
+ }
1544
+
1545
+ function sendJavaScript(
1546
+ res: http.ServerResponse,
1547
+ content: Buffer,
1548
+ immutable = true
1549
+ ): void {
1550
+ res.writeHead(
1551
+ 200,
1552
+ {
1553
+ "Content-Type":
1554
+ "text/javascript; charset=utf-8",
1555
+ "Cache-Control":
1556
+ immutable
1557
+ ? "public, max-age=31536000, immutable"
1558
+ : "no-store",
1559
+ "Content-Length":
1560
+ content.length,
1561
+ }
1562
+ );
1563
+
1564
+ res.end(
1565
+ content
1566
+ );
1567
+ }
1568
+
1569
+ function sendMethodNotAllowed(
1570
+ res: http.ServerResponse,
1571
+ methods: string[]
1572
+ ): void {
1573
+ const body =
1574
+ JSON.stringify({
1575
+ error:
1576
+ "Method Not Allowed",
1577
+ allowedMethods:
1578
+ methods,
1579
+ });
1580
+
1581
+ res.writeHead(
1582
+ 405,
1583
+ {
1584
+ "Content-Type":
1585
+ "application/json; charset=utf-8",
1586
+ Allow:
1587
+ methods.join(
1588
+ ", "
1589
+ ),
1590
+ "Content-Length":
1591
+ Buffer.byteLength(
1592
+ body
1593
+ ),
1594
+ }
1595
+ );
1596
+
1597
+ res.end(
1598
+ body
1599
+ );
1600
+ }
1601
+
1602
+ function sendNotFound(
1603
+ res: http.ServerResponse
1604
+ ): void {
1605
+ const body =
1606
+ "<!DOCTYPE html><html><body><h1>404</h1><p>Page Not Found</p></body></html>";
1607
+
1608
+ res.writeHead(
1609
+ 404,
1610
+ {
1611
+ "Content-Type":
1612
+ "text/html; charset=utf-8",
1613
+ "Content-Length":
1614
+ Buffer.byteLength(
1615
+ body
1616
+ ),
1617
+ }
1618
+ );
1619
+
1620
+ res.end(
1621
+ body
1622
+ );
1623
+ }
1624
+
1625
+ function sendInternalServerError(
1626
+ res: http.ServerResponse,
1627
+ error: unknown
1628
+ ): void {
1629
+ const message =
1630
+ error instanceof Error
1631
+ ? error.message
1632
+ : "Unknown error";
1633
+
1634
+ const body =
1635
+ `<!DOCTYPE html><html><body><h1>500</h1><p>Internal Server Error</p><pre>${escapeHtml(
1636
+ message
1637
+ )}</pre></body></html>`;
1638
+
1639
+ res.writeHead(
1640
+ 500,
1641
+ {
1642
+ "Content-Type":
1643
+ "text/html; charset=utf-8",
1644
+ "Cache-Control":
1645
+ "no-store",
1646
+ "Content-Length":
1647
+ Buffer.byteLength(
1648
+ body
1649
+ ),
1650
+ }
1651
+ );
1652
+
1653
+ res.end(
1654
+ body
1655
+ );
1656
+ }
1657
+
1658
+ function createVariantEtag(
1659
+ etag: string,
1660
+ encoding: ContentEncoding
1661
+ ): string {
1662
+ if (!encoding) {
1663
+ return etag;
1664
+ }
1665
+
1666
+ const value =
1667
+ etag
1668
+ .replace(/^W\//, "")
1669
+ .replace(/^"|"$/g, "");
1670
+
1671
+ return `W/"${value}-${encoding}"`;
1672
+ }
1673
+
1674
+ function normalizePathname(
1675
+ pathname: string
1676
+ ): string {
1677
+ if (
1678
+ pathname.length > 1 &&
1679
+ pathname.endsWith("/")
1680
+ ) {
1681
+ return pathname.slice(
1682
+ 0,
1683
+ -1
1684
+ );
1685
+ }
1686
+
1687
+ return pathname || "/";
1688
+ }
1689
+
1690
+ function isPathInside(
1691
+ root: string,
1692
+ candidate: string
1693
+ ): boolean {
1694
+ const relative =
1695
+ path.relative(
1696
+ root,
1697
+ candidate
1698
+ );
1699
+
1700
+ return (
1701
+ relative === "" ||
1702
+ (
1703
+ relative !== ".." &&
1704
+ !relative.startsWith(
1705
+ `..${path.sep}`
1706
+ ) &&
1707
+ !path.isAbsolute(
1708
+ relative
1709
+ )
1710
+ )
1711
+ );
1712
+ }
1713
+
1714
+ function escapeHtml(
1715
+ value: string
1716
+ ): string {
1717
+ return value
1718
+ .replaceAll(
1719
+ "&",
1720
+ "&amp;"
1721
+ )
1722
+ .replaceAll(
1723
+ "<",
1724
+ "&lt;"
1725
+ )
1726
+ .replaceAll(
1727
+ ">",
1728
+ "&gt;"
1729
+ )
1730
+ .replaceAll(
1731
+ '"',
1732
+ "&quot;"
1733
+ )
1734
+ .replaceAll(
1735
+ "'",
1736
+ "&#039;"
1737
+ );
1738
+ }
1739
+
1740
+ function escapeHtmlAttribute(
1741
+ value: string
1742
+ ): string {
1743
+ return escapeHtml(
1744
+ value
1745
+ );
1746
+ }