@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,2265 @@
1
+ import * as http from "node:http";
2
+
3
+ import path from "node:path";
4
+
5
+ import {
6
+ pathToFileURL,
7
+ } from "node:url";
8
+
9
+ import chokidar from "chokidar";
10
+
11
+ import {
12
+ createElement,
13
+ type ComponentType,
14
+ type ReactElement,
15
+ } from "react";
16
+
17
+ import {
18
+ renderToString,
19
+ } from "react-dom/server";
20
+
21
+ import {
22
+ assertNoPageApiConflicts,
23
+ matchApiRoute,
24
+ matchRoute,
25
+ scanApiRoutes,
26
+ scanRoutes,
27
+ type ApiRoute,
28
+ type Route,
29
+ } from "../../router/src/index.js";
30
+
31
+ import {
32
+ buildDevAssets,
33
+ DEV_RUNTIME_PATH,
34
+ DevClientBundler,
35
+ type ClientBundle,
36
+ type DevAsset,
37
+ } from "../../bundler/src/index.js";
38
+
39
+ import {
40
+ DEV_EVENTS_PATH,
41
+ DevHmrServer,
42
+ } from "./dev-hmr.js";
43
+
44
+ /*
45
+ * =====================================
46
+ * Constants
47
+ * =====================================
48
+ */
49
+
50
+ const REBUILD_DEBOUNCE_MS =
51
+ 100;
52
+
53
+ /*
54
+ * =====================================
55
+ * Types
56
+ * =====================================
57
+ */
58
+
59
+ export interface DevServerOptions {
60
+ port?: number;
61
+
62
+ hostname?: string;
63
+
64
+ rootDirectory?: string;
65
+ }
66
+
67
+ interface FrameworkData {
68
+ route: string;
69
+
70
+ params: Record<
71
+ string,
72
+ string
73
+ >;
74
+ }
75
+
76
+ interface ApiContext {
77
+ params: Record<
78
+ string,
79
+ string
80
+ >;
81
+ }
82
+
83
+ type ApiHandler = (
84
+ request: Request,
85
+ context: ApiContext
86
+ ) =>
87
+ | Response
88
+ | Promise<Response>;
89
+
90
+ const HTTP_METHODS = [
91
+ "GET",
92
+ "POST",
93
+ "PUT",
94
+ "PATCH",
95
+ "DELETE",
96
+ "HEAD",
97
+ "OPTIONS",
98
+ ] as const;
99
+
100
+ type HttpMethod =
101
+ typeof HTTP_METHODS[number];
102
+
103
+ type FileEvent =
104
+ | "add"
105
+ | "change"
106
+ | "unlink";
107
+
108
+ /*
109
+ * =====================================
110
+ * Server
111
+ * =====================================
112
+ */
113
+
114
+ export function createDevServer(
115
+ options: DevServerOptions = {}
116
+ ) {
117
+ const port =
118
+ options.port ?? 3000;
119
+
120
+ const hostname =
121
+ options.hostname ??
122
+ "localhost";
123
+
124
+ const rootDirectory =
125
+ options.rootDirectory ??
126
+ process.cwd();
127
+
128
+ const appDirectory =
129
+ path.join(
130
+ rootDirectory,
131
+ "app"
132
+ );
133
+
134
+ /*
135
+ * =====================================
136
+ * Framework State
137
+ * =====================================
138
+ */
139
+
140
+ let routes:
141
+ Route[] = [];
142
+
143
+ let apiRoutes:
144
+ ApiRoute[] = [];
145
+
146
+ let clientBundles:
147
+ ClientBundle[] = [];
148
+
149
+ let devAssets:
150
+ DevAsset[] = [];
151
+
152
+ let clientBundler:
153
+ DevClientBundler | null =
154
+ null;
155
+
156
+ /*
157
+ * Server module cache version
158
+ */
159
+ let moduleVersion =
160
+ 0;
161
+
162
+ /*
163
+ * Browser bundle version
164
+ */
165
+ let clientVersion =
166
+ 0;
167
+
168
+ const hmr =
169
+ new DevHmrServer();
170
+
171
+ /*
172
+ * =====================================
173
+ * Watcher State
174
+ * =====================================
175
+ */
176
+
177
+ let watcher:
178
+ ReturnType<
179
+ typeof chokidar.watch
180
+ > | null = null;
181
+
182
+ let rebuildTimer:
183
+ NodeJS.Timeout | null =
184
+ null;
185
+
186
+ let rebuildRunning =
187
+ false;
188
+
189
+ let rebuildQueued =
190
+ false;
191
+
192
+ /*
193
+ * Step 8/9 เก็บ change ล่าสุดแค่ 1 file
194
+ *
195
+ * Step 10:
196
+ * เก็บทุกไฟล์ที่เปลี่ยนใน debounce window
197
+ */
198
+ const pendingChanges =
199
+ new Map<
200
+ string,
201
+ FileEvent
202
+ >();
203
+
204
+ /*
205
+ * =====================================
206
+ * Routes
207
+ * =====================================
208
+ */
209
+
210
+ function scanAllRoutes() {
211
+ const nextRoutes =
212
+ scanRoutes(
213
+ appDirectory
214
+ );
215
+
216
+ const nextApiRoutes =
217
+ scanApiRoutes(
218
+ appDirectory
219
+ );
220
+
221
+ assertNoPageApiConflicts(
222
+ nextRoutes,
223
+ nextApiRoutes
224
+ );
225
+
226
+ return {
227
+ routes:
228
+ nextRoutes,
229
+
230
+ apiRoutes:
231
+ nextApiRoutes,
232
+ };
233
+ }
234
+
235
+ function loadInitialRoutes() {
236
+ const result =
237
+ scanAllRoutes();
238
+
239
+ routes =
240
+ result.routes;
241
+
242
+ apiRoutes =
243
+ result.apiRoutes;
244
+
245
+ printRoutes();
246
+ }
247
+
248
+ function printRoutes() {
249
+ console.log("");
250
+ console.log(
251
+ "Page Routes:"
252
+ );
253
+
254
+ if (
255
+ routes.length === 0
256
+ ) {
257
+ console.log(
258
+ " (none)"
259
+ );
260
+ }
261
+
262
+ for (
263
+ const route
264
+ of routes
265
+ ) {
266
+ console.log(
267
+ ` ${route.pathname}`
268
+ );
269
+
270
+ console.log(
271
+ ` Page: ${route.filePath}`
272
+ );
273
+
274
+ for (
275
+ const layout
276
+ of route.layouts
277
+ ) {
278
+ console.log(
279
+ ` Layout: ${layout}`
280
+ );
281
+ }
282
+ }
283
+
284
+ console.log("");
285
+ console.log(
286
+ "API Routes:"
287
+ );
288
+
289
+ if (
290
+ apiRoutes.length === 0
291
+ ) {
292
+ console.log(
293
+ " (none)"
294
+ );
295
+ }
296
+
297
+ for (
298
+ const route
299
+ of apiRoutes
300
+ ) {
301
+ console.log(
302
+ ` ${route.pathname}`
303
+ );
304
+
305
+ console.log(
306
+ ` Handler: ${route.filePath}`
307
+ );
308
+ }
309
+ }
310
+
311
+ loadInitialRoutes();
312
+
313
+ /*
314
+ * =====================================
315
+ * HTTP
316
+ * =====================================
317
+ */
318
+
319
+ const server =
320
+ http.createServer(
321
+ async (
322
+ req,
323
+ res
324
+ ) => {
325
+ const startedAt =
326
+ performance.now();
327
+
328
+ let pathname =
329
+ "/";
330
+
331
+ try {
332
+ const host =
333
+ req.headers.host ??
334
+ `${hostname}:${port}`;
335
+
336
+ const requestUrl =
337
+ new URL(
338
+ req.url ?? "/",
339
+ `http://${host}`
340
+ );
341
+
342
+ pathname =
343
+ normalizePathname(
344
+ requestUrl.pathname
345
+ );
346
+
347
+ /*
348
+ * =================================
349
+ * HMR Event Stream
350
+ * =================================
351
+ */
352
+
353
+ if (
354
+ pathname ===
355
+ DEV_EVENTS_PATH
356
+ ) {
357
+ hmr.connect(
358
+ req,
359
+ res
360
+ );
361
+
362
+ return;
363
+ }
364
+
365
+ /*
366
+ * =================================
367
+ * HTTP Log
368
+ * =================================
369
+ */
370
+
371
+ res.once(
372
+ "finish",
373
+ () => {
374
+ const duration =
375
+ performance.now() -
376
+ startedAt;
377
+
378
+ console.log(
379
+ `${req.method ?? "GET"} ${pathname} ${res.statusCode} ${duration.toFixed(2)}ms`
380
+ );
381
+ }
382
+ );
383
+
384
+ /*
385
+ * =================================
386
+ * Dev Assets
387
+ * =================================
388
+ */
389
+
390
+ const devAsset =
391
+ devAssets.find(
392
+ (asset) =>
393
+ asset.publicPath ===
394
+ pathname
395
+ );
396
+
397
+ if (
398
+ devAsset
399
+ ) {
400
+ sendDevAsset(
401
+ res,
402
+ devAsset
403
+ );
404
+
405
+ return;
406
+ }
407
+
408
+ /*
409
+ * =================================
410
+ * Client Bundles
411
+ * =================================
412
+ */
413
+
414
+ const requestedBundle =
415
+ clientBundles.find(
416
+ (bundle) =>
417
+ bundle.publicPath ===
418
+ pathname
419
+ );
420
+
421
+ if (
422
+ requestedBundle
423
+ ) {
424
+ sendJavaScript(
425
+ res,
426
+ requestedBundle
427
+ .content
428
+ );
429
+
430
+ return;
431
+ }
432
+
433
+ /*
434
+ * =================================
435
+ * API
436
+ * =================================
437
+ */
438
+
439
+ const apiMatch =
440
+ matchApiRoute(
441
+ apiRoutes,
442
+ pathname
443
+ );
444
+
445
+ if (
446
+ apiMatch
447
+ ) {
448
+ await handleApiRoute(
449
+ req,
450
+ res,
451
+ requestUrl,
452
+ apiMatch.route,
453
+ apiMatch.params,
454
+ moduleVersion
455
+ );
456
+
457
+ return;
458
+ }
459
+
460
+ /*
461
+ * =================================
462
+ * Page
463
+ * =================================
464
+ */
465
+
466
+ const match =
467
+ matchRoute(
468
+ routes,
469
+ pathname
470
+ );
471
+
472
+ if (
473
+ !match
474
+ ) {
475
+ sendNotFound(
476
+ res
477
+ );
478
+
479
+ return;
480
+ }
481
+
482
+ const {
483
+ route,
484
+ params,
485
+ } = match;
486
+
487
+ const pageClientBundle =
488
+ clientBundles.find(
489
+ (bundle) =>
490
+ bundle.pathname ===
491
+ route.pathname
492
+ );
493
+
494
+ if (
495
+ !pageClientBundle
496
+ ) {
497
+ throw new Error(
498
+ `Client bundle was not found for route "${route.pathname}".`
499
+ );
500
+ }
501
+
502
+ /*
503
+ * =================================
504
+ * Server Components Import
505
+ * =================================
506
+ */
507
+
508
+ const importStartedAt =
509
+ performance.now();
510
+
511
+ const Page =
512
+ await importComponent(
513
+ route.filePath,
514
+ "Page",
515
+ moduleVersion
516
+ );
517
+
518
+ const Layouts:
519
+ ComponentType<any>[] =
520
+ [];
521
+
522
+ for (
523
+ const layoutFile
524
+ of route.layouts
525
+ ) {
526
+ const Layout =
527
+ await importComponent(
528
+ layoutFile,
529
+ "Layout",
530
+ moduleVersion
531
+ );
532
+
533
+ Layouts.push(
534
+ Layout
535
+ );
536
+ }
537
+
538
+ console.log(
539
+ `Import: ${(performance.now() - importStartedAt).toFixed(2)}ms`
540
+ );
541
+
542
+ /*
543
+ * =================================
544
+ * React Tree
545
+ * =================================
546
+ */
547
+
548
+ const renderStartedAt =
549
+ performance.now();
550
+
551
+ let element:
552
+ ReactElement =
553
+ createElement(
554
+ Page,
555
+ {
556
+ params,
557
+ }
558
+ );
559
+
560
+ for (
561
+ let index =
562
+ Layouts.length -
563
+ 1;
564
+
565
+ index >= 0;
566
+
567
+ index--
568
+ ) {
569
+ const Layout =
570
+ Layouts[index];
571
+
572
+ element =
573
+ createElement(
574
+ Layout,
575
+ {
576
+ params,
577
+ },
578
+ element
579
+ );
580
+ }
581
+
582
+ const content =
583
+ renderToString(
584
+ element
585
+ );
586
+
587
+ console.log(
588
+ `SSR: ${(performance.now() - renderStartedAt).toFixed(2)}ms`
589
+ );
590
+
591
+ /*
592
+ * =================================
593
+ * Framework Data
594
+ * =================================
595
+ */
596
+
597
+ const frameworkData:
598
+ FrameworkData = {
599
+ route:
600
+ route.pathname,
601
+
602
+ params,
603
+ };
604
+
605
+ const html =
606
+ renderDocument(
607
+ content,
608
+
609
+ pageClientBundle
610
+ .publicPath,
611
+
612
+ frameworkData,
613
+
614
+ clientVersion
615
+ );
616
+
617
+ sendHtml(
618
+ res,
619
+ html
620
+ );
621
+ } catch (error) {
622
+ console.error("");
623
+ console.error(
624
+ "BCP Framework Error:"
625
+ );
626
+
627
+ console.error(
628
+ error
629
+ );
630
+
631
+ if (
632
+ res.headersSent
633
+ ) {
634
+ res.end();
635
+ return;
636
+ }
637
+
638
+ sendInternalServerError(
639
+ res,
640
+ error
641
+ );
642
+ }
643
+ }
644
+ );
645
+
646
+ /*
647
+ * =====================================
648
+ * Watcher
649
+ * =====================================
650
+ */
651
+
652
+ function startWatcher() {
653
+ console.log("");
654
+ console.log(
655
+ "Starting file watcher..."
656
+ );
657
+
658
+ watcher =
659
+ chokidar.watch(
660
+ rootDirectory,
661
+ {
662
+ persistent:
663
+ true,
664
+
665
+ ignoreInitial:
666
+ true,
667
+
668
+ atomic:
669
+ true,
670
+
671
+ ignored: (
672
+ watchedPath,
673
+ stats
674
+ ) => {
675
+ const relative =
676
+ path.relative(
677
+ rootDirectory,
678
+ watchedPath
679
+ );
680
+
681
+ const normalized =
682
+ relative.replace(
683
+ /\\/g,
684
+ "/"
685
+ );
686
+
687
+ if (
688
+ normalized ===
689
+ "node_modules" ||
690
+ normalized.startsWith(
691
+ "node_modules/"
692
+ ) ||
693
+ normalized ===
694
+ ".bcp-framework" ||
695
+ normalized.startsWith(
696
+ ".bcp-framework/"
697
+ ) ||
698
+ normalized ===
699
+ "dist" ||
700
+ normalized.startsWith(
701
+ "dist/"
702
+ )
703
+ ) {
704
+ return true;
705
+ }
706
+
707
+ if (
708
+ stats?.isDirectory()
709
+ ) {
710
+ return false;
711
+ }
712
+
713
+ if (
714
+ !stats
715
+ ) {
716
+ return false;
717
+ }
718
+
719
+ return !/\.[jt]sx?$/.test(
720
+ watchedPath
721
+ );
722
+ },
723
+ }
724
+ );
725
+
726
+ watcher.on(
727
+ "add",
728
+ (filePath) => {
729
+ scheduleRebuild(
730
+ "add",
731
+ filePath
732
+ );
733
+ }
734
+ );
735
+
736
+ watcher.on(
737
+ "change",
738
+ (filePath) => {
739
+ scheduleRebuild(
740
+ "change",
741
+ filePath
742
+ );
743
+ }
744
+ );
745
+
746
+ watcher.on(
747
+ "unlink",
748
+ (filePath) => {
749
+ scheduleRebuild(
750
+ "unlink",
751
+ filePath
752
+ );
753
+ }
754
+ );
755
+
756
+ watcher.on(
757
+ "error",
758
+ (error) => {
759
+ console.error(
760
+ "[dev] Watcher error:",
761
+ error
762
+ );
763
+ }
764
+ );
765
+
766
+ watcher.on(
767
+ "ready",
768
+ () => {
769
+ console.log(
770
+ "File watcher ready."
771
+ );
772
+
773
+ console.log("");
774
+ }
775
+ );
776
+ }
777
+
778
+ /*
779
+ * =====================================
780
+ * Schedule
781
+ * =====================================
782
+ */
783
+
784
+ function scheduleRebuild(
785
+ event: FileEvent,
786
+ filePath: string
787
+ ) {
788
+ const existing =
789
+ pendingChanges.get(
790
+ filePath
791
+ );
792
+
793
+ /*
794
+ * add/unlink สำคัญกว่า change
795
+ *
796
+ * อย่าให้ change ที่ตามหลัง
797
+ * overwrite structural event
798
+ */
799
+ if (
800
+ existing !==
801
+ "add" &&
802
+ existing !==
803
+ "unlink"
804
+ ) {
805
+ pendingChanges.set(
806
+ filePath,
807
+ event
808
+ );
809
+ }
810
+
811
+ if (
812
+ rebuildRunning
813
+ ) {
814
+ rebuildQueued =
815
+ true;
816
+
817
+ return;
818
+ }
819
+
820
+ if (
821
+ rebuildTimer
822
+ ) {
823
+ clearTimeout(
824
+ rebuildTimer
825
+ );
826
+ }
827
+
828
+ rebuildTimer =
829
+ setTimeout(
830
+ () => {
831
+ rebuildTimer =
832
+ null;
833
+
834
+ void rebuildDevServer();
835
+ },
836
+ REBUILD_DEBOUNCE_MS
837
+ );
838
+ }
839
+
840
+ /*
841
+ * =====================================
842
+ * Development Rebuild
843
+ * =====================================
844
+ */
845
+
846
+ async function rebuildDevServer() {
847
+ if (
848
+ rebuildRunning
849
+ ) {
850
+ rebuildQueued =
851
+ true;
852
+
853
+ return;
854
+ }
855
+
856
+ rebuildRunning =
857
+ true;
858
+
859
+ /*
860
+ * Snapshot pending changes
861
+ */
862
+ const changes =
863
+ Array.from(
864
+ pendingChanges.entries()
865
+ ).map(
866
+ (
867
+ [
868
+ filePath,
869
+ event,
870
+ ]
871
+ ) => ({
872
+ filePath,
873
+ event,
874
+ })
875
+ );
876
+
877
+ pendingChanges.clear();
878
+
879
+ const startedAt =
880
+ performance.now();
881
+
882
+ try {
883
+ console.log("");
884
+
885
+ for (
886
+ const change
887
+ of changes
888
+ ) {
889
+ console.log(
890
+ `[dev] ${change.event} ${path.relative(
891
+ rootDirectory,
892
+ change.filePath
893
+ )}`
894
+ );
895
+ }
896
+
897
+ /*
898
+ * Routes scan ใหม่
899
+ *
900
+ * ทำเพื่อ detect conflict
901
+ * และ route additions/deletions
902
+ */
903
+ const nextRouteState =
904
+ scanAllRoutes();
905
+
906
+ const hasStructuralChange =
907
+ changes.some(
908
+ (change) =>
909
+ change.event ===
910
+ "add" ||
911
+ change.event ===
912
+ "unlink"
913
+ );
914
+
915
+ /*
916
+ * =================================
917
+ * Structural Change
918
+ * =================================
919
+ *
920
+ * add/delete:
921
+ *
922
+ * page.tsx
923
+ * layout.tsx
924
+ * route.ts
925
+ *
926
+ * route graph อาจเปลี่ยน
927
+ *
928
+ * สร้าง bundler ชุดใหม่
929
+ */
930
+ if (
931
+ hasStructuralChange
932
+ ) {
933
+ console.log(
934
+ "[dev] route structure changed"
935
+ );
936
+
937
+ const nextBundler =
938
+ await DevClientBundler
939
+ .create(
940
+ nextRouteState
941
+ .routes,
942
+
943
+ rootDirectory,
944
+
945
+ {
946
+ fastRefresh:
947
+ true,
948
+ }
949
+ );
950
+
951
+ const oldBundler =
952
+ clientBundler;
953
+
954
+ /*
955
+ * Build ผ่านแล้ว
956
+ * ค่อย swap
957
+ */
958
+ routes =
959
+ nextRouteState.routes;
960
+
961
+ apiRoutes =
962
+ nextRouteState.apiRoutes;
963
+
964
+ clientBundler =
965
+ nextBundler;
966
+
967
+ clientBundles =
968
+ nextBundler
969
+ .getBundles();
970
+
971
+ moduleVersion++;
972
+
973
+ clientVersion++;
974
+
975
+ /*
976
+ * Dispose old contexts
977
+ */
978
+ if (
979
+ oldBundler
980
+ ) {
981
+ await oldBundler
982
+ .dispose();
983
+ }
984
+
985
+ const duration =
986
+ performance.now() -
987
+ startedAt;
988
+
989
+ console.log(
990
+ `[dev] full client rebuild in ${duration.toFixed(2)}ms`
991
+ );
992
+
993
+ hmr.reload(
994
+ clientVersion
995
+ );
996
+
997
+ return;
998
+ }
999
+
1000
+ /*
1001
+ * =================================
1002
+ * Normal File Changes
1003
+ * =================================
1004
+ */
1005
+
1006
+ routes =
1007
+ nextRouteState.routes;
1008
+
1009
+ apiRoutes =
1010
+ nextRouteState.apiRoutes;
1011
+
1012
+ /*
1013
+ * Server-side direct modules
1014
+ */
1015
+ moduleVersion++;
1016
+
1017
+ if (
1018
+ !clientBundler
1019
+ ) {
1020
+ throw new Error(
1021
+ "BCP Framework: client bundler is not initialized."
1022
+ );
1023
+ }
1024
+
1025
+ const changedFiles =
1026
+ changes.map(
1027
+ (change) =>
1028
+ change.filePath
1029
+ );
1030
+
1031
+ /*
1032
+ * Debug dependency graph
1033
+ */
1034
+ for (
1035
+ const filePath
1036
+ of changedFiles
1037
+ ) {
1038
+ clientBundler
1039
+ .printAffectedRoutes(
1040
+ filePath
1041
+ );
1042
+ }
1043
+
1044
+ /*
1045
+ * =================================
1046
+ * Incremental Rebuild
1047
+ * =================================
1048
+ */
1049
+
1050
+ const affectedRoutes =
1051
+ await clientBundler
1052
+ .rebuildAffected(
1053
+ changedFiles
1054
+ );
1055
+
1056
+ /*
1057
+ * ไม่มี client route ใช้ไฟล์นี้
1058
+ *
1059
+ * เช่น:
1060
+ *
1061
+ * app/api/users/route.ts
1062
+ */
1063
+ if (
1064
+ affectedRoutes.length ===
1065
+ 0
1066
+ ) {
1067
+ const duration =
1068
+ performance.now() -
1069
+ startedAt;
1070
+
1071
+ console.log(
1072
+ `[dev] server-only update in ${duration.toFixed(2)}ms`
1073
+ );
1074
+
1075
+ return;
1076
+ }
1077
+
1078
+ /*
1079
+ * Update RAM bundle references
1080
+ */
1081
+ clientBundles =
1082
+ clientBundler
1083
+ .getBundles();
1084
+
1085
+ clientVersion++;
1086
+
1087
+ const duration =
1088
+ performance.now() -
1089
+ startedAt;
1090
+
1091
+ console.log(
1092
+ `[dev] rebuilt ${affectedRoutes.length}/${routes.length} client route(s) in ${duration.toFixed(2)}ms`
1093
+ );
1094
+
1095
+ console.log(
1096
+ `[dev] affected: ${affectedRoutes.join(", ")}`
1097
+ );
1098
+
1099
+ /*
1100
+ * Fast Refresh เฉพาะ
1101
+ * affected routes
1102
+ */
1103
+ hmr.refresh(
1104
+ clientVersion,
1105
+ affectedRoutes
1106
+ );
1107
+ } catch (error) {
1108
+ console.error("");
1109
+ console.error(
1110
+ "[dev] rebuild failed:"
1111
+ );
1112
+
1113
+ console.error(
1114
+ error
1115
+ );
1116
+
1117
+ console.error("");
1118
+ } finally {
1119
+ rebuildRunning =
1120
+ false;
1121
+
1122
+ /*
1123
+ * มี save เกิดขึ้น
1124
+ * ระหว่าง rebuild
1125
+ */
1126
+ if (
1127
+ rebuildQueued ||
1128
+ pendingChanges.size >
1129
+ 0
1130
+ ) {
1131
+ rebuildQueued =
1132
+ false;
1133
+
1134
+ if (
1135
+ rebuildTimer
1136
+ ) {
1137
+ clearTimeout(
1138
+ rebuildTimer
1139
+ );
1140
+ }
1141
+
1142
+ rebuildTimer =
1143
+ setTimeout(
1144
+ () => {
1145
+ rebuildTimer =
1146
+ null;
1147
+
1148
+ void rebuildDevServer();
1149
+ },
1150
+ REBUILD_DEBOUNCE_MS
1151
+ );
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ /*
1157
+ * =====================================
1158
+ * Public Server API
1159
+ * =====================================
1160
+ */
1161
+
1162
+ return {
1163
+ async start() {
1164
+ console.log("");
1165
+
1166
+ /*
1167
+ * =================================
1168
+ * Dev Runtime
1169
+ * =================================
1170
+ */
1171
+
1172
+ console.log(
1173
+ "Building React dev runtime..."
1174
+ );
1175
+
1176
+ devAssets =
1177
+ await buildDevAssets(
1178
+ rootDirectory
1179
+ );
1180
+
1181
+ /*
1182
+ * =================================
1183
+ * Incremental Client Bundler
1184
+ * =================================
1185
+ */
1186
+
1187
+ console.log(
1188
+ "Creating incremental client bundler..."
1189
+ );
1190
+
1191
+ clientBundler =
1192
+ await DevClientBundler
1193
+ .create(
1194
+ routes,
1195
+
1196
+ rootDirectory,
1197
+
1198
+ {
1199
+ fastRefresh:
1200
+ true,
1201
+ }
1202
+ );
1203
+
1204
+ clientBundles =
1205
+ clientBundler
1206
+ .getBundles();
1207
+
1208
+ for (
1209
+ const bundle
1210
+ of clientBundles
1211
+ ) {
1212
+ console.log(
1213
+ ` ${bundle.pathname} -> ${bundle.publicPath}`
1214
+ );
1215
+ }
1216
+
1217
+ /*
1218
+ * =================================
1219
+ * HTTP
1220
+ * =================================
1221
+ */
1222
+
1223
+ await new Promise<void>(
1224
+ (
1225
+ resolve,
1226
+ reject
1227
+ ) => {
1228
+ server.once(
1229
+ "error",
1230
+ reject
1231
+ );
1232
+
1233
+ server.listen(
1234
+ port,
1235
+ hostname,
1236
+ () => {
1237
+ server.off(
1238
+ "error",
1239
+ reject
1240
+ );
1241
+
1242
+ console.log("");
1243
+ console.log(
1244
+ "BCP Framework"
1245
+ );
1246
+
1247
+ console.log(
1248
+ `Local: http://${hostname}:${port}`
1249
+ );
1250
+
1251
+ console.log("");
1252
+
1253
+ resolve();
1254
+ }
1255
+ );
1256
+ }
1257
+ );
1258
+
1259
+ startWatcher();
1260
+ },
1261
+
1262
+ /*
1263
+ * =================================
1264
+ * Stop
1265
+ * =================================
1266
+ */
1267
+
1268
+ async stop() {
1269
+ if (
1270
+ rebuildTimer
1271
+ ) {
1272
+ clearTimeout(
1273
+ rebuildTimer
1274
+ );
1275
+
1276
+ rebuildTimer =
1277
+ null;
1278
+ }
1279
+
1280
+ if (
1281
+ watcher
1282
+ ) {
1283
+ await watcher.close();
1284
+
1285
+ watcher =
1286
+ null;
1287
+ }
1288
+
1289
+ hmr.close();
1290
+
1291
+ /*
1292
+ * esbuild contexts
1293
+ */
1294
+ if (
1295
+ clientBundler
1296
+ ) {
1297
+ await clientBundler
1298
+ .dispose();
1299
+
1300
+ clientBundler =
1301
+ null;
1302
+ }
1303
+
1304
+ if (
1305
+ !server.listening
1306
+ ) {
1307
+ return;
1308
+ }
1309
+
1310
+ await new Promise<void>(
1311
+ (
1312
+ resolve,
1313
+ reject
1314
+ ) => {
1315
+ server.close(
1316
+ (error) => {
1317
+ if (
1318
+ error
1319
+ ) {
1320
+ reject(
1321
+ error
1322
+ );
1323
+
1324
+ return;
1325
+ }
1326
+
1327
+ resolve();
1328
+ }
1329
+ );
1330
+ }
1331
+ );
1332
+ },
1333
+ };
1334
+ }
1335
+
1336
+ /*
1337
+ * =====================================
1338
+ * API Route
1339
+ * =====================================
1340
+ */
1341
+
1342
+ async function handleApiRoute(
1343
+ req: http.IncomingMessage,
1344
+
1345
+ res: http.ServerResponse,
1346
+
1347
+ requestUrl: URL,
1348
+
1349
+ route: ApiRoute,
1350
+
1351
+ params: Record<
1352
+ string,
1353
+ string
1354
+ >,
1355
+
1356
+ moduleVersion: number
1357
+ ) {
1358
+ const moduleUrl =
1359
+ createVersionedModuleUrl(
1360
+ route.filePath,
1361
+ moduleVersion
1362
+ );
1363
+
1364
+ const apiModule =
1365
+ await import(
1366
+ moduleUrl
1367
+ );
1368
+
1369
+ const method =
1370
+ normalizeHttpMethod(
1371
+ req.method
1372
+ );
1373
+
1374
+ const allowedMethods =
1375
+ getAllowedMethods(
1376
+ apiModule
1377
+ );
1378
+
1379
+ /*
1380
+ * Automatic OPTIONS
1381
+ */
1382
+ if (
1383
+ method ===
1384
+ "OPTIONS" &&
1385
+ !getApiHandler(
1386
+ apiModule,
1387
+ "OPTIONS"
1388
+ )
1389
+ ) {
1390
+ res.writeHead(
1391
+ 204,
1392
+ {
1393
+ Allow:
1394
+ allowedMethods.join(
1395
+ ", "
1396
+ ),
1397
+ }
1398
+ );
1399
+
1400
+ res.end();
1401
+
1402
+ return;
1403
+ }
1404
+
1405
+ let handler =
1406
+ getApiHandler(
1407
+ apiModule,
1408
+ method
1409
+ );
1410
+
1411
+ /*
1412
+ * HEAD -> GET
1413
+ */
1414
+ if (
1415
+ !handler &&
1416
+ method ===
1417
+ "HEAD"
1418
+ ) {
1419
+ handler =
1420
+ getApiHandler(
1421
+ apiModule,
1422
+ "GET"
1423
+ );
1424
+ }
1425
+
1426
+ if (
1427
+ !handler
1428
+ ) {
1429
+ sendMethodNotAllowed(
1430
+ res,
1431
+ allowedMethods
1432
+ );
1433
+
1434
+ return;
1435
+ }
1436
+
1437
+ const request =
1438
+ await createWebRequest(
1439
+ req,
1440
+ requestUrl
1441
+ );
1442
+
1443
+ const response =
1444
+ await handler(
1445
+ request,
1446
+ {
1447
+ params,
1448
+ }
1449
+ );
1450
+
1451
+ if (
1452
+ !(
1453
+ response instanceof
1454
+ Response
1455
+ )
1456
+ ) {
1457
+ throw new Error(
1458
+ `API handler "${method} ${route.pathname}" must return a Response object.`
1459
+ );
1460
+ }
1461
+
1462
+ await sendWebResponse(
1463
+ res,
1464
+ response,
1465
+ method === "HEAD"
1466
+ );
1467
+ }
1468
+
1469
+ /*
1470
+ * =====================================
1471
+ * API Helpers
1472
+ * =====================================
1473
+ */
1474
+
1475
+ function normalizeHttpMethod(
1476
+ method:
1477
+ string | undefined
1478
+ ): HttpMethod {
1479
+ return (
1480
+ (
1481
+ method ??
1482
+ "GET"
1483
+ ).toUpperCase() as
1484
+ HttpMethod
1485
+ );
1486
+ }
1487
+
1488
+ function getApiHandler(
1489
+ module:
1490
+ Record<
1491
+ string,
1492
+ unknown
1493
+ >,
1494
+
1495
+ method: string
1496
+ ): ApiHandler | undefined {
1497
+ const value =
1498
+ module[
1499
+ method
1500
+ ];
1501
+
1502
+ if (
1503
+ typeof value !==
1504
+ "function"
1505
+ ) {
1506
+ return undefined;
1507
+ }
1508
+
1509
+ return (
1510
+ value as ApiHandler
1511
+ );
1512
+ }
1513
+
1514
+ function getAllowedMethods(
1515
+ module:
1516
+ Record<
1517
+ string,
1518
+ unknown
1519
+ >
1520
+ ): string[] {
1521
+ const allowed:
1522
+ string[] = [];
1523
+
1524
+ for (
1525
+ const method
1526
+ of HTTP_METHODS
1527
+ ) {
1528
+ if (
1529
+ method ===
1530
+ "OPTIONS"
1531
+ ) {
1532
+ continue;
1533
+ }
1534
+
1535
+ if (
1536
+ method ===
1537
+ "HEAD"
1538
+ ) {
1539
+ if (
1540
+ getApiHandler(
1541
+ module,
1542
+ "HEAD"
1543
+ ) ||
1544
+ getApiHandler(
1545
+ module,
1546
+ "GET"
1547
+ )
1548
+ ) {
1549
+ allowed.push(
1550
+ "HEAD"
1551
+ );
1552
+ }
1553
+
1554
+ continue;
1555
+ }
1556
+
1557
+ if (
1558
+ getApiHandler(
1559
+ module,
1560
+ method
1561
+ )
1562
+ ) {
1563
+ allowed.push(
1564
+ method
1565
+ );
1566
+ }
1567
+ }
1568
+
1569
+ allowed.push(
1570
+ "OPTIONS"
1571
+ );
1572
+
1573
+ return allowed;
1574
+ }
1575
+
1576
+ /*
1577
+ * =====================================
1578
+ * Node Request → Web Request
1579
+ * =====================================
1580
+ */
1581
+
1582
+ async function createWebRequest(
1583
+ req: http.IncomingMessage,
1584
+ url: URL
1585
+ ): Promise<Request> {
1586
+ const headers =
1587
+ new Headers();
1588
+
1589
+ for (
1590
+ const [
1591
+ key,
1592
+ value,
1593
+ ]
1594
+ of Object.entries(
1595
+ req.headers
1596
+ )
1597
+ ) {
1598
+ if (
1599
+ value ===
1600
+ undefined
1601
+ ) {
1602
+ continue;
1603
+ }
1604
+
1605
+ if (
1606
+ Array.isArray(
1607
+ value
1608
+ )
1609
+ ) {
1610
+ for (
1611
+ const item
1612
+ of value
1613
+ ) {
1614
+ headers.append(
1615
+ key,
1616
+ item
1617
+ );
1618
+ }
1619
+
1620
+ continue;
1621
+ }
1622
+
1623
+ headers.set(
1624
+ key,
1625
+ value
1626
+ );
1627
+ }
1628
+
1629
+ const method =
1630
+ (
1631
+ req.method ??
1632
+ "GET"
1633
+ ).toUpperCase();
1634
+
1635
+ const init:
1636
+ RequestInit = {
1637
+ method,
1638
+ headers,
1639
+ };
1640
+
1641
+ if (
1642
+ method !==
1643
+ "GET" &&
1644
+ method !==
1645
+ "HEAD"
1646
+ ) {
1647
+ const body =
1648
+ await readRequestBody(
1649
+ req
1650
+ );
1651
+
1652
+ if (
1653
+ body.length >
1654
+ 0
1655
+ ) {
1656
+ init.body =
1657
+ body as unknown as BodyInit;
1658
+ }
1659
+ }
1660
+
1661
+ return new Request(
1662
+ url,
1663
+ init
1664
+ );
1665
+ }
1666
+
1667
+ async function readRequestBody(
1668
+ req: http.IncomingMessage
1669
+ ): Promise<Buffer> {
1670
+ const chunks:
1671
+ Buffer[] = [];
1672
+
1673
+ for await (
1674
+ const chunk
1675
+ of req
1676
+ ) {
1677
+ chunks.push(
1678
+ Buffer.isBuffer(
1679
+ chunk
1680
+ )
1681
+ ? chunk
1682
+ : Buffer.from(
1683
+ chunk
1684
+ )
1685
+ );
1686
+ }
1687
+
1688
+ return Buffer.concat(
1689
+ chunks
1690
+ );
1691
+ }
1692
+
1693
+ /*
1694
+ * =====================================
1695
+ * Web Response → Node Response
1696
+ * =====================================
1697
+ */
1698
+
1699
+ async function sendWebResponse(
1700
+ res: http.ServerResponse,
1701
+
1702
+ response: Response,
1703
+
1704
+ isHead: boolean
1705
+ ) {
1706
+ res.statusCode =
1707
+ response.status;
1708
+
1709
+ response.headers.forEach(
1710
+ (
1711
+ value,
1712
+ key
1713
+ ) => {
1714
+ if (
1715
+ key.toLowerCase() ===
1716
+ "set-cookie"
1717
+ ) {
1718
+ return;
1719
+ }
1720
+
1721
+ res.setHeader(
1722
+ key,
1723
+ value
1724
+ );
1725
+ }
1726
+ );
1727
+
1728
+ const responseHeaders =
1729
+ response.headers as
1730
+ Headers & {
1731
+ getSetCookie?: () =>
1732
+ string[];
1733
+ };
1734
+
1735
+ const cookies =
1736
+ responseHeaders
1737
+ .getSetCookie?.();
1738
+
1739
+ if (
1740
+ cookies &&
1741
+ cookies.length >
1742
+ 0
1743
+ ) {
1744
+ res.setHeader(
1745
+ "Set-Cookie",
1746
+ cookies
1747
+ );
1748
+ }
1749
+
1750
+ if (
1751
+ response.status ===
1752
+ 204 ||
1753
+ response.status ===
1754
+ 304
1755
+ ) {
1756
+ res.end();
1757
+
1758
+ return;
1759
+ }
1760
+
1761
+ const arrayBuffer =
1762
+ await response
1763
+ .arrayBuffer();
1764
+
1765
+ const body =
1766
+ Buffer.from(
1767
+ arrayBuffer
1768
+ );
1769
+
1770
+ if (
1771
+ !res.hasHeader(
1772
+ "Content-Length"
1773
+ )
1774
+ ) {
1775
+ res.setHeader(
1776
+ "Content-Length",
1777
+ body.length
1778
+ );
1779
+ }
1780
+
1781
+ if (
1782
+ isHead
1783
+ ) {
1784
+ res.end();
1785
+
1786
+ return;
1787
+ }
1788
+
1789
+ res.end(
1790
+ body
1791
+ );
1792
+ }
1793
+
1794
+ /*
1795
+ * =====================================
1796
+ * React Server Import
1797
+ * =====================================
1798
+ */
1799
+
1800
+ async function importComponent(
1801
+ filePath: string,
1802
+
1803
+ componentType: string,
1804
+
1805
+ moduleVersion: number
1806
+ ): Promise<
1807
+ ComponentType<any>
1808
+ > {
1809
+ const moduleUrl =
1810
+ createVersionedModuleUrl(
1811
+ filePath,
1812
+ moduleVersion
1813
+ );
1814
+
1815
+ const module =
1816
+ await import(
1817
+ moduleUrl
1818
+ );
1819
+
1820
+ const Component =
1821
+ module.default;
1822
+
1823
+ if (
1824
+ typeof Component !==
1825
+ "function"
1826
+ ) {
1827
+ throw new Error(
1828
+ `${componentType} "${filePath}" does not export a default React component.`
1829
+ );
1830
+ }
1831
+
1832
+ return Component;
1833
+ }
1834
+
1835
+ /*
1836
+ * =====================================
1837
+ * Server Cache Busting
1838
+ * =====================================
1839
+ */
1840
+
1841
+ function createVersionedModuleUrl(
1842
+ filePath: string,
1843
+ version: number
1844
+ ): string {
1845
+ const url =
1846
+ pathToFileURL(
1847
+ filePath
1848
+ );
1849
+
1850
+ url.searchParams.set(
1851
+ "bcpv",
1852
+ String(
1853
+ version
1854
+ )
1855
+ );
1856
+
1857
+ return url.href;
1858
+ }
1859
+
1860
+ /*
1861
+ * =====================================
1862
+ * URL
1863
+ * =====================================
1864
+ */
1865
+
1866
+ function normalizePathname(
1867
+ pathname: string
1868
+ ): string {
1869
+ if (
1870
+ pathname.length >
1871
+ 1 &&
1872
+ pathname.endsWith(
1873
+ "/"
1874
+ )
1875
+ ) {
1876
+ return pathname.slice(
1877
+ 0,
1878
+ -1
1879
+ );
1880
+ }
1881
+
1882
+ return pathname;
1883
+ }
1884
+
1885
+ /*
1886
+ * =====================================
1887
+ * HTML
1888
+ * =====================================
1889
+ */
1890
+
1891
+ function renderDocument(
1892
+ content: string,
1893
+
1894
+ clientScript: string,
1895
+
1896
+ frameworkData:
1897
+ FrameworkData,
1898
+
1899
+ clientVersion: number
1900
+ ): string {
1901
+ const serializedData =
1902
+ serializeFrameworkData(
1903
+ frameworkData
1904
+ );
1905
+
1906
+ const devConfig =
1907
+ JSON.stringify({
1908
+ eventsPath:
1909
+ DEV_EVENTS_PATH,
1910
+
1911
+ clientScript,
1912
+
1913
+ /*
1914
+ * Route pattern
1915
+ *
1916
+ * เช่น:
1917
+ *
1918
+ * /users/[id]
1919
+ */
1920
+ route:
1921
+ frameworkData.route,
1922
+
1923
+ version:
1924
+ clientVersion,
1925
+ })
1926
+ .replace(
1927
+ /</g,
1928
+ "\\u003c"
1929
+ );
1930
+
1931
+ const runtimeUrl =
1932
+ `${DEV_RUNTIME_PATH}?v=${clientVersion}`;
1933
+
1934
+ const clientUrl =
1935
+ `${clientScript}?v=${clientVersion}`;
1936
+
1937
+ return `
1938
+ <!DOCTYPE html>
1939
+
1940
+ <html lang="en">
1941
+ <head>
1942
+ <meta charset="UTF-8" />
1943
+
1944
+ <meta
1945
+ name="viewport"
1946
+ content="width=device-width, initial-scale=1.0"
1947
+ />
1948
+
1949
+ <title>
1950
+ BCP Framework
1951
+ </title>
1952
+ </head>
1953
+
1954
+ <body>
1955
+ <div id="root">${content}</div>
1956
+
1957
+ <script
1958
+ id="__BCP_DATA__"
1959
+ type="application/json"
1960
+ >${serializedData}</script>
1961
+
1962
+ <script>
1963
+ globalThis.__BCP_DEV_CONFIG__ =
1964
+ ${devConfig};
1965
+ </script>
1966
+
1967
+ <script
1968
+ type="module"
1969
+ data-bcp-bootstrap
1970
+ >
1971
+ const runtimeUrl =
1972
+ ${JSON.stringify(runtimeUrl)};
1973
+
1974
+ const clientUrl =
1975
+ ${JSON.stringify(clientUrl)};
1976
+
1977
+ const bootstrap =
1978
+ async () => {
1979
+ try {
1980
+ /*
1981
+ * Keep React Refresh ordering intact while
1982
+ * removing the external classic script from
1983
+ * the parser's critical rendering path.
1984
+ */
1985
+ await import(
1986
+ runtimeUrl
1987
+ );
1988
+
1989
+ await import(
1990
+ clientUrl
1991
+ );
1992
+ } catch (error) {
1993
+ console.error(
1994
+ "[BCP] Client bootstrap failed:",
1995
+ error
1996
+ );
1997
+ }
1998
+ };
1999
+
2000
+ requestAnimationFrame(
2001
+ () => {
2002
+ void bootstrap();
2003
+ }
2004
+ );
2005
+ </script>
2006
+ </body>
2007
+ </html>
2008
+ `.trim();
2009
+ }
2010
+
2011
+ function serializeFrameworkData(
2012
+ data:
2013
+ FrameworkData
2014
+ ): string {
2015
+ return JSON.stringify(
2016
+ data
2017
+ )
2018
+ .replace(
2019
+ /</g,
2020
+ "\\u003c"
2021
+ )
2022
+ .replace(
2023
+ /\u2028/g,
2024
+ "\\u2028"
2025
+ )
2026
+ .replace(
2027
+ /\u2029/g,
2028
+ "\\u2029"
2029
+ );
2030
+ }
2031
+
2032
+ /*
2033
+ * =====================================
2034
+ * Responses
2035
+ * =====================================
2036
+ */
2037
+
2038
+ function sendHtml(
2039
+ res:
2040
+ http.ServerResponse,
2041
+
2042
+ html: string
2043
+ ) {
2044
+ res.writeHead(
2045
+ 200,
2046
+ {
2047
+ "Content-Type":
2048
+ "text/html; charset=utf-8",
2049
+
2050
+ "Cache-Control":
2051
+ "no-store",
2052
+ }
2053
+ );
2054
+
2055
+ res.end(
2056
+ html
2057
+ );
2058
+ }
2059
+
2060
+ function sendJavaScript(
2061
+ res:
2062
+ http.ServerResponse,
2063
+
2064
+ content: Buffer
2065
+ ) {
2066
+ res.writeHead(
2067
+ 200,
2068
+ {
2069
+ "Content-Type":
2070
+ "text/javascript; charset=utf-8",
2071
+
2072
+ "Cache-Control":
2073
+ "no-store",
2074
+
2075
+ "Content-Length":
2076
+ content.length,
2077
+ }
2078
+ );
2079
+
2080
+ res.end(
2081
+ content
2082
+ );
2083
+ }
2084
+
2085
+ function sendDevAsset(
2086
+ res:
2087
+ http.ServerResponse,
2088
+
2089
+ asset:
2090
+ DevAsset
2091
+ ) {
2092
+ res.writeHead(
2093
+ 200,
2094
+ {
2095
+ "Content-Type":
2096
+ asset.contentType,
2097
+
2098
+ "Cache-Control":
2099
+ "no-store",
2100
+
2101
+ "Content-Length":
2102
+ asset.content
2103
+ .length,
2104
+ }
2105
+ );
2106
+
2107
+ res.end(
2108
+ asset.content
2109
+ );
2110
+ }
2111
+
2112
+ function sendMethodNotAllowed(
2113
+ res:
2114
+ http.ServerResponse,
2115
+
2116
+ methods:
2117
+ string[]
2118
+ ) {
2119
+ const body =
2120
+ JSON.stringify({
2121
+ error:
2122
+ "Method Not Allowed",
2123
+
2124
+ allowedMethods:
2125
+ methods,
2126
+ });
2127
+
2128
+ res.writeHead(
2129
+ 405,
2130
+ {
2131
+ "Content-Type":
2132
+ "application/json; charset=utf-8",
2133
+
2134
+ Allow:
2135
+ methods.join(
2136
+ ", "
2137
+ ),
2138
+
2139
+ "Content-Length":
2140
+ Buffer.byteLength(
2141
+ body
2142
+ ),
2143
+ }
2144
+ );
2145
+
2146
+ res.end(
2147
+ body
2148
+ );
2149
+ }
2150
+
2151
+ function sendNotFound(
2152
+ res:
2153
+ http.ServerResponse
2154
+ ) {
2155
+ const html = `
2156
+ <!DOCTYPE html>
2157
+
2158
+ <html lang="en">
2159
+ <body>
2160
+ <h1>
2161
+ 404
2162
+ </h1>
2163
+
2164
+ <p>
2165
+ Page Not Found
2166
+ </p>
2167
+ </body>
2168
+ </html>
2169
+ `.trim();
2170
+
2171
+ res.writeHead(
2172
+ 404,
2173
+ {
2174
+ "Content-Type":
2175
+ "text/html; charset=utf-8",
2176
+
2177
+ "Cache-Control":
2178
+ "no-store",
2179
+ }
2180
+ );
2181
+
2182
+ res.end(
2183
+ html
2184
+ );
2185
+ }
2186
+
2187
+ function sendInternalServerError(
2188
+ res:
2189
+ http.ServerResponse,
2190
+
2191
+ error:
2192
+ unknown
2193
+ ) {
2194
+ const message =
2195
+ error instanceof
2196
+ Error
2197
+ ? error.message
2198
+ : "Unknown error";
2199
+
2200
+ const html = `
2201
+ <!DOCTYPE html>
2202
+
2203
+ <html lang="en">
2204
+ <body>
2205
+ <h1>
2206
+ 500
2207
+ </h1>
2208
+
2209
+ <p>
2210
+ Internal Server Error
2211
+ </p>
2212
+
2213
+ <pre>${escapeHtml(message)}</pre>
2214
+ </body>
2215
+ </html>
2216
+ `.trim();
2217
+
2218
+ res.writeHead(
2219
+ 500,
2220
+ {
2221
+ "Content-Type":
2222
+ "text/html; charset=utf-8",
2223
+
2224
+ "Cache-Control":
2225
+ "no-store",
2226
+ }
2227
+ );
2228
+
2229
+ res.end(
2230
+ html
2231
+ );
2232
+ }
2233
+
2234
+ /*
2235
+ * =====================================
2236
+ * Security
2237
+ * =====================================
2238
+ */
2239
+
2240
+ function escapeHtml(
2241
+ value:
2242
+ string
2243
+ ): string {
2244
+ return value
2245
+ .replaceAll(
2246
+ "&",
2247
+ "&amp;"
2248
+ )
2249
+ .replaceAll(
2250
+ "<",
2251
+ "&lt;"
2252
+ )
2253
+ .replaceAll(
2254
+ ">",
2255
+ "&gt;"
2256
+ )
2257
+ .replaceAll(
2258
+ '"',
2259
+ "&quot;"
2260
+ )
2261
+ .replaceAll(
2262
+ "'",
2263
+ "&#039;"
2264
+ );
2265
+ }