@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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/docs/caching.md +76 -0
- package/docs/configuration.md +97 -0
- package/docs/deployment.md +74 -0
- package/docs/getting-started.md +82 -0
- package/docs/middleware.md +58 -0
- package/docs/releasing.md +299 -0
- package/docs/routing.md +103 -0
- package/docs/security.md +57 -0
- package/package.json +68 -0
- package/packages/bundler/src/client-islands.ts +1457 -0
- package/packages/bundler/src/incremental-context.ts +206 -0
- package/packages/bundler/src/index.ts +1991 -0
- package/packages/bundler/src/module-graph.ts +317 -0
- package/packages/bundler/src/partial-hydration.ts +414 -0
- package/packages/bundler/src/production.ts +974 -0
- package/packages/bundler/src/server-production-middleware.ts +447 -0
- package/packages/bundler/src/server-production.ts +1193 -0
- package/packages/bundler/src/special-files.ts +131 -0
- package/packages/cache/src/index.ts +761 -0
- package/packages/cli/bin/bcp.mjs +93 -0
- package/packages/cli/src/args.ts +305 -0
- package/packages/cli/src/bootstrap.ts +514 -0
- package/packages/cli/src/index.ts +504 -0
- package/packages/cli/src/version.ts +45 -0
- package/packages/client/src/cache.ts +11 -0
- package/packages/client/src/config.ts +18 -0
- package/packages/client/src/error-boundary.tsx +149 -0
- package/packages/client/src/hydration.ts +3 -0
- package/packages/client/src/index.tsx +57 -0
- package/packages/client/src/islands.tsx +315 -0
- package/packages/client/src/metadata.ts +281 -0
- package/packages/client/src/navigation-loading.ts +52 -0
- package/packages/client/src/navigation-state.ts +80 -0
- package/packages/client/src/not-found.ts +34 -0
- package/packages/client/src/persistent-layout-runtime.ts +273 -0
- package/packages/client/src/router-v2.tsx +969 -0
- package/packages/client/src/router.tsx +1 -0
- package/packages/config/src/index.ts +1038 -0
- package/packages/env/src/index.ts +593 -0
- package/packages/router/src/advanced-router.ts +1032 -0
- package/packages/router/src/index.ts +1 -0
- package/packages/server/src/compression.ts +249 -0
- package/packages/server/src/dev-document-metadata.ts +154 -0
- package/packages/server/src/dev-hmr.ts +225 -0
- package/packages/server/src/index.ts +2265 -0
- package/packages/server/src/metadata.ts +478 -0
- package/packages/server/src/middleware-dev-server.ts +260 -0
- package/packages/server/src/middleware-loader.ts +140 -0
- package/packages/server/src/middleware-proxy.ts +516 -0
- package/packages/server/src/middleware.ts +704 -0
- package/packages/server/src/navigation-payload.ts +471 -0
- package/packages/server/src/production-server.ts +1746 -0
- package/packages/server/src/response-cache-proxy.ts +828 -0
- package/packages/server/src/security-proxy.ts +406 -0
- package/packages/server/src/security.ts +451 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
- package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
- package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
- package/packages/server/src/standalone-production-runtime.ts +1951 -0
- package/packages/server/src/standalone-production-server.ts +6 -0
- package/packages/server/src/static-assets.ts +262 -0
- 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
|
+
}
|