@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,1032 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export type RouteSegment =
5
+ | {
6
+ type: "static";
7
+ value: string;
8
+ }
9
+ | {
10
+ type: "dynamic";
11
+ value: string;
12
+ }
13
+ | {
14
+ type: "catchAll";
15
+ value: string;
16
+ }
17
+ | {
18
+ type: "optionalCatchAll";
19
+ value: string;
20
+ }
21
+ | {
22
+ type: "group";
23
+ value: string;
24
+ };
25
+
26
+ export type RouteParamValue =
27
+ | string
28
+ | string[]
29
+ | undefined;
30
+
31
+ export type RouteParams = Record<
32
+ string,
33
+ RouteParamValue
34
+ >;
35
+
36
+ interface RoutePattern {
37
+ pathname: string;
38
+ filePath: string;
39
+ segments: RouteSegment[];
40
+ appDirectory?: string;
41
+ }
42
+
43
+ export interface Route
44
+ extends RoutePattern {
45
+ layouts: string[];
46
+ }
47
+
48
+ export interface ApiRoute
49
+ extends RoutePattern { }
50
+
51
+ export interface RouteMatch<
52
+ T extends RoutePattern
53
+ > {
54
+ route: T;
55
+
56
+ /*
57
+ * Kept structurally permissive for backward compatibility with
58
+ * pre-Step-27 server internals that still annotate params as
59
+ * Record<string, string>. Runtime values follow RouteParams:
60
+ * dynamic => string, catch-all => string[], optional catch-all =>
61
+ * string[] | undefined.
62
+ */
63
+ params: Record<string, any>;
64
+ }
65
+
66
+ export function scanRoutes(
67
+ appDirectory: string
68
+ ): Route[] {
69
+ validateAppDirectory(
70
+ appDirectory
71
+ );
72
+
73
+ const routes: Route[] = [];
74
+
75
+ walkDirectory(
76
+ appDirectory,
77
+ (fullPath, fileName) => {
78
+ if (!isPageFile(fileName)) {
79
+ return;
80
+ }
81
+
82
+ const relativePath =
83
+ path.relative(
84
+ appDirectory,
85
+ fullPath
86
+ );
87
+
88
+ const sourceSegments =
89
+ getRouteSourceSegments(
90
+ relativePath,
91
+ "page"
92
+ );
93
+
94
+ routes.push({
95
+ pathname:
96
+ sourceSegmentsToPathname(
97
+ sourceSegments
98
+ ),
99
+ filePath:
100
+ fullPath,
101
+ layouts:
102
+ collectLayouts(
103
+ appDirectory,
104
+ fullPath
105
+ ),
106
+ segments:
107
+ parseRouteSegments(
108
+ sourceSegments,
109
+ relativePath
110
+ ),
111
+ appDirectory:
112
+ path.resolve(
113
+ appDirectory
114
+ ),
115
+ });
116
+ }
117
+ );
118
+
119
+ validateRouteCollection(
120
+ routes,
121
+ "page"
122
+ );
123
+
124
+ return routes.sort(
125
+ compareRoutes
126
+ );
127
+ }
128
+
129
+ export function scanApiRoutes(
130
+ appDirectory: string
131
+ ): ApiRoute[] {
132
+ validateAppDirectory(
133
+ appDirectory
134
+ );
135
+
136
+ const routes: ApiRoute[] = [];
137
+
138
+ walkDirectory(
139
+ appDirectory,
140
+ (fullPath, fileName) => {
141
+ if (!isApiRouteFile(fileName)) {
142
+ return;
143
+ }
144
+
145
+ const relativePath =
146
+ path.relative(
147
+ appDirectory,
148
+ fullPath
149
+ );
150
+
151
+ const sourceSegments =
152
+ getRouteSourceSegments(
153
+ relativePath,
154
+ "route"
155
+ );
156
+
157
+ routes.push({
158
+ pathname:
159
+ sourceSegmentsToPathname(
160
+ sourceSegments
161
+ ),
162
+ filePath:
163
+ fullPath,
164
+ segments:
165
+ parseRouteSegments(
166
+ sourceSegments,
167
+ relativePath
168
+ ),
169
+ appDirectory:
170
+ path.resolve(
171
+ appDirectory
172
+ ),
173
+ });
174
+ }
175
+ );
176
+
177
+ validateRouteCollection(
178
+ routes,
179
+ "API"
180
+ );
181
+
182
+ return routes.sort(
183
+ compareRoutes
184
+ );
185
+ }
186
+
187
+ export function assertNoPageApiConflicts(
188
+ pageRoutes: Route[],
189
+ apiRoutes: ApiRoute[]
190
+ ): void {
191
+ const pages =
192
+ new Map<string, Route>();
193
+
194
+ for (const route of pageRoutes) {
195
+ pages.set(
196
+ createRouteSignature(
197
+ route
198
+ ),
199
+ route
200
+ );
201
+ }
202
+
203
+ for (const apiRoute of apiRoutes) {
204
+ const page =
205
+ pages.get(
206
+ createRouteSignature(
207
+ apiRoute
208
+ )
209
+ );
210
+
211
+ if (!page) {
212
+ continue;
213
+ }
214
+
215
+ throw new Error(
216
+ "Page route and API route conflict:\n" +
217
+ ` Page: ${page.pathname}\n` +
218
+ ` API: ${apiRoute.pathname}\n\n` +
219
+ "page.tsx and route.ts cannot own the same route pattern."
220
+ );
221
+ }
222
+ }
223
+
224
+ export function matchRoute(
225
+ routes: Route[],
226
+ pathname: string
227
+ ): RouteMatch<Route> | null {
228
+ return matchRoutePattern(
229
+ routes,
230
+ pathname
231
+ );
232
+ }
233
+
234
+ export function matchApiRoute(
235
+ routes: ApiRoute[],
236
+ pathname: string
237
+ ): RouteMatch<ApiRoute> | null {
238
+ return matchRoutePattern(
239
+ routes,
240
+ pathname
241
+ );
242
+ }
243
+
244
+ function matchRoutePattern<
245
+ T extends RoutePattern
246
+ >(
247
+ routes: T[],
248
+ pathname: string
249
+ ): RouteMatch<T> | null {
250
+ const requestSegments =
251
+ splitRequestPath(
252
+ pathname
253
+ );
254
+
255
+ for (const route of routes) {
256
+ const urlSegments =
257
+ getUrlSegments(
258
+ route.segments
259
+ );
260
+
261
+ const params:
262
+ Record<string, any> = {};
263
+
264
+ let requestIndex = 0;
265
+ let matched = true;
266
+
267
+ for (
268
+ let routeIndex = 0;
269
+ routeIndex < urlSegments.length;
270
+ routeIndex++
271
+ ) {
272
+ const segment =
273
+ urlSegments[
274
+ routeIndex
275
+ ];
276
+
277
+ if (
278
+ segment.type ===
279
+ "catchAll" ||
280
+ segment.type ===
281
+ "optionalCatchAll"
282
+ ) {
283
+ const remaining =
284
+ requestSegments.slice(
285
+ requestIndex
286
+ );
287
+
288
+ if (
289
+ segment.type ===
290
+ "catchAll" &&
291
+ remaining.length === 0
292
+ ) {
293
+ matched = false;
294
+ break;
295
+ }
296
+
297
+ params[
298
+ segment.value
299
+ ] =
300
+ remaining.length > 0
301
+ ? remaining
302
+ : undefined;
303
+
304
+ requestIndex =
305
+ requestSegments.length;
306
+ continue;
307
+ }
308
+
309
+ const requestSegment =
310
+ requestSegments[
311
+ requestIndex
312
+ ];
313
+
314
+ if (
315
+ requestSegment ===
316
+ undefined
317
+ ) {
318
+ matched = false;
319
+ break;
320
+ }
321
+
322
+ if (
323
+ segment.type ===
324
+ "static"
325
+ ) {
326
+ if (
327
+ segment.value !==
328
+ requestSegment
329
+ ) {
330
+ matched = false;
331
+ break;
332
+ }
333
+ } else {
334
+ params[
335
+ segment.value
336
+ ] =
337
+ requestSegment;
338
+ }
339
+
340
+ requestIndex++;
341
+ }
342
+
343
+ if (
344
+ matched &&
345
+ requestIndex ===
346
+ requestSegments.length
347
+ ) {
348
+ return {
349
+ route,
350
+ params,
351
+ };
352
+ }
353
+ }
354
+
355
+ return null;
356
+ }
357
+
358
+ function validateRouteCollection<
359
+ T extends RoutePattern
360
+ >(
361
+ routes: T[],
362
+ type: string
363
+ ): void {
364
+ assertNoDuplicateRoutes(
365
+ routes,
366
+ type
367
+ );
368
+ assertNoAmbiguousRoutes(
369
+ routes,
370
+ type
371
+ );
372
+ }
373
+
374
+ function walkDirectory(
375
+ directory: string,
376
+ callback: (
377
+ fullPath: string,
378
+ fileName: string
379
+ ) => void
380
+ ): void {
381
+ const entries =
382
+ fs.readdirSync(
383
+ directory,
384
+ {
385
+ withFileTypes: true,
386
+ }
387
+ );
388
+
389
+ for (const entry of entries) {
390
+ const fullPath =
391
+ path.join(
392
+ directory,
393
+ entry.name
394
+ );
395
+
396
+ if (entry.isDirectory()) {
397
+ walkDirectory(
398
+ fullPath,
399
+ callback
400
+ );
401
+ continue;
402
+ }
403
+
404
+ callback(
405
+ fullPath,
406
+ entry.name
407
+ );
408
+ }
409
+ }
410
+
411
+ function validateAppDirectory(
412
+ appDirectory: string
413
+ ): void {
414
+ if (
415
+ fs.existsSync(
416
+ appDirectory
417
+ )
418
+ ) {
419
+ return;
420
+ }
421
+
422
+ throw new Error(
423
+ `App directory was not found: ${appDirectory}`
424
+ );
425
+ }
426
+
427
+ function getRouteSourceSegments(
428
+ filePath: string,
429
+ kind: "page" | "route"
430
+ ): string[] {
431
+ const normalized =
432
+ filePath.replace(
433
+ /\\/g,
434
+ "/"
435
+ );
436
+
437
+ const filePattern =
438
+ kind === "page"
439
+ ? /(?:^|\/)page\.tsx?$/
440
+ : /(?:^|\/)route\.ts$/;
441
+
442
+ const withoutFile =
443
+ normalized.replace(
444
+ filePattern,
445
+ ""
446
+ );
447
+
448
+ if (!withoutFile) {
449
+ return [];
450
+ }
451
+
452
+ return withoutFile
453
+ .split("/")
454
+ .filter(Boolean);
455
+ }
456
+
457
+ function sourceSegmentsToPathname(
458
+ segments: string[]
459
+ ): string {
460
+ const urlSegments =
461
+ segments.filter(
462
+ (segment) =>
463
+ !isRouteGroup(
464
+ segment
465
+ )
466
+ );
467
+
468
+ return urlSegments.length > 0
469
+ ? `/${urlSegments.join("/")}`
470
+ : "/";
471
+ }
472
+
473
+ function parseRouteSegments(
474
+ sourceSegments: string[],
475
+ sourceLabel: string
476
+ ): RouteSegment[] {
477
+ const dynamicNames =
478
+ new Set<string>();
479
+
480
+ const result:
481
+ RouteSegment[] = [];
482
+
483
+ let foundCatchAll = false;
484
+
485
+ for (
486
+ let index = 0;
487
+ index < sourceSegments.length;
488
+ index++
489
+ ) {
490
+ const source =
491
+ sourceSegments[index];
492
+
493
+ if (isRouteGroup(source)) {
494
+ validateRouteGroup(
495
+ source,
496
+ sourceLabel
497
+ );
498
+
499
+ result.push({
500
+ type: "group",
501
+ value:
502
+ source.slice(
503
+ 1,
504
+ -1
505
+ ),
506
+ });
507
+ continue;
508
+ }
509
+
510
+ if (foundCatchAll) {
511
+ throw new Error(
512
+ `Catch-all segment must be the last URL segment in "${sourceLabel}".`
513
+ );
514
+ }
515
+
516
+ const optionalCatchAll =
517
+ /^\[\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]\]$/.exec(
518
+ source
519
+ );
520
+
521
+ if (optionalCatchAll) {
522
+ const name =
523
+ optionalCatchAll[1];
524
+
525
+ assertUniqueParam(
526
+ dynamicNames,
527
+ name,
528
+ sourceLabel
529
+ );
530
+
531
+ result.push({
532
+ type:
533
+ "optionalCatchAll",
534
+ value:
535
+ name,
536
+ });
537
+ foundCatchAll = true;
538
+ continue;
539
+ }
540
+
541
+ const catchAll =
542
+ /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
543
+ source
544
+ );
545
+
546
+ if (catchAll) {
547
+ const name =
548
+ catchAll[1];
549
+
550
+ assertUniqueParam(
551
+ dynamicNames,
552
+ name,
553
+ sourceLabel
554
+ );
555
+
556
+ result.push({
557
+ type:
558
+ "catchAll",
559
+ value:
560
+ name,
561
+ });
562
+ foundCatchAll = true;
563
+ continue;
564
+ }
565
+
566
+ const dynamic =
567
+ /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
568
+ source
569
+ );
570
+
571
+ if (dynamic) {
572
+ const name =
573
+ dynamic[1];
574
+
575
+ assertUniqueParam(
576
+ dynamicNames,
577
+ name,
578
+ sourceLabel
579
+ );
580
+
581
+ result.push({
582
+ type:
583
+ "dynamic",
584
+ value:
585
+ name,
586
+ });
587
+ continue;
588
+ }
589
+
590
+ if (
591
+ source.includes("[") ||
592
+ source.includes("]")
593
+ ) {
594
+ throw new Error(
595
+ `Invalid dynamic segment "${source}" in "${sourceLabel}".`
596
+ );
597
+ }
598
+
599
+ if (
600
+ source.startsWith("(") ||
601
+ source.endsWith(")")
602
+ ) {
603
+ throw new Error(
604
+ `Invalid route group segment "${source}" in "${sourceLabel}".`
605
+ );
606
+ }
607
+
608
+ result.push({
609
+ type: "static",
610
+ value: source,
611
+ });
612
+ }
613
+
614
+ return result;
615
+ }
616
+
617
+ function assertUniqueParam(
618
+ dynamicNames: Set<string>,
619
+ name: string,
620
+ sourceLabel: string
621
+ ): void {
622
+ if (dynamicNames.has(name)) {
623
+ throw new Error(
624
+ `Duplicate dynamic parameter "${name}" in route "${sourceLabel}".`
625
+ );
626
+ }
627
+
628
+ dynamicNames.add(name);
629
+ }
630
+
631
+ function isRouteGroup(
632
+ segment: string
633
+ ): boolean {
634
+ return /^\([^()]+\)$/.test(
635
+ segment
636
+ );
637
+ }
638
+
639
+ function validateRouteGroup(
640
+ segment: string,
641
+ sourceLabel: string
642
+ ): void {
643
+ const name =
644
+ segment.slice(
645
+ 1,
646
+ -1
647
+ ).trim();
648
+
649
+ if (!name) {
650
+ throw new Error(
651
+ `Route group cannot be empty in "${sourceLabel}".`
652
+ );
653
+ }
654
+
655
+ if (
656
+ name.includes("/") ||
657
+ name.includes("\\")
658
+ ) {
659
+ throw new Error(
660
+ `Invalid route group "${segment}" in "${sourceLabel}".`
661
+ );
662
+ }
663
+ }
664
+
665
+ function getUrlSegments(
666
+ segments: RouteSegment[]
667
+ ): Exclude<
668
+ RouteSegment,
669
+ { type: "group" }
670
+ >[] {
671
+ return segments.filter(
672
+ (
673
+ segment
674
+ ): segment is Exclude<
675
+ RouteSegment,
676
+ { type: "group" }
677
+ > =>
678
+ segment.type !==
679
+ "group"
680
+ );
681
+ }
682
+
683
+ function splitRequestPath(
684
+ pathname: string
685
+ ): string[] {
686
+ const cleanPathname =
687
+ pathname.split("?")[0]
688
+ .split("#")[0] ||
689
+ "/";
690
+
691
+ if (cleanPathname === "/") {
692
+ return [];
693
+ }
694
+
695
+ return cleanPathname
696
+ .replace(/^\/+|\/+$/g, "")
697
+ .split("/")
698
+ .filter(Boolean)
699
+ .map(
700
+ safeDecodeURIComponent
701
+ );
702
+ }
703
+
704
+ function safeDecodeURIComponent(
705
+ value: string
706
+ ): string {
707
+ try {
708
+ return decodeURIComponent(
709
+ value
710
+ );
711
+ } catch {
712
+ return value;
713
+ }
714
+ }
715
+
716
+ function compareRoutes<
717
+ T extends RoutePattern
718
+ >(
719
+ left: T,
720
+ right: T
721
+ ): number {
722
+ const a =
723
+ getUrlSegments(
724
+ left.segments
725
+ );
726
+ const b =
727
+ getUrlSegments(
728
+ right.segments
729
+ );
730
+
731
+ const maxLength =
732
+ Math.max(
733
+ a.length,
734
+ b.length
735
+ );
736
+
737
+ for (
738
+ let index = 0;
739
+ index < maxLength;
740
+ index++
741
+ ) {
742
+ const aSegment =
743
+ a[index];
744
+ const bSegment =
745
+ b[index];
746
+
747
+ if (!aSegment || !bSegment) {
748
+ if (
749
+ !aSegment &&
750
+ bSegment?.type ===
751
+ "optionalCatchAll"
752
+ ) {
753
+ return -1;
754
+ }
755
+
756
+ if (
757
+ !bSegment &&
758
+ aSegment?.type ===
759
+ "optionalCatchAll"
760
+ ) {
761
+ return 1;
762
+ }
763
+
764
+ break;
765
+ }
766
+
767
+ const rankDifference =
768
+ segmentRank(
769
+ bSegment
770
+ ) -
771
+ segmentRank(
772
+ aSegment
773
+ );
774
+
775
+ if (rankDifference !== 0) {
776
+ return rankDifference;
777
+ }
778
+
779
+ if (
780
+ aSegment.type ===
781
+ "static" &&
782
+ bSegment.type ===
783
+ "static" &&
784
+ aSegment.value !==
785
+ bSegment.value
786
+ ) {
787
+ return aSegment.value
788
+ .localeCompare(
789
+ bSegment.value
790
+ );
791
+ }
792
+ }
793
+
794
+ if (a.length !== b.length) {
795
+ return b.length - a.length;
796
+ }
797
+
798
+ return left.pathname
799
+ .localeCompare(
800
+ right.pathname
801
+ );
802
+ }
803
+
804
+ function segmentRank(
805
+ segment: Exclude<
806
+ RouteSegment,
807
+ { type: "group" }
808
+ >
809
+ ): number {
810
+ switch (segment.type) {
811
+ case "static":
812
+ return 4;
813
+ case "dynamic":
814
+ return 3;
815
+ case "catchAll":
816
+ return 2;
817
+ case "optionalCatchAll":
818
+ return 1;
819
+ }
820
+ }
821
+
822
+ function assertNoDuplicateRoutes<
823
+ T extends RoutePattern
824
+ >(
825
+ routes: T[],
826
+ type: string
827
+ ): void {
828
+ const paths =
829
+ new Map<string, T>();
830
+
831
+ for (const route of routes) {
832
+ const existing =
833
+ paths.get(
834
+ route.pathname
835
+ );
836
+
837
+ if (existing) {
838
+ throw new Error(
839
+ `Duplicate ${type} route "${route.pathname}":\n` +
840
+ ` ${existing.filePath}\n` +
841
+ ` ${route.filePath}\n\n` +
842
+ "Route groups do not appear in URLs, so grouped routes must still have unique public pathnames."
843
+ );
844
+ }
845
+
846
+ paths.set(
847
+ route.pathname,
848
+ route
849
+ );
850
+ }
851
+ }
852
+
853
+ function assertNoAmbiguousRoutes<
854
+ T extends RoutePattern
855
+ >(
856
+ routes: T[],
857
+ type: string
858
+ ): void {
859
+ const signatures =
860
+ new Map<string, T>();
861
+
862
+ for (const route of routes) {
863
+ const signature =
864
+ createRouteSignature(
865
+ route
866
+ );
867
+
868
+ const existing =
869
+ signatures.get(
870
+ signature
871
+ );
872
+
873
+ if (
874
+ existing &&
875
+ existing.pathname !==
876
+ route.pathname
877
+ ) {
878
+ throw new Error(
879
+ `Ambiguous ${type} routes:\n` +
880
+ ` ${existing.pathname}\n` +
881
+ ` ${route.pathname}\n\n` +
882
+ "Both routes match the same URL pattern."
883
+ );
884
+ }
885
+
886
+ signatures.set(
887
+ signature,
888
+ route
889
+ );
890
+ }
891
+ }
892
+
893
+ function createRouteSignature(
894
+ route: RoutePattern
895
+ ): string {
896
+ return getUrlSegments(
897
+ route.segments
898
+ )
899
+ .map(
900
+ (segment) => {
901
+ switch (
902
+ segment.type
903
+ ) {
904
+ case "static":
905
+ return `s:${segment.value}`;
906
+ case "dynamic":
907
+ return "d:[]";
908
+ case "catchAll":
909
+ return "c:[...]";
910
+ case "optionalCatchAll":
911
+ return "o:[[...]]";
912
+ }
913
+ }
914
+ )
915
+ .join("/");
916
+ }
917
+
918
+ function isPageFile(
919
+ fileName: string
920
+ ): boolean {
921
+ return (
922
+ fileName === "page.ts" ||
923
+ fileName === "page.tsx"
924
+ );
925
+ }
926
+
927
+ function isApiRouteFile(
928
+ fileName: string
929
+ ): boolean {
930
+ return fileName ===
931
+ "route.ts";
932
+ }
933
+
934
+ function collectLayouts(
935
+ appDirectory: string,
936
+ pageFilePath: string
937
+ ): string[] {
938
+ const layouts: string[] = [];
939
+
940
+ const pageDirectory =
941
+ path.dirname(
942
+ pageFilePath
943
+ );
944
+
945
+ const relativeDirectory =
946
+ path.relative(
947
+ appDirectory,
948
+ pageDirectory
949
+ );
950
+
951
+ const segments =
952
+ relativeDirectory
953
+ ? relativeDirectory.split(
954
+ path.sep
955
+ )
956
+ : [];
957
+
958
+ let currentDirectory =
959
+ appDirectory;
960
+
961
+ const rootLayout =
962
+ findLayoutFile(
963
+ currentDirectory
964
+ );
965
+
966
+ if (rootLayout) {
967
+ layouts.push(
968
+ rootLayout
969
+ );
970
+ }
971
+
972
+ for (const segment of segments) {
973
+ currentDirectory =
974
+ path.join(
975
+ currentDirectory,
976
+ segment
977
+ );
978
+
979
+ const layout =
980
+ findLayoutFile(
981
+ currentDirectory
982
+ );
983
+
984
+ if (layout) {
985
+ layouts.push(
986
+ layout
987
+ );
988
+ }
989
+ }
990
+
991
+ return layouts;
992
+ }
993
+
994
+ function findLayoutFile(
995
+ directory: string
996
+ ): string | null {
997
+ const tsxFile =
998
+ path.join(
999
+ directory,
1000
+ "layout.tsx"
1001
+ );
1002
+ const tsFile =
1003
+ path.join(
1004
+ directory,
1005
+ "layout.ts"
1006
+ );
1007
+
1008
+ const hasTsx =
1009
+ fs.existsSync(
1010
+ tsxFile
1011
+ );
1012
+ const hasTs =
1013
+ fs.existsSync(
1014
+ tsFile
1015
+ );
1016
+
1017
+ if (hasTsx && hasTs) {
1018
+ throw new Error(
1019
+ `Both layout.tsx and layout.ts exist in ${directory}. Keep only one layout file.`
1020
+ );
1021
+ }
1022
+
1023
+ if (hasTsx) {
1024
+ return tsxFile;
1025
+ }
1026
+
1027
+ if (hasTs) {
1028
+ return tsFile;
1029
+ }
1030
+
1031
+ return null;
1032
+ }