@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,1991 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ createRequire,
6
+ } from "node:module";
7
+
8
+ import {
9
+ build,
10
+ context,
11
+ type BuildContext,
12
+ type BuildResult,
13
+ type Plugin,
14
+ } from "esbuild";
15
+
16
+ import {
17
+ transformAsync,
18
+ } from "@babel/core";
19
+
20
+ import {
21
+ createClientEnvironmentDefines,
22
+ } from "../../env/src/index.js";
23
+
24
+ import type {
25
+ Route,
26
+ } from "../../router/src/index.js";
27
+
28
+ /*
29
+ * =====================================
30
+ * CommonJS / Babel Helpers
31
+ * =====================================
32
+ */
33
+
34
+ const require =
35
+ createRequire(
36
+ import.meta.url
37
+ );
38
+
39
+ function unwrapModule<T>(
40
+ value: any
41
+ ): T {
42
+ return (
43
+ value?.default ??
44
+ value
45
+ ) as T;
46
+ }
47
+
48
+ const reactRefreshBabel =
49
+ unwrapModule<any>(
50
+ require(
51
+ "react-refresh/babel"
52
+ )
53
+ );
54
+
55
+ const presetReact =
56
+ unwrapModule<any>(
57
+ require(
58
+ "@babel/preset-react"
59
+ )
60
+ );
61
+
62
+ const presetTypeScript =
63
+ unwrapModule<any>(
64
+ require(
65
+ "@babel/preset-typescript"
66
+ )
67
+ );
68
+
69
+ /*
70
+ * =====================================
71
+ * Types
72
+ * =====================================
73
+ */
74
+
75
+ export interface ClientBundle {
76
+ pathname: string;
77
+
78
+ publicPath: string;
79
+
80
+ filePath: string;
81
+
82
+ content: Buffer;
83
+ }
84
+
85
+ export interface DevAsset {
86
+ publicPath: string;
87
+
88
+ filePath: string;
89
+
90
+ content: Buffer;
91
+
92
+ contentType: string;
93
+ }
94
+
95
+ export interface ClientBuildOptions {
96
+ fastRefresh?: boolean;
97
+ }
98
+
99
+ interface RouteBuildState {
100
+ route: Route;
101
+
102
+ context: BuildContext;
103
+
104
+ outputFile: string;
105
+
106
+ bundle: ClientBundle;
107
+
108
+ /*
109
+ * Dependency files ของ route นี้
110
+ */
111
+ inputFiles: Set<string>;
112
+ }
113
+
114
+ interface RouteBuildSnapshot {
115
+ bundle: ClientBundle;
116
+
117
+ inputFiles: Set<string>;
118
+ }
119
+
120
+ /*
121
+ * =====================================
122
+ * Public Paths
123
+ * =====================================
124
+ */
125
+
126
+ const VENDOR_PATH =
127
+ "/_bcp/vendor";
128
+
129
+ export const DEV_RUNTIME_PATH =
130
+ "/_bcp/dev-runtime.js";
131
+
132
+ /*
133
+ * =====================================
134
+ * Vendor Proxy
135
+ * =====================================
136
+ */
137
+
138
+ function createVendorProxySource(
139
+ packageName: string
140
+ ): string {
141
+ const packageModule =
142
+ require(
143
+ packageName
144
+ );
145
+
146
+ const exportNames =
147
+ Object.keys(
148
+ packageModule
149
+ )
150
+ .filter(
151
+ (name) =>
152
+ name !==
153
+ "default" &&
154
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(
155
+ name
156
+ )
157
+ );
158
+
159
+ const namedExports =
160
+ exportNames
161
+ .map(
162
+ (name) => `
163
+ export const ${name} =
164
+ __bcpModule[${JSON.stringify(name)}];
165
+ `.trim()
166
+ )
167
+ .join("\n\n");
168
+
169
+ return `
170
+ import __bcpModule from ${JSON.stringify(packageName)};
171
+
172
+ ${namedExports}
173
+
174
+ export default __bcpModule;
175
+ `.trim();
176
+ }
177
+
178
+ /*
179
+ * =====================================
180
+ * React Vendor + Refresh Runtime
181
+ * =====================================
182
+ */
183
+
184
+ export async function buildDevAssets(
185
+ rootDirectory: string
186
+ ): Promise<DevAsset[]> {
187
+ const frameworkDirectory =
188
+ path.join(
189
+ rootDirectory,
190
+ ".bcp-framework"
191
+ );
192
+
193
+ const vendorSourceDirectory =
194
+ path.join(
195
+ frameworkDirectory,
196
+ "vendor-src"
197
+ );
198
+
199
+ const vendorOutputDirectory =
200
+ path.join(
201
+ frameworkDirectory,
202
+ "vendor"
203
+ );
204
+
205
+ const runtimeDirectory =
206
+ path.join(
207
+ frameworkDirectory,
208
+ "runtime"
209
+ );
210
+
211
+ fs.rmSync(
212
+ vendorSourceDirectory,
213
+ {
214
+ recursive: true,
215
+ force: true,
216
+ }
217
+ );
218
+
219
+ fs.rmSync(
220
+ vendorOutputDirectory,
221
+ {
222
+ recursive: true,
223
+ force: true,
224
+ }
225
+ );
226
+
227
+ fs.rmSync(
228
+ runtimeDirectory,
229
+ {
230
+ recursive: true,
231
+ force: true,
232
+ }
233
+ );
234
+
235
+ fs.mkdirSync(
236
+ vendorSourceDirectory,
237
+ {
238
+ recursive: true,
239
+ }
240
+ );
241
+
242
+ fs.mkdirSync(
243
+ vendorOutputDirectory,
244
+ {
245
+ recursive: true,
246
+ }
247
+ );
248
+
249
+ fs.mkdirSync(
250
+ runtimeDirectory,
251
+ {
252
+ recursive: true,
253
+ }
254
+ );
255
+
256
+ /*
257
+ * =================================
258
+ * Vendor Entry Files
259
+ * =================================
260
+ */
261
+
262
+ const reactEntry =
263
+ path.join(
264
+ vendorSourceDirectory,
265
+ "react.ts"
266
+ );
267
+
268
+ const jsxRuntimeEntry =
269
+ path.join(
270
+ vendorSourceDirectory,
271
+ "react-jsx-runtime.ts"
272
+ );
273
+
274
+ const jsxDevRuntimeEntry =
275
+ path.join(
276
+ vendorSourceDirectory,
277
+ "react-jsx-dev-runtime.ts"
278
+ );
279
+
280
+ const reactDomEntry =
281
+ path.join(
282
+ vendorSourceDirectory,
283
+ "react-dom.ts"
284
+ );
285
+
286
+ const reactDomClientEntry =
287
+ path.join(
288
+ vendorSourceDirectory,
289
+ "react-dom-client.ts"
290
+ );
291
+
292
+ fs.writeFileSync(
293
+ reactEntry,
294
+ createVendorProxySource(
295
+ "react"
296
+ )
297
+ );
298
+
299
+ fs.writeFileSync(
300
+ jsxRuntimeEntry,
301
+ createVendorProxySource(
302
+ "react/jsx-runtime"
303
+ )
304
+ );
305
+
306
+ fs.writeFileSync(
307
+ jsxDevRuntimeEntry,
308
+ createVendorProxySource(
309
+ "react/jsx-dev-runtime"
310
+ )
311
+ );
312
+
313
+ fs.writeFileSync(
314
+ reactDomEntry,
315
+ createVendorProxySource(
316
+ "react-dom"
317
+ )
318
+ );
319
+
320
+ fs.writeFileSync(
321
+ reactDomClientEntry,
322
+ createVendorProxySource(
323
+ "react-dom/client"
324
+ )
325
+ );
326
+
327
+ /*
328
+ * Build shared React vendor
329
+ */
330
+ await build({
331
+ entryPoints: {
332
+ react:
333
+ reactEntry,
334
+
335
+ "react-jsx-runtime":
336
+ jsxRuntimeEntry,
337
+
338
+ "react-jsx-dev-runtime":
339
+ jsxDevRuntimeEntry,
340
+
341
+ "react-dom":
342
+ reactDomEntry,
343
+
344
+ "react-dom-client":
345
+ reactDomClientEntry,
346
+ },
347
+
348
+ outdir:
349
+ vendorOutputDirectory,
350
+
351
+ bundle:
352
+ true,
353
+
354
+ splitting:
355
+ true,
356
+
357
+ format:
358
+ "esm",
359
+
360
+ platform:
361
+ "browser",
362
+
363
+ target: [
364
+ "es2022",
365
+ ],
366
+
367
+ entryNames:
368
+ "[name]",
369
+
370
+ chunkNames:
371
+ "chunks/[name]-[hash]",
372
+
373
+ define: {
374
+ "process.env.NODE_ENV":
375
+ '"development"',
376
+ },
377
+
378
+ sourcemap:
379
+ "inline",
380
+
381
+ logLevel:
382
+ "silent",
383
+ });
384
+
385
+ /*
386
+ * =================================
387
+ * HMR Runtime
388
+ * =================================
389
+ */
390
+
391
+ const runtimeOutput =
392
+ path.join(
393
+ runtimeDirectory,
394
+ "dev-runtime.js"
395
+ );
396
+
397
+ const runtimeSource = `
398
+ import RefreshRuntime from "react-refresh/runtime";
399
+
400
+ /*
401
+ * Inject React Refresh
402
+ */
403
+ if (!globalThis.__BCP_REFRESH_RUNTIME__) {
404
+ RefreshRuntime.injectIntoGlobalHook(
405
+ window
406
+ );
407
+
408
+ globalThis.__BCP_REFRESH_RUNTIME__ =
409
+ RefreshRuntime;
410
+ }
411
+
412
+ const config =
413
+ globalThis.__BCP_DEV_CONFIG__;
414
+
415
+ if (!config) {
416
+ throw new Error(
417
+ "BCP Framework: dev configuration was not found."
418
+ );
419
+ }
420
+
421
+ /*
422
+ * EventSource เปิดแค่ครั้งเดียว
423
+ */
424
+ if (!globalThis.__BCP_HMR_SOURCE__) {
425
+ const source =
426
+ new EventSource(
427
+ config.eventsPath
428
+ );
429
+
430
+ globalThis.__BCP_HMR_SOURCE__ =
431
+ source;
432
+
433
+ /*
434
+ * =================================
435
+ * Fast Refresh
436
+ * =================================
437
+ */
438
+
439
+ source.addEventListener(
440
+ "refresh",
441
+ async (event) => {
442
+ const data =
443
+ JSON.parse(
444
+ event.data
445
+ );
446
+
447
+ /*
448
+ * สำคัญ:
449
+ *
450
+ * Server ส่ง affected routes มา
451
+ *
452
+ * ถ้า route ปัจจุบันไม่ได้รับผลกระทบ
453
+ * ไม่ต้อง import bundle ใหม่
454
+ */
455
+ if (
456
+ Array.isArray(
457
+ data.routes
458
+ ) &&
459
+ !data.routes.includes(
460
+ config.route
461
+ )
462
+ ) {
463
+ return;
464
+ }
465
+
466
+ const separator =
467
+ config.clientScript.includes("?")
468
+ ? "&"
469
+ : "?";
470
+
471
+ const moduleUrl =
472
+ config.clientScript +
473
+ separator +
474
+ "v=" +
475
+ encodeURIComponent(
476
+ data.version
477
+ );
478
+
479
+ try {
480
+ await import(
481
+ moduleUrl
482
+ );
483
+
484
+ RefreshRuntime
485
+ .performReactRefresh();
486
+
487
+ console.log(
488
+ "[BCP] Fast Refresh applied:",
489
+ config.route
490
+ );
491
+ } catch (error) {
492
+ console.error(
493
+ "[BCP] Fast Refresh failed:",
494
+ error
495
+ );
496
+
497
+ window.location.reload();
498
+ }
499
+ }
500
+ );
501
+
502
+ /*
503
+ * =================================
504
+ * Full Reload
505
+ * =================================
506
+ */
507
+
508
+ source.addEventListener(
509
+ "reload",
510
+ () => {
511
+ console.log(
512
+ "[BCP] Full reload"
513
+ );
514
+
515
+ window.location.reload();
516
+ }
517
+ );
518
+ }
519
+ `.trim();
520
+
521
+ await build({
522
+ stdin: {
523
+ contents:
524
+ runtimeSource,
525
+
526
+ resolveDir:
527
+ rootDirectory,
528
+
529
+ sourcefile:
530
+ "bcp-dev-runtime.js",
531
+
532
+ loader:
533
+ "js",
534
+ },
535
+
536
+ outfile:
537
+ runtimeOutput,
538
+
539
+ bundle:
540
+ true,
541
+
542
+ format:
543
+ "iife",
544
+
545
+ platform:
546
+ "browser",
547
+
548
+ target: [
549
+ "es2022",
550
+ ],
551
+
552
+ define: {
553
+ "process.env.NODE_ENV":
554
+ '"development"',
555
+ },
556
+
557
+ sourcemap:
558
+ "inline",
559
+
560
+ logLevel:
561
+ "silent",
562
+ });
563
+
564
+ /*
565
+ * =================================
566
+ * Assets → RAM
567
+ * =================================
568
+ */
569
+
570
+ const assets:
571
+ DevAsset[] = [];
572
+
573
+ collectFiles(
574
+ vendorOutputDirectory,
575
+ (filePath) => {
576
+ const relative =
577
+ path.relative(
578
+ vendorOutputDirectory,
579
+ filePath
580
+ )
581
+ .replace(
582
+ /\\/g,
583
+ "/"
584
+ );
585
+
586
+ assets.push({
587
+ publicPath:
588
+ `${VENDOR_PATH}/${relative}`,
589
+
590
+ filePath,
591
+
592
+ content:
593
+ fs.readFileSync(
594
+ filePath
595
+ ),
596
+
597
+ contentType:
598
+ "text/javascript; charset=utf-8",
599
+ });
600
+ }
601
+ );
602
+
603
+ assets.push({
604
+ publicPath:
605
+ DEV_RUNTIME_PATH,
606
+
607
+ filePath:
608
+ runtimeOutput,
609
+
610
+ content:
611
+ fs.readFileSync(
612
+ runtimeOutput
613
+ ),
614
+
615
+ contentType:
616
+ "text/javascript; charset=utf-8",
617
+ });
618
+
619
+ return assets;
620
+ }
621
+
622
+ /*
623
+ * =====================================
624
+ * Incremental Client Bundler
625
+ * =====================================
626
+ */
627
+
628
+ export class DevClientBundler {
629
+ private readonly states =
630
+ new Map<
631
+ string,
632
+ RouteBuildState
633
+ >();
634
+
635
+ /*
636
+ * Dependency graph:
637
+ *
638
+ * filePath
639
+ *
640
+ * ↓
641
+ *
642
+ * Set<Route Pathname>
643
+ */
644
+ private readonly moduleToRoutes =
645
+ new Map<
646
+ string,
647
+ Set<string>
648
+ >();
649
+
650
+ private constructor(
651
+ private readonly rootDirectory:
652
+ string,
653
+
654
+ private readonly options:
655
+ ClientBuildOptions
656
+ ) { }
657
+
658
+ /*
659
+ * =================================
660
+ * Create
661
+ * =================================
662
+ */
663
+
664
+ static async create(
665
+ routes: Route[],
666
+ rootDirectory: string,
667
+ options:
668
+ ClientBuildOptions = {}
669
+ ): Promise<DevClientBundler> {
670
+ const bundler =
671
+ new DevClientBundler(
672
+ rootDirectory,
673
+ options
674
+ );
675
+
676
+ try {
677
+ await bundler.initialize(
678
+ routes
679
+ );
680
+
681
+ return bundler;
682
+ } catch (error) {
683
+ await bundler.dispose();
684
+
685
+ throw error;
686
+ }
687
+ }
688
+
689
+ /*
690
+ * =================================
691
+ * Initial Build
692
+ * =================================
693
+ */
694
+
695
+ private async initialize(
696
+ routes: Route[]
697
+ ) {
698
+ const outputDirectory =
699
+ path.join(
700
+ this.rootDirectory,
701
+ ".bcp-framework",
702
+ "client"
703
+ );
704
+
705
+ fs.rmSync(
706
+ outputDirectory,
707
+ {
708
+ recursive: true,
709
+ force: true,
710
+ }
711
+ );
712
+
713
+ fs.mkdirSync(
714
+ outputDirectory,
715
+ {
716
+ recursive: true,
717
+ }
718
+ );
719
+
720
+ /*
721
+ * สร้าง esbuild Context
722
+ * ต่อ Route
723
+ */
724
+ for (
725
+ const route
726
+ of routes
727
+ ) {
728
+ const state =
729
+ await this.createRouteState(
730
+ route,
731
+ outputDirectory
732
+ );
733
+
734
+ this.states.set(
735
+ route.pathname,
736
+ state
737
+ );
738
+ }
739
+
740
+ /*
741
+ * Initial build
742
+ *
743
+ * build ทุก route ครั้งแรก
744
+ */
745
+ const states =
746
+ Array.from(
747
+ this.states.values()
748
+ );
749
+
750
+ const snapshots =
751
+ await Promise.all(
752
+ states.map(
753
+ (state) =>
754
+ this.buildState(
755
+ state
756
+ )
757
+ )
758
+ );
759
+
760
+ /*
761
+ * ทุก route build ผ่านแล้ว
762
+ * จึงค่อย commit state
763
+ */
764
+ for (
765
+ let index = 0;
766
+ index <
767
+ states.length;
768
+ index++
769
+ ) {
770
+ this.commitSnapshot(
771
+ states[index],
772
+ snapshots[index]
773
+ );
774
+ }
775
+
776
+ this.rebuildModuleGraph();
777
+ }
778
+
779
+ /*
780
+ * =================================
781
+ * Create Route Context
782
+ * =================================
783
+ */
784
+
785
+ private async createRouteState(
786
+ route: Route,
787
+ outputDirectory: string
788
+ ): Promise<RouteBuildState> {
789
+ const bundleName =
790
+ routeToBundleName(
791
+ route.pathname
792
+ );
793
+
794
+ const outputFile =
795
+ path.join(
796
+ outputDirectory,
797
+ bundleName
798
+ );
799
+
800
+ const clientEntry =
801
+ createClientEntry(
802
+ route,
803
+ this.rootDirectory
804
+ );
805
+
806
+ const plugins:
807
+ Plugin[] = [];
808
+
809
+ if (
810
+ this.options.fastRefresh
811
+ ) {
812
+ plugins.push(
813
+ createReactVendorPlugin()
814
+ );
815
+
816
+ plugins.push(
817
+ createReactRefreshPlugin(
818
+ this.rootDirectory
819
+ )
820
+ );
821
+ }
822
+
823
+ /*
824
+ * esbuild context()
825
+ *
826
+ * Context นี้จะถูกเก็บไว้
827
+ * ตลอด Dev Server lifetime
828
+ */
829
+ const buildContext =
830
+ await context({
831
+ absWorkingDir:
832
+ this.rootDirectory,
833
+
834
+ stdin: {
835
+ contents:
836
+ clientEntry,
837
+
838
+ resolveDir:
839
+ this.rootDirectory,
840
+
841
+ sourcefile:
842
+ `bcp-entry-${sanitizeBundleId(route.pathname)}.js`,
843
+
844
+ loader:
845
+ "js",
846
+ },
847
+
848
+ outfile:
849
+ outputFile,
850
+
851
+ bundle:
852
+ true,
853
+
854
+ /*
855
+ * สำคัญ:
856
+ *
857
+ * ไม่ให้ esbuild เขียนลง disk
858
+ * ทุก rebuild
859
+ *
860
+ * เราจะอ่าน outputFiles
861
+ * เข้า RAM โดยตรง
862
+ */
863
+ write:
864
+ false,
865
+
866
+ metafile:
867
+ true,
868
+
869
+ platform:
870
+ "browser",
871
+
872
+ format:
873
+ "esm",
874
+
875
+ target: [
876
+ "es2022",
877
+ ],
878
+
879
+ plugins,
880
+
881
+ jsx:
882
+ "automatic",
883
+
884
+ define:
885
+ createClientEnvironmentDefines(
886
+ "development"
887
+ ),
888
+
889
+ sourcemap:
890
+ "inline",
891
+
892
+ logLevel:
893
+ "silent",
894
+ });
895
+
896
+ const publicPath =
897
+ `/_bcp/client/${bundleName}`;
898
+
899
+ return {
900
+ route,
901
+
902
+ context:
903
+ buildContext,
904
+
905
+ outputFile,
906
+
907
+ /*
908
+ * Placeholder
909
+ * จะถูกแทนหลัง initial rebuild
910
+ */
911
+ bundle: {
912
+ pathname:
913
+ route.pathname,
914
+
915
+ publicPath,
916
+
917
+ filePath:
918
+ outputFile,
919
+
920
+ content:
921
+ Buffer.alloc(0),
922
+ },
923
+
924
+ inputFiles:
925
+ new Set(),
926
+ };
927
+ }
928
+
929
+ /*
930
+ * =================================
931
+ * Incremental Rebuild
932
+ * =================================
933
+ */
934
+
935
+ async rebuildAffected(
936
+ changedFiles: string[]
937
+ ): Promise<string[]> {
938
+ const affected =
939
+ this.findAffectedRoutes(
940
+ changedFiles
941
+ );
942
+
943
+ if (
944
+ affected.length ===
945
+ 0
946
+ ) {
947
+ return [];
948
+ }
949
+
950
+ const states =
951
+ affected
952
+ .map(
953
+ (pathname) =>
954
+ this.states.get(
955
+ pathname
956
+ )
957
+ )
958
+ .filter(
959
+ (
960
+ state
961
+ ): state is
962
+ RouteBuildState =>
963
+ Boolean(
964
+ state
965
+ )
966
+ );
967
+
968
+ /*
969
+ * context.rebuild()
970
+ *
971
+ * esbuild จะ reuse cache
972
+ * ของ context เดิม
973
+ */
974
+ const snapshots =
975
+ await Promise.all(
976
+ states.map(
977
+ (state) =>
978
+ this.buildState(
979
+ state
980
+ )
981
+ )
982
+ );
983
+
984
+ /*
985
+ * Atomic-ish commit:
986
+ *
987
+ * ถ้า Promise.all fail
988
+ * จะยังไม่ commit bundle ใหม่
989
+ */
990
+ for (
991
+ let index = 0;
992
+ index <
993
+ states.length;
994
+ index++
995
+ ) {
996
+ this.commitSnapshot(
997
+ states[index],
998
+ snapshots[index]
999
+ );
1000
+ }
1001
+
1002
+ /*
1003
+ * Dependency อาจเปลี่ยนหลัง build
1004
+ *
1005
+ * เช่น:
1006
+ *
1007
+ * Page
1008
+ * เดิม import A
1009
+ *
1010
+ * เปลี่ยนเป็น
1011
+ *
1012
+ * Page
1013
+ * import A
1014
+ * import B
1015
+ *
1016
+ * จึง rebuild graph ใหม่
1017
+ */
1018
+ this.rebuildModuleGraph();
1019
+
1020
+ return affected;
1021
+ }
1022
+
1023
+ /*
1024
+ * =================================
1025
+ * Build One Route
1026
+ * =================================
1027
+ */
1028
+
1029
+ private async buildState(
1030
+ state: RouteBuildState
1031
+ ): Promise<RouteBuildSnapshot> {
1032
+ const result =
1033
+ await state.context
1034
+ .rebuild();
1035
+
1036
+ const output =
1037
+ findJavaScriptOutput(
1038
+ result,
1039
+ state.outputFile
1040
+ );
1041
+
1042
+ if (!output) {
1043
+ throw new Error(
1044
+ `BCP Framework: output bundle was not found for route "${state.route.pathname}".`
1045
+ );
1046
+ }
1047
+
1048
+ const inputFiles =
1049
+ collectInputFiles(
1050
+ result,
1051
+ this.rootDirectory
1052
+ );
1053
+
1054
+ /*
1055
+ * Page/Layout ต้องอยู่ graph
1056
+ * เสมอ แม้ metafile มี behavior
1057
+ * แตกต่างจาก plugin
1058
+ */
1059
+ inputFiles.add(
1060
+ normalizeGraphPath(
1061
+ state.route.filePath
1062
+ )
1063
+ );
1064
+
1065
+ for (
1066
+ const layoutFile
1067
+ of state.route.layouts
1068
+ ) {
1069
+ inputFiles.add(
1070
+ normalizeGraphPath(
1071
+ layoutFile
1072
+ )
1073
+ );
1074
+ }
1075
+
1076
+ return {
1077
+ bundle: {
1078
+ pathname:
1079
+ state.route.pathname,
1080
+
1081
+ publicPath:
1082
+ state.bundle.publicPath,
1083
+
1084
+ filePath:
1085
+ state.outputFile,
1086
+
1087
+ content:
1088
+ Buffer.from(
1089
+ output.contents
1090
+ ),
1091
+ },
1092
+
1093
+ inputFiles,
1094
+ };
1095
+ }
1096
+
1097
+ /*
1098
+ * =================================
1099
+ * Commit Route Build
1100
+ * =================================
1101
+ */
1102
+
1103
+ private commitSnapshot(
1104
+ state: RouteBuildState,
1105
+ snapshot: RouteBuildSnapshot
1106
+ ) {
1107
+ state.bundle =
1108
+ snapshot.bundle;
1109
+
1110
+ state.inputFiles =
1111
+ snapshot.inputFiles;
1112
+ }
1113
+
1114
+ /*
1115
+ * =================================
1116
+ * Module Graph
1117
+ * =================================
1118
+ */
1119
+
1120
+ private rebuildModuleGraph() {
1121
+ this.moduleToRoutes
1122
+ .clear();
1123
+
1124
+ for (
1125
+ const state
1126
+ of this.states.values()
1127
+ ) {
1128
+ for (
1129
+ const filePath
1130
+ of state.inputFiles
1131
+ ) {
1132
+ let routeSet =
1133
+ this.moduleToRoutes
1134
+ .get(
1135
+ filePath
1136
+ );
1137
+
1138
+ if (!routeSet) {
1139
+ routeSet =
1140
+ new Set<string>();
1141
+
1142
+ this.moduleToRoutes.set(
1143
+ filePath,
1144
+ routeSet
1145
+ );
1146
+ }
1147
+
1148
+ routeSet.add(
1149
+ state.route.pathname
1150
+ );
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ /*
1156
+ * =================================
1157
+ * Find Affected Routes
1158
+ * =================================
1159
+ */
1160
+
1161
+ findAffectedRoutes(
1162
+ changedFiles: string[]
1163
+ ): string[] {
1164
+ const affected =
1165
+ new Set<string>();
1166
+
1167
+ for (
1168
+ const filePath
1169
+ of changedFiles
1170
+ ) {
1171
+ const normalized =
1172
+ normalizeGraphPath(
1173
+ filePath
1174
+ );
1175
+
1176
+ const routes =
1177
+ this.moduleToRoutes
1178
+ .get(
1179
+ normalized
1180
+ );
1181
+
1182
+ if (!routes) {
1183
+ continue;
1184
+ }
1185
+
1186
+ for (
1187
+ const pathname
1188
+ of routes
1189
+ ) {
1190
+ affected.add(
1191
+ pathname
1192
+ );
1193
+ }
1194
+ }
1195
+
1196
+ return Array.from(
1197
+ affected
1198
+ ).sort();
1199
+ }
1200
+
1201
+ /*
1202
+ * =================================
1203
+ * Debug Graph
1204
+ * =================================
1205
+ */
1206
+
1207
+ printAffectedRoutes(
1208
+ filePath: string
1209
+ ) {
1210
+ const affected =
1211
+ this.findAffectedRoutes([
1212
+ filePath,
1213
+ ]);
1214
+
1215
+ console.log(
1216
+ `[module-graph] ${path.relative(
1217
+ this.rootDirectory,
1218
+ filePath
1219
+ )}`
1220
+ );
1221
+
1222
+ if (
1223
+ affected.length ===
1224
+ 0
1225
+ ) {
1226
+ console.log(
1227
+ " → no client routes"
1228
+ );
1229
+
1230
+ return;
1231
+ }
1232
+
1233
+ for (
1234
+ const pathname
1235
+ of affected
1236
+ ) {
1237
+ console.log(
1238
+ ` → ${pathname}`
1239
+ );
1240
+ }
1241
+ }
1242
+
1243
+ /*
1244
+ * =================================
1245
+ * Bundles
1246
+ * =================================
1247
+ */
1248
+
1249
+ getBundles():
1250
+ ClientBundle[] {
1251
+ return Array.from(
1252
+ this.states.values()
1253
+ ).map(
1254
+ (state) =>
1255
+ state.bundle
1256
+ );
1257
+ }
1258
+
1259
+ /*
1260
+ * =================================
1261
+ * Dispose
1262
+ * =================================
1263
+ */
1264
+
1265
+ async dispose() {
1266
+ const contexts =
1267
+ Array.from(
1268
+ this.states.values()
1269
+ ).map(
1270
+ (state) =>
1271
+ state.context
1272
+ );
1273
+
1274
+ this.states.clear();
1275
+
1276
+ this.moduleToRoutes
1277
+ .clear();
1278
+
1279
+ await Promise.all(
1280
+ contexts.map(
1281
+ (buildContext) =>
1282
+ buildContext.dispose()
1283
+ )
1284
+ );
1285
+ }
1286
+ }
1287
+
1288
+ /*
1289
+ * =====================================
1290
+ * Client Entry Generator
1291
+ * =====================================
1292
+ */
1293
+
1294
+ function createClientEntry(
1295
+ route: Route,
1296
+ rootDirectory: string
1297
+ ): string {
1298
+ const pageImport =
1299
+ toImportSpecifier(
1300
+ rootDirectory,
1301
+ route.filePath
1302
+ );
1303
+
1304
+ const layoutImports =
1305
+ route.layouts
1306
+ .map(
1307
+ (
1308
+ layoutFile,
1309
+ index
1310
+ ) => {
1311
+ const importPath =
1312
+ toImportSpecifier(
1313
+ rootDirectory,
1314
+ layoutFile
1315
+ );
1316
+
1317
+ return `
1318
+ import Layout${index} from ${JSON.stringify(importPath)};
1319
+ `.trim();
1320
+ }
1321
+ )
1322
+ .join("\n");
1323
+
1324
+ const layoutNames =
1325
+ route.layouts
1326
+ .map(
1327
+ (
1328
+ _,
1329
+ index
1330
+ ) =>
1331
+ `Layout${index}`
1332
+ )
1333
+ .join(", ");
1334
+
1335
+ return `
1336
+ import {
1337
+ createElement
1338
+ } from "react";
1339
+
1340
+ import {
1341
+ hydrateRoot
1342
+ } from "react-dom/client";
1343
+
1344
+ import Page from ${JSON.stringify(pageImport)};
1345
+
1346
+ ${layoutImports}
1347
+
1348
+ const rootElement =
1349
+ document.getElementById(
1350
+ "root"
1351
+ );
1352
+
1353
+ if (!rootElement) {
1354
+ throw new Error(
1355
+ "BCP Framework: root element was not found."
1356
+ );
1357
+ }
1358
+
1359
+ const dataElement =
1360
+ document.getElementById(
1361
+ "__BCP_DATA__"
1362
+ );
1363
+
1364
+ if (!dataElement) {
1365
+ throw new Error(
1366
+ "BCP Framework: framework data was not found."
1367
+ );
1368
+ }
1369
+
1370
+ const data =
1371
+ JSON.parse(
1372
+ dataElement.textContent ||
1373
+ "{}"
1374
+ );
1375
+
1376
+ const params =
1377
+ data.params || {};
1378
+
1379
+ const layouts = [
1380
+ ${layoutNames}
1381
+ ];
1382
+
1383
+ let tree =
1384
+ createElement(
1385
+ Page,
1386
+ {
1387
+ params
1388
+ }
1389
+ );
1390
+
1391
+ for (
1392
+ let index =
1393
+ layouts.length - 1;
1394
+
1395
+ index >= 0;
1396
+
1397
+ index--
1398
+ ) {
1399
+ const Layout =
1400
+ layouts[index];
1401
+
1402
+ tree =
1403
+ createElement(
1404
+ Layout,
1405
+ {
1406
+ params
1407
+ },
1408
+ tree
1409
+ );
1410
+ }
1411
+
1412
+ /*
1413
+ * Initial navigation:
1414
+ * hydrate the server-rendered HTML once.
1415
+ *
1416
+ * Client navigation / HMR:
1417
+ * render into the existing hydrated root.
1418
+ * React can then reconcile the tree and keep
1419
+ * state for layouts that remain at the same
1420
+ * position.
1421
+ */
1422
+ if (
1423
+ globalThis.__BCP_REACT_ROOT__
1424
+ ) {
1425
+ globalThis.__BCP_REACT_ROOT__
1426
+ .render(tree);
1427
+ } else {
1428
+ globalThis.__BCP_REACT_ROOT__ =
1429
+ hydrateRoot(
1430
+ rootElement,
1431
+ tree
1432
+ );
1433
+ }
1434
+ `.trim();
1435
+ }
1436
+
1437
+ /*
1438
+ * =====================================
1439
+ * Build Result → Input Files
1440
+ * =====================================
1441
+ */
1442
+
1443
+ function collectInputFiles(
1444
+ result: BuildResult,
1445
+ rootDirectory: string
1446
+ ): Set<string> {
1447
+ const files =
1448
+ new Set<string>();
1449
+
1450
+ const metafile =
1451
+ result.metafile;
1452
+
1453
+ if (!metafile) {
1454
+ return files;
1455
+ }
1456
+
1457
+ for (
1458
+ const input
1459
+ of Object.keys(
1460
+ metafile.inputs
1461
+ )
1462
+ ) {
1463
+ /*
1464
+ * Virtual entry
1465
+ *
1466
+ * เช่น:
1467
+ *
1468
+ * bcp-entry-index.js
1469
+ *
1470
+ * ไม่ใช่ application file
1471
+ */
1472
+ if (
1473
+ input.startsWith(
1474
+ "<"
1475
+ ) ||
1476
+ input.startsWith(
1477
+ "bcp-entry-"
1478
+ )
1479
+ ) {
1480
+ continue;
1481
+ }
1482
+
1483
+ const absolute =
1484
+ path.isAbsolute(
1485
+ input
1486
+ )
1487
+ ? input
1488
+ : path.resolve(
1489
+ rootDirectory,
1490
+ input
1491
+ );
1492
+
1493
+ files.add(
1494
+ normalizeGraphPath(
1495
+ absolute
1496
+ )
1497
+ );
1498
+ }
1499
+
1500
+ return files;
1501
+ }
1502
+
1503
+ /*
1504
+ * =====================================
1505
+ * Output Helper
1506
+ * =====================================
1507
+ */
1508
+
1509
+ function findJavaScriptOutput(
1510
+ result: BuildResult,
1511
+ outputFile: string
1512
+ ) {
1513
+ const outputs =
1514
+ result.outputFiles ??
1515
+ [];
1516
+
1517
+ const normalizedTarget =
1518
+ path.normalize(
1519
+ outputFile
1520
+ );
1521
+
1522
+ const exact =
1523
+ outputs.find(
1524
+ (file) =>
1525
+ path.normalize(
1526
+ file.path
1527
+ ) ===
1528
+ normalizedTarget
1529
+ );
1530
+
1531
+ if (exact) {
1532
+ return exact;
1533
+ }
1534
+
1535
+ return outputs.find(
1536
+ (file) =>
1537
+ file.path.endsWith(
1538
+ ".js"
1539
+ )
1540
+ );
1541
+ }
1542
+
1543
+ /*
1544
+ * =====================================
1545
+ * Graph Path
1546
+ * =====================================
1547
+ */
1548
+
1549
+ function normalizeGraphPath(
1550
+ filePath: string
1551
+ ): string {
1552
+ let normalized =
1553
+ path.normalize(
1554
+ path.resolve(
1555
+ filePath
1556
+ )
1557
+ );
1558
+
1559
+ /*
1560
+ * Windows file system
1561
+ * ปกติ case insensitive
1562
+ *
1563
+ * D:\\App\\Page.tsx
1564
+ * d:\\app\\page.tsx
1565
+ *
1566
+ * ให้ถือเป็นไฟล์เดียวกัน
1567
+ */
1568
+ if (
1569
+ process.platform ===
1570
+ "win32"
1571
+ ) {
1572
+ normalized =
1573
+ normalized.toLowerCase();
1574
+ }
1575
+
1576
+ return normalized;
1577
+ }
1578
+
1579
+ /*
1580
+ * =====================================
1581
+ * React Vendor Plugin
1582
+ * =====================================
1583
+ */
1584
+
1585
+ function createReactVendorPlugin():
1586
+ Plugin {
1587
+ const mappings =
1588
+ new Map<
1589
+ string,
1590
+ string
1591
+ >([
1592
+ [
1593
+ "react",
1594
+ `${VENDOR_PATH}/react.js`,
1595
+ ],
1596
+
1597
+ [
1598
+ "react/jsx-runtime",
1599
+ `${VENDOR_PATH}/react-jsx-runtime.js`,
1600
+ ],
1601
+
1602
+ [
1603
+ "react/jsx-dev-runtime",
1604
+ `${VENDOR_PATH}/react-jsx-dev-runtime.js`,
1605
+ ],
1606
+
1607
+ [
1608
+ "react-dom",
1609
+ `${VENDOR_PATH}/react-dom.js`,
1610
+ ],
1611
+
1612
+ [
1613
+ "react-dom/client",
1614
+ `${VENDOR_PATH}/react-dom-client.js`,
1615
+ ],
1616
+ ]);
1617
+
1618
+ return {
1619
+ name:
1620
+ "bcp-react-vendor",
1621
+
1622
+ setup(buildApi) {
1623
+ buildApi.onResolve(
1624
+ {
1625
+ filter:
1626
+ /^(react|react\/jsx-runtime|react\/jsx-dev-runtime|react-dom|react-dom\/client)$/,
1627
+ },
1628
+
1629
+ (args) => {
1630
+ const publicPath =
1631
+ mappings.get(
1632
+ args.path
1633
+ );
1634
+
1635
+ if (!publicPath) {
1636
+ return;
1637
+ }
1638
+
1639
+ return {
1640
+ path:
1641
+ publicPath,
1642
+
1643
+ external:
1644
+ true,
1645
+ };
1646
+ }
1647
+ );
1648
+ },
1649
+ };
1650
+ }
1651
+
1652
+ /*
1653
+ * =====================================
1654
+ * React Refresh Plugin
1655
+ * =====================================
1656
+ */
1657
+
1658
+ function createReactRefreshPlugin(
1659
+ rootDirectory: string
1660
+ ): Plugin {
1661
+ return {
1662
+ name:
1663
+ "bcp-react-refresh",
1664
+
1665
+ setup(buildApi) {
1666
+ buildApi.onLoad(
1667
+ {
1668
+ filter:
1669
+ /\.[jt]sx?$/,
1670
+ },
1671
+
1672
+ async (args) => {
1673
+ /*
1674
+ * node_modules
1675
+ */
1676
+ if (
1677
+ args.path.includes(
1678
+ `${path.sep}node_modules${path.sep}`
1679
+ )
1680
+ ) {
1681
+ return;
1682
+ }
1683
+
1684
+ /*
1685
+ * generated framework files
1686
+ */
1687
+ if (
1688
+ args.path.includes(
1689
+ `${path.sep}.bcp-framework${path.sep}`
1690
+ )
1691
+ ) {
1692
+ return;
1693
+ }
1694
+
1695
+ const relative =
1696
+ path.relative(
1697
+ rootDirectory,
1698
+ args.path
1699
+ );
1700
+
1701
+ /*
1702
+ * ไม่ transform file
1703
+ * นอก application
1704
+ */
1705
+ if (
1706
+ relative.startsWith(
1707
+ ".."
1708
+ )
1709
+ ) {
1710
+ return;
1711
+ }
1712
+
1713
+ const source =
1714
+ await fs.promises
1715
+ .readFile(
1716
+ args.path,
1717
+ "utf8"
1718
+ );
1719
+
1720
+ const extension =
1721
+ path.extname(
1722
+ args.path
1723
+ );
1724
+
1725
+ const isTypeScript =
1726
+ extension === ".ts" ||
1727
+ extension === ".tsx";
1728
+
1729
+ const presets:
1730
+ any[] = [];
1731
+
1732
+ /*
1733
+ * Babel 8
1734
+ *
1735
+ * ไม่ใช้:
1736
+ *
1737
+ * allowDeclareFields
1738
+ * allExtensions
1739
+ * isTSX
1740
+ */
1741
+ if (
1742
+ isTypeScript
1743
+ ) {
1744
+ presets.push([
1745
+ presetTypeScript,
1746
+ ]);
1747
+ }
1748
+
1749
+ presets.push([
1750
+ presetReact,
1751
+ {
1752
+ runtime:
1753
+ "automatic",
1754
+
1755
+ development:
1756
+ true,
1757
+ },
1758
+ ]);
1759
+
1760
+ const result =
1761
+ await transformAsync(
1762
+ source,
1763
+ {
1764
+ filename:
1765
+ args.path,
1766
+
1767
+ babelrc:
1768
+ false,
1769
+
1770
+ configFile:
1771
+ false,
1772
+
1773
+ sourceType:
1774
+ "module",
1775
+
1776
+ presets,
1777
+
1778
+ plugins: [
1779
+ reactRefreshBabel,
1780
+ ],
1781
+ }
1782
+ );
1783
+
1784
+ if (
1785
+ !result?.code
1786
+ ) {
1787
+ return;
1788
+ }
1789
+
1790
+ const moduleId =
1791
+ path.relative(
1792
+ rootDirectory,
1793
+ args.path
1794
+ )
1795
+ .replace(
1796
+ /\\/g,
1797
+ "/"
1798
+ );
1799
+
1800
+ const transformed =
1801
+ `
1802
+ const __bcpRefreshRuntime =
1803
+ globalThis.__BCP_REFRESH_RUNTIME__;
1804
+
1805
+ if (!__bcpRefreshRuntime) {
1806
+ throw new Error(
1807
+ "BCP Framework: React Refresh runtime is missing."
1808
+ );
1809
+ }
1810
+
1811
+ const $RefreshReg$ =
1812
+ (type, id) => {
1813
+ __bcpRefreshRuntime.register(
1814
+ type,
1815
+ ${JSON.stringify(moduleId)} + " " + id
1816
+ );
1817
+ };
1818
+
1819
+ const $RefreshSig$ =
1820
+ __bcpRefreshRuntime
1821
+ .createSignatureFunctionForTransform;
1822
+
1823
+ ${result.code}
1824
+ `.trim();
1825
+
1826
+ return {
1827
+ contents:
1828
+ transformed,
1829
+
1830
+ loader:
1831
+ "js",
1832
+
1833
+ resolveDir:
1834
+ path.dirname(
1835
+ args.path
1836
+ ),
1837
+ };
1838
+ }
1839
+ );
1840
+ },
1841
+ };
1842
+ }
1843
+
1844
+ /*
1845
+ * =====================================
1846
+ * File Utilities
1847
+ * =====================================
1848
+ */
1849
+
1850
+ function collectFiles(
1851
+ directory: string,
1852
+ callback:
1853
+ (
1854
+ filePath: string
1855
+ ) => void
1856
+ ) {
1857
+ const entries =
1858
+ fs.readdirSync(
1859
+ directory,
1860
+ {
1861
+ withFileTypes: true,
1862
+ }
1863
+ );
1864
+
1865
+ for (
1866
+ const entry
1867
+ of entries
1868
+ ) {
1869
+ const filePath =
1870
+ path.join(
1871
+ directory,
1872
+ entry.name
1873
+ );
1874
+
1875
+ if (
1876
+ entry.isDirectory()
1877
+ ) {
1878
+ collectFiles(
1879
+ filePath,
1880
+ callback
1881
+ );
1882
+
1883
+ continue;
1884
+ }
1885
+
1886
+ callback(
1887
+ filePath
1888
+ );
1889
+ }
1890
+ }
1891
+
1892
+ /*
1893
+ * =====================================
1894
+ * Import Specifier
1895
+ * =====================================
1896
+ */
1897
+
1898
+ function toImportSpecifier(
1899
+ rootDirectory: string,
1900
+ filePath: string
1901
+ ): string {
1902
+ let result =
1903
+ path.relative(
1904
+ rootDirectory,
1905
+ filePath
1906
+ );
1907
+
1908
+ result =
1909
+ result.replace(
1910
+ /\\/g,
1911
+ "/"
1912
+ );
1913
+
1914
+ if (
1915
+ !result.startsWith(
1916
+ "."
1917
+ )
1918
+ ) {
1919
+ result =
1920
+ `./${result}`;
1921
+ }
1922
+
1923
+ return result;
1924
+ }
1925
+
1926
+ /*
1927
+ * =====================================
1928
+ * Bundle Names
1929
+ * =====================================
1930
+ */
1931
+
1932
+ function routeToBundleName(
1933
+ pathname: string
1934
+ ): string {
1935
+ if (
1936
+ pathname === "/"
1937
+ ) {
1938
+ return "index.js";
1939
+ }
1940
+
1941
+ const name =
1942
+ pathname
1943
+ .slice(1)
1944
+ .split("/")
1945
+ .map(
1946
+ sanitizeSegment
1947
+ )
1948
+ .join("__");
1949
+
1950
+ return `${name}.js`;
1951
+ }
1952
+
1953
+ function sanitizeBundleId(
1954
+ pathname: string
1955
+ ): string {
1956
+ if (
1957
+ pathname === "/"
1958
+ ) {
1959
+ return "root";
1960
+ }
1961
+
1962
+ return pathname
1963
+ .slice(1)
1964
+ .split("/")
1965
+ .map(
1966
+ sanitizeSegment
1967
+ )
1968
+ .join("-");
1969
+ }
1970
+
1971
+ function sanitizeSegment(
1972
+ segment: string
1973
+ ): string {
1974
+ const dynamicMatch =
1975
+ /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
1976
+ segment
1977
+ );
1978
+
1979
+ if (
1980
+ dynamicMatch
1981
+ ) {
1982
+ return (
1983
+ `param-${dynamicMatch[1]}`
1984
+ );
1985
+ }
1986
+
1987
+ return segment.replace(
1988
+ /[^A-Za-z0-9._-]/g,
1989
+ "_"
1990
+ );
1991
+ }