@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,974 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ brotliCompressSync,
5
+ constants,
6
+ gzipSync,
7
+ } from "node:zlib";
8
+
9
+ import {
10
+ build,
11
+ type Metafile,
12
+ } from "esbuild";
13
+
14
+ import {
15
+ createClientEnvironmentDefines,
16
+ } from "../../env/src/index.js";
17
+
18
+ import type {
19
+ Route,
20
+ } from "../../router/src/index.js";
21
+
22
+ import {
23
+ findNearestSpecialFile,
24
+ } from "./special-files.js";
25
+
26
+ export interface ProductionAssetSize {
27
+ raw: number;
28
+ gzip: number;
29
+ brotli: number;
30
+ }
31
+
32
+ export interface ProductionAssetInfo {
33
+ path: string;
34
+ size: ProductionAssetSize;
35
+ }
36
+
37
+ export interface ProductionRouteAsset {
38
+ entry: string;
39
+ imports: string[];
40
+ size: ProductionAssetSize;
41
+ }
42
+
43
+ export interface ProductionBuildManifest {
44
+ version: number;
45
+ buildId: string;
46
+ builtAt: string;
47
+ routes: Record<
48
+ string,
49
+ ProductionRouteAsset
50
+ >;
51
+ assets: ProductionAssetInfo[];
52
+ }
53
+
54
+ interface OutputNode {
55
+ absolutePath: string;
56
+ publicPath: string;
57
+ metadata:
58
+ Metafile["outputs"][string];
59
+ }
60
+
61
+ const PRECOMPRESS_MIN_BYTES =
62
+ 1024;
63
+
64
+ export async function buildProductionClient(
65
+ routes: Route[],
66
+ rootDirectory: string
67
+ ): Promise<ProductionBuildManifest> {
68
+ const buildRoot =
69
+ path.join(
70
+ rootDirectory,
71
+ ".bcp-framework",
72
+ "build"
73
+ );
74
+
75
+ const clientDirectory =
76
+ path.join(
77
+ buildRoot,
78
+ "client"
79
+ );
80
+
81
+ const entryDirectory =
82
+ path.join(
83
+ buildRoot,
84
+ ".entries"
85
+ );
86
+
87
+ fs.rmSync(
88
+ buildRoot,
89
+ {
90
+ recursive: true,
91
+ force: true,
92
+ }
93
+ );
94
+
95
+ fs.mkdirSync(
96
+ entryDirectory,
97
+ {
98
+ recursive: true,
99
+ }
100
+ );
101
+
102
+ fs.mkdirSync(
103
+ clientDirectory,
104
+ {
105
+ recursive: true,
106
+ }
107
+ );
108
+
109
+ const entryPoints:
110
+ Record<string, string> = {};
111
+
112
+ const entryToRoute =
113
+ new Map<string, string>();
114
+
115
+ for (
116
+ const route
117
+ of routes
118
+ ) {
119
+ const entryName =
120
+ routeToEntryName(
121
+ route.pathname
122
+ );
123
+
124
+ const entryFile =
125
+ path.join(
126
+ entryDirectory,
127
+ `${entryName}.mjs`
128
+ );
129
+
130
+ fs.writeFileSync(
131
+ entryFile,
132
+ createProductionClientEntry(
133
+ route,
134
+ entryDirectory
135
+ ),
136
+ "utf8"
137
+ );
138
+
139
+ entryPoints[
140
+ entryName
141
+ ] =
142
+ entryFile;
143
+
144
+ entryToRoute.set(
145
+ normalizePath(
146
+ entryFile
147
+ ),
148
+ route.pathname
149
+ );
150
+ }
151
+
152
+ const result =
153
+ await build({
154
+ absWorkingDir:
155
+ rootDirectory,
156
+ entryPoints,
157
+ outdir:
158
+ clientDirectory,
159
+ bundle:
160
+ true,
161
+ splitting:
162
+ true,
163
+ format:
164
+ "esm",
165
+ platform:
166
+ "browser",
167
+ target: [
168
+ "es2022",
169
+ ],
170
+ minify:
171
+ process.env
172
+ .BCP_BUILD_MINIFY !==
173
+ "0",
174
+ treeShaking:
175
+ true,
176
+ sourcemap:
177
+ process.env
178
+ .BCP_BUILD_SOURCE_MAPS ===
179
+ "1",
180
+ metafile:
181
+ true,
182
+ jsx:
183
+ "automatic",
184
+ entryNames:
185
+ "routes/[name]-[hash]",
186
+ chunkNames:
187
+ "chunks/[name]-[hash]",
188
+ assetNames:
189
+ "assets/[name]-[hash]",
190
+ define:
191
+ createClientEnvironmentDefines(
192
+ "production"
193
+ ),
194
+ legalComments:
195
+ "none",
196
+ logLevel:
197
+ "silent",
198
+ });
199
+
200
+ if (!result.metafile) {
201
+ throw new Error(
202
+ "BCP Framework: esbuild did not return production metadata."
203
+ );
204
+ }
205
+
206
+ const outputNodes =
207
+ createOutputNodeMap(
208
+ result.metafile,
209
+ rootDirectory,
210
+ clientDirectory
211
+ );
212
+
213
+ const assetInfoByPath =
214
+ new Map<
215
+ string,
216
+ ProductionAssetInfo
217
+ >();
218
+
219
+ for (
220
+ const node
221
+ of outputNodes.values()
222
+ ) {
223
+ const assetInfo =
224
+ precompressOutput(
225
+ node.absolutePath,
226
+ node.publicPath
227
+ );
228
+
229
+ assetInfoByPath.set(
230
+ node.publicPath,
231
+ assetInfo
232
+ );
233
+ }
234
+
235
+ const routeManifest:
236
+ Record<
237
+ string,
238
+ ProductionRouteAsset
239
+ > = {};
240
+
241
+ for (
242
+ const node
243
+ of outputNodes.values()
244
+ ) {
245
+ if (
246
+ !node.metadata.entryPoint
247
+ ) {
248
+ continue;
249
+ }
250
+
251
+ const absoluteEntry =
252
+ path.isAbsolute(
253
+ node.metadata.entryPoint
254
+ )
255
+ ? node.metadata.entryPoint
256
+ : path.resolve(
257
+ rootDirectory,
258
+ node.metadata.entryPoint
259
+ );
260
+
261
+ const routePathname =
262
+ entryToRoute.get(
263
+ normalizePath(
264
+ absoluteEntry
265
+ )
266
+ );
267
+
268
+ if (!routePathname) {
269
+ continue;
270
+ }
271
+
272
+ const imports =
273
+ collectOutputImports(
274
+ node,
275
+ outputNodes,
276
+ rootDirectory
277
+ );
278
+
279
+ const routeAssetPaths = [
280
+ node.publicPath,
281
+ ...imports,
282
+ ];
283
+
284
+ routeManifest[
285
+ routePathname
286
+ ] = {
287
+ entry:
288
+ node.publicPath,
289
+ imports,
290
+ size:
291
+ sumAssetSizes(
292
+ routeAssetPaths,
293
+ assetInfoByPath
294
+ ),
295
+ };
296
+ }
297
+
298
+ for (
299
+ const route
300
+ of routes
301
+ ) {
302
+ if (
303
+ !routeManifest[
304
+ route.pathname
305
+ ]
306
+ ) {
307
+ throw new Error(
308
+ `BCP Framework: production client output was not found for route "${route.pathname}".`
309
+ );
310
+ }
311
+ }
312
+
313
+ const version =
314
+ Date.now();
315
+
316
+ const manifest:
317
+ ProductionBuildManifest = {
318
+ version,
319
+ buildId:
320
+ version.toString(36),
321
+ builtAt:
322
+ new Date(
323
+ version
324
+ ).toISOString(),
325
+ routes:
326
+ routeManifest,
327
+ assets:
328
+ Array.from(
329
+ assetInfoByPath.values()
330
+ ).sort(
331
+ (left, right) =>
332
+ left.path.localeCompare(
333
+ right.path
334
+ )
335
+ ),
336
+ };
337
+
338
+ fs.writeFileSync(
339
+ path.join(
340
+ buildRoot,
341
+ "manifest.json"
342
+ ),
343
+ JSON.stringify(
344
+ manifest,
345
+ null,
346
+ 2
347
+ ),
348
+ "utf8"
349
+ );
350
+
351
+ fs.rmSync(
352
+ entryDirectory,
353
+ {
354
+ recursive: true,
355
+ force: true,
356
+ }
357
+ );
358
+
359
+ return manifest;
360
+ }
361
+
362
+ function createOutputNodeMap(
363
+ metafile: Metafile,
364
+ rootDirectory: string,
365
+ clientDirectory: string
366
+ ): Map<string, OutputNode> {
367
+ const nodes =
368
+ new Map<string, OutputNode>();
369
+
370
+ for (
371
+ const [
372
+ outputName,
373
+ metadata,
374
+ ]
375
+ of Object.entries(
376
+ metafile.outputs
377
+ )
378
+ ) {
379
+ const absolutePath =
380
+ path.isAbsolute(
381
+ outputName
382
+ )
383
+ ? outputName
384
+ : path.resolve(
385
+ rootDirectory,
386
+ outputName
387
+ );
388
+
389
+ const relativePath =
390
+ path.relative(
391
+ clientDirectory,
392
+ absolutePath
393
+ );
394
+
395
+ if (
396
+ relativePath.startsWith(
397
+ ".."
398
+ ) ||
399
+ path.isAbsolute(
400
+ relativePath
401
+ )
402
+ ) {
403
+ continue;
404
+ }
405
+
406
+ const publicPath =
407
+ `/_bcp/client/${relativePath.replace(
408
+ /\\/g,
409
+ "/"
410
+ )}`;
411
+
412
+ nodes.set(
413
+ normalizePath(
414
+ absolutePath
415
+ ),
416
+ {
417
+ absolutePath,
418
+ publicPath,
419
+ metadata,
420
+ }
421
+ );
422
+ }
423
+
424
+ return nodes;
425
+ }
426
+
427
+ function collectOutputImports(
428
+ entryNode: OutputNode,
429
+ nodes: Map<string, OutputNode>,
430
+ rootDirectory: string
431
+ ): string[] {
432
+ const imports =
433
+ new Set<string>();
434
+
435
+ const visited =
436
+ new Set<string>();
437
+
438
+ function visit(
439
+ node: OutputNode
440
+ ): void {
441
+ const key =
442
+ normalizePath(
443
+ node.absolutePath
444
+ );
445
+
446
+ if (
447
+ visited.has(
448
+ key
449
+ )
450
+ ) {
451
+ return;
452
+ }
453
+
454
+ visited.add(
455
+ key
456
+ );
457
+
458
+ for (
459
+ const imported
460
+ of node.metadata.imports
461
+ ) {
462
+ if (
463
+ imported.external
464
+ ) {
465
+ continue;
466
+ }
467
+
468
+ const target =
469
+ resolveImportedOutput(
470
+ node,
471
+ imported.path,
472
+ nodes,
473
+ rootDirectory
474
+ );
475
+
476
+ if (!target) {
477
+ continue;
478
+ }
479
+
480
+ if (
481
+ target.publicPath !==
482
+ entryNode.publicPath &&
483
+ isModulePreloadAsset(
484
+ target.publicPath
485
+ )
486
+ ) {
487
+ imports.add(
488
+ target.publicPath
489
+ );
490
+ }
491
+
492
+ visit(
493
+ target
494
+ );
495
+ }
496
+ }
497
+
498
+ visit(
499
+ entryNode
500
+ );
501
+
502
+ return Array.from(
503
+ imports
504
+ ).sort();
505
+ }
506
+
507
+ function resolveImportedOutput(
508
+ source: OutputNode,
509
+ importPath: string,
510
+ nodes: Map<string, OutputNode>,
511
+ rootDirectory: string
512
+ ): OutputNode | null {
513
+ const candidates =
514
+ path.isAbsolute(
515
+ importPath
516
+ )
517
+ ? [
518
+ importPath,
519
+ ]
520
+ : [
521
+ path.resolve(
522
+ rootDirectory,
523
+ importPath
524
+ ),
525
+ path.resolve(
526
+ path.dirname(
527
+ source.absolutePath
528
+ ),
529
+ importPath
530
+ ),
531
+ ];
532
+
533
+ for (
534
+ const candidate
535
+ of candidates
536
+ ) {
537
+ const node =
538
+ nodes.get(
539
+ normalizePath(
540
+ candidate
541
+ )
542
+ );
543
+
544
+ if (node) {
545
+ return node;
546
+ }
547
+ }
548
+
549
+ return null;
550
+ }
551
+
552
+ function precompressOutput(
553
+ absolutePath: string,
554
+ publicPath: string
555
+ ): ProductionAssetInfo {
556
+ const content =
557
+ fs.readFileSync(
558
+ absolutePath
559
+ );
560
+
561
+ const raw =
562
+ content.length;
563
+
564
+ let gzip = raw;
565
+ let brotli = raw;
566
+
567
+ if (
568
+ process.env
569
+ .BCP_COMPRESSION !==
570
+ "0" &&
571
+ raw >=
572
+ PRECOMPRESS_MIN_BYTES &&
573
+ isPrecompressibleAsset(
574
+ absolutePath
575
+ )
576
+ ) {
577
+ const gzipContent =
578
+ gzipSync(
579
+ content,
580
+ {
581
+ level: 9,
582
+ }
583
+ );
584
+
585
+ if (
586
+ gzipContent.length < raw
587
+ ) {
588
+ fs.writeFileSync(
589
+ `${absolutePath}.gz`,
590
+ gzipContent
591
+ );
592
+
593
+ gzip =
594
+ gzipContent.length;
595
+ }
596
+
597
+ const brotliContent =
598
+ brotliCompressSync(
599
+ content,
600
+ {
601
+ params: {
602
+ [constants.BROTLI_PARAM_QUALITY]:
603
+ 8,
604
+ },
605
+ }
606
+ );
607
+
608
+ if (
609
+ brotliContent.length < raw
610
+ ) {
611
+ fs.writeFileSync(
612
+ `${absolutePath}.br`,
613
+ brotliContent
614
+ );
615
+
616
+ brotli =
617
+ brotliContent.length;
618
+ }
619
+ }
620
+
621
+ return {
622
+ path:
623
+ publicPath,
624
+ size: {
625
+ raw,
626
+ gzip,
627
+ brotli,
628
+ },
629
+ };
630
+ }
631
+
632
+ function sumAssetSizes(
633
+ assetPaths: string[],
634
+ assets: Map<
635
+ string,
636
+ ProductionAssetInfo
637
+ >
638
+ ): ProductionAssetSize {
639
+ const total:
640
+ ProductionAssetSize = {
641
+ raw: 0,
642
+ gzip: 0,
643
+ brotli: 0,
644
+ };
645
+
646
+ for (
647
+ const assetPath
648
+ of assetPaths
649
+ ) {
650
+ const asset =
651
+ assets.get(
652
+ assetPath
653
+ );
654
+
655
+ if (!asset) {
656
+ continue;
657
+ }
658
+
659
+ total.raw +=
660
+ asset.size.raw;
661
+ total.gzip +=
662
+ asset.size.gzip;
663
+ total.brotli +=
664
+ asset.size.brotli;
665
+ }
666
+
667
+ return total;
668
+ }
669
+
670
+ function isModulePreloadAsset(
671
+ publicPath: string
672
+ ): boolean {
673
+ return /\.(?:m?js)$/i.test(
674
+ publicPath
675
+ );
676
+ }
677
+
678
+ function isPrecompressibleAsset(
679
+ filePath: string
680
+ ): boolean {
681
+ return /\.(?:m?js|css|json|html?|svg|txt|xml)$/i.test(
682
+ filePath
683
+ );
684
+ }
685
+
686
+ function createProductionClientEntry(
687
+ route: Route,
688
+ entryDirectory: string
689
+ ): string {
690
+ const pageImport =
691
+ toImportSpecifier(
692
+ entryDirectory,
693
+ route.filePath
694
+ );
695
+
696
+ const appDirectory =
697
+ resolveRouteAppDirectory(
698
+ route
699
+ );
700
+
701
+ const errorFile =
702
+ findNearestSpecialFile(
703
+ appDirectory,
704
+ route.filePath,
705
+ "error"
706
+ );
707
+
708
+ const notFoundFile =
709
+ findNearestSpecialFile(
710
+ appDirectory,
711
+ route.filePath,
712
+ "not-found"
713
+ );
714
+
715
+ const errorImport =
716
+ errorFile
717
+ ? `import ErrorComponent from ${JSON.stringify(
718
+ toImportSpecifier(
719
+ entryDirectory,
720
+ errorFile
721
+ )
722
+ )};`
723
+ : "const ErrorComponent = null;";
724
+
725
+ const notFoundImport =
726
+ notFoundFile
727
+ ? `import NotFoundComponent from ${JSON.stringify(
728
+ toImportSpecifier(
729
+ entryDirectory,
730
+ notFoundFile
731
+ )
732
+ )};`
733
+ : "const NotFoundComponent = null;";
734
+
735
+ const layoutImports =
736
+ route.layouts
737
+ .map(
738
+ (
739
+ layoutFile,
740
+ index
741
+ ) =>
742
+ `import Layout${index} from ${JSON.stringify(
743
+ toImportSpecifier(
744
+ entryDirectory,
745
+ layoutFile
746
+ )
747
+ )};`
748
+ )
749
+ .join("\n");
750
+
751
+ const layoutNames =
752
+ route.layouts
753
+ .map(
754
+ (
755
+ _,
756
+ index
757
+ ) =>
758
+ `Layout${index}`
759
+ )
760
+ .join(", ");
761
+
762
+ return `
763
+ import {
764
+ BcpRouteErrorBoundary
765
+ } from "bcp";
766
+
767
+ import {
768
+ createElement
769
+ } from "react";
770
+
771
+ import {
772
+ hydrateRoot
773
+ } from "react-dom/client";
774
+
775
+ import Page from ${JSON.stringify(pageImport)};
776
+
777
+ ${errorImport}
778
+ ${notFoundImport}
779
+ ${layoutImports}
780
+
781
+ const rootElement =
782
+ document.getElementById(
783
+ "root"
784
+ );
785
+
786
+ if (!rootElement) {
787
+ throw new Error(
788
+ "BCP Framework: root element was not found."
789
+ );
790
+ }
791
+
792
+ const dataElement =
793
+ document.getElementById(
794
+ "__BCP_DATA__"
795
+ );
796
+
797
+ if (!dataElement) {
798
+ throw new Error(
799
+ "BCP Framework: framework data was not found."
800
+ );
801
+ }
802
+
803
+ const data =
804
+ JSON.parse(
805
+ dataElement.textContent ||
806
+ "{}"
807
+ );
808
+
809
+ const params =
810
+ data.params || {};
811
+
812
+ const layouts = [
813
+ ${layoutNames}
814
+ ];
815
+
816
+ let tree =
817
+ createElement(
818
+ Page,
819
+ {
820
+ params
821
+ }
822
+ );
823
+
824
+ tree =
825
+ createElement(
826
+ BcpRouteErrorBoundary,
827
+ {
828
+ ErrorComponent,
829
+ NotFoundComponent
830
+ },
831
+ tree
832
+ );
833
+
834
+ for (
835
+ let index =
836
+ layouts.length - 1;
837
+
838
+ index >= 0;
839
+
840
+ index--
841
+ ) {
842
+ const Layout =
843
+ layouts[index];
844
+
845
+ tree =
846
+ createElement(
847
+ Layout,
848
+ {
849
+ params
850
+ },
851
+ tree
852
+ );
853
+ }
854
+
855
+ if (
856
+ globalThis.__BCP_REACT_ROOT__
857
+ ) {
858
+ globalThis.__BCP_REACT_ROOT__
859
+ .render(tree);
860
+ } else {
861
+ globalThis.__BCP_REACT_ROOT__ =
862
+ hydrateRoot(
863
+ rootElement,
864
+ tree
865
+ );
866
+ }
867
+ `.trim();
868
+ }
869
+
870
+ function resolveRouteAppDirectory(
871
+ route: Route
872
+ ): string {
873
+ let directory =
874
+ path.dirname(
875
+ route.filePath
876
+ );
877
+
878
+ for (
879
+ let index = 0;
880
+ index <
881
+ route.segments.length;
882
+ index++
883
+ ) {
884
+ directory =
885
+ path.dirname(
886
+ directory
887
+ );
888
+ }
889
+
890
+ return directory;
891
+ }
892
+
893
+ function toImportSpecifier(
894
+ fromDirectory: string,
895
+ filePath: string
896
+ ): string {
897
+ let relative =
898
+ path.relative(
899
+ fromDirectory,
900
+ filePath
901
+ )
902
+ .replace(
903
+ /\\/g,
904
+ "/"
905
+ );
906
+
907
+ if (
908
+ !relative.startsWith(".")
909
+ ) {
910
+ relative =
911
+ `./${relative}`;
912
+ }
913
+
914
+ return relative;
915
+ }
916
+
917
+ function routeToEntryName(
918
+ pathname: string
919
+ ): string {
920
+ if (
921
+ pathname === "/"
922
+ ) {
923
+ return "index";
924
+ }
925
+
926
+ return pathname
927
+ .slice(1)
928
+ .split("/")
929
+ .map(
930
+ sanitizeSegment
931
+ )
932
+ .join("__");
933
+ }
934
+
935
+ function sanitizeSegment(
936
+ segment: string
937
+ ): string {
938
+ const dynamicMatch =
939
+ /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
940
+ segment
941
+ );
942
+
943
+ if (
944
+ dynamicMatch
945
+ ) {
946
+ return `param-${dynamicMatch[1]}`;
947
+ }
948
+
949
+ return segment.replace(
950
+ /[^A-Za-z0-9._-]/g,
951
+ "_"
952
+ );
953
+ }
954
+
955
+ function normalizePath(
956
+ filePath: string
957
+ ): string {
958
+ let normalized =
959
+ path.normalize(
960
+ path.resolve(
961
+ filePath
962
+ )
963
+ );
964
+
965
+ if (
966
+ process.platform ===
967
+ "win32"
968
+ ) {
969
+ normalized =
970
+ normalized.toLowerCase();
971
+ }
972
+
973
+ return normalized;
974
+ }