@sigil-dev/grimoire 0.7.5 → 0.7.6
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/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +34 -34
- package/package.json +8 -4
- package/preload.js +2 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +224 -76
- package/src/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +72 -72
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +81 -27
- package/src/rendering/index.ts +199 -186
- package/src/rendering/ssrPlugin.ts +53 -47
- package/src/routing/manifest-gen.ts +39 -26
- package/src/routing/router.ts +106 -98
- package/src/routing/scanner.ts +135 -129
- package/src/routing/transform-routes.ts +101 -101
- package/src/server/build.ts +147 -90
- package/src/server/coordinator.ts +306 -297
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +144 -70
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +353 -340
- package/src/types.ts +269 -260
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -221
- package/test/rendering.test.ts +425 -425
- package/test/routing.test.ts +83 -45
- package/test/scanning.test.ts +181 -169
- package/test/server.test.ts +229 -229
- package/test/streaming.test.ts +106 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +19 -1
|
@@ -1,26 +1,39 @@
|
|
|
1
|
-
import type { RouteFile } from "./scanner";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generates a manifest module that re-exports page route components keyed
|
|
5
|
-
* by route path. Emits plain JS so the same output is valid as both `.ts`
|
|
6
|
-
* and `.js`.
|
|
7
|
-
*
|
|
8
|
-
* If `fileMap` is provided, original `filePath`s are remapped to compiled
|
|
9
|
-
* `.js` paths (used after pre-transforming routes to plain JS).
|
|
10
|
-
*/
|
|
11
|
-
export function generateManifest(
|
|
12
|
-
routes: RouteFile[],
|
|
13
|
-
fileMap?: Map<string, string>,
|
|
14
|
-
): string {
|
|
15
|
-
const pages = routes.filter((r) => r.type === "page" || r.type === "simple");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import type { RouteFile } from "./scanner";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a manifest module that re-exports page route components keyed
|
|
5
|
+
* by route path. Emits plain JS so the same output is valid as both `.ts`
|
|
6
|
+
* and `.js`.
|
|
7
|
+
*
|
|
8
|
+
* If `fileMap` is provided, original `filePath`s are remapped to compiled
|
|
9
|
+
* `.js` paths (used after pre-transforming routes to plain JS).
|
|
10
|
+
*/
|
|
11
|
+
export function generateManifest(
|
|
12
|
+
routes: RouteFile[],
|
|
13
|
+
fileMap?: Map<string, string>,
|
|
14
|
+
): string {
|
|
15
|
+
const pages = routes.filter((r) => r.type === "page" || r.type === "simple");
|
|
16
|
+
const layouts = routes.filter((r) => r.type === "layout");
|
|
17
|
+
|
|
18
|
+
const pageImports = pages
|
|
19
|
+
.map((r, i) => {
|
|
20
|
+
const path = fileMap?.get(r.filePath) ?? r.filePath;
|
|
21
|
+
return `import __Page${i} from ${JSON.stringify(path)};`;
|
|
22
|
+
})
|
|
23
|
+
.join("\n");
|
|
24
|
+
const layoutImports = layouts
|
|
25
|
+
.map((r, i) => {
|
|
26
|
+
const path = fileMap?.get(r.filePath) ?? r.filePath;
|
|
27
|
+
return `import __Layout${i} from ${JSON.stringify(path)};`;
|
|
28
|
+
})
|
|
29
|
+
.join("\n");
|
|
30
|
+
|
|
31
|
+
const map = pages
|
|
32
|
+
.map((r, i) => ` ${JSON.stringify(r.path)}: __Page${i},`)
|
|
33
|
+
.join("\n");
|
|
34
|
+
const layoutMap = layouts
|
|
35
|
+
.map((r, i) => ` { path: ${JSON.stringify(r.path)}, component: __Layout${i} },`)
|
|
36
|
+
.join("\n");
|
|
37
|
+
|
|
38
|
+
return `${pageImports}\n${layoutImports}\nexport const routes = {\n${map}\n};\nexport const layouts = [\n${layoutMap}\n];\n`;
|
|
39
|
+
}
|
package/src/routing/router.ts
CHANGED
|
@@ -1,98 +1,106 @@
|
|
|
1
|
-
import type { RouteFile, RouteTree } from "./scanner";
|
|
2
|
-
|
|
3
|
-
export interface MatchedRoute {
|
|
4
|
-
route: RouteFile;
|
|
5
|
-
params: Record<string, string>;
|
|
6
|
-
layouts: RouteFile[]; // was: layout?: RouteFile
|
|
7
|
-
layoutServers: RouteFile[]; // was: layoutServer?: RouteFile
|
|
8
|
-
pageServer?: RouteFile;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function matchRoute(tree: RouteTree, url: URL): MatchedRoute | null {
|
|
12
|
-
const pathname = url.pathname;
|
|
13
|
-
|
|
14
|
-
// check server routes first
|
|
15
|
-
for (const server of tree.servers) {
|
|
16
|
-
const params = matchPattern(server.path, pathname);
|
|
17
|
-
if (params !== null) return {
|
|
18
|
-
route: server,
|
|
19
|
-
params,
|
|
20
|
-
layouts: [],
|
|
21
|
-
layoutServers: []
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// then pages
|
|
26
|
-
for (const route of tree.routes) {
|
|
27
|
-
if (route.type === "pageServer") continue;
|
|
28
|
-
const params = matchPattern(route.path, pathname);
|
|
29
|
-
if (params === null) continue;
|
|
30
|
-
|
|
31
|
-
// find applicable
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
(l)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
.sort((a, b) => a.path.length - b.path.length); // root first, innermost last
|
|
39
|
-
|
|
40
|
-
const layoutServers = tree.layouts
|
|
41
|
-
.filter(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
1
|
+
import type { RouteFile, RouteTree } from "./scanner";
|
|
2
|
+
|
|
3
|
+
export interface MatchedRoute {
|
|
4
|
+
route: RouteFile;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
layouts: RouteFile[]; // was: layout?: RouteFile
|
|
7
|
+
layoutServers: RouteFile[]; // was: layoutServer?: RouteFile
|
|
8
|
+
pageServer?: RouteFile;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function matchRoute(tree: RouteTree, url: URL): MatchedRoute | null {
|
|
12
|
+
const pathname = url.pathname;
|
|
13
|
+
|
|
14
|
+
// check server routes first
|
|
15
|
+
for (const server of tree.servers) {
|
|
16
|
+
const params = matchPattern(server.path, pathname);
|
|
17
|
+
if (params !== null) return {
|
|
18
|
+
route: server,
|
|
19
|
+
params,
|
|
20
|
+
layouts: [],
|
|
21
|
+
layoutServers: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// then pages
|
|
26
|
+
for (const route of tree.routes) {
|
|
27
|
+
if (route.type === "pageServer") continue;
|
|
28
|
+
const params = matchPattern(route.path, pathname);
|
|
29
|
+
if (params === null) continue;
|
|
30
|
+
|
|
31
|
+
// find applicable layouts by matching route pattern prefix, not URL pathname.
|
|
32
|
+
// pathname.startsWith(l.path) would fail for parameterized layouts like /w/:workspace.
|
|
33
|
+
const layoutMatches = (l: RouteFile) =>
|
|
34
|
+
l.path === "/" || route.path === l.path || route.path.startsWith(l.path + "/");
|
|
35
|
+
|
|
36
|
+
const layouts = tree.layouts
|
|
37
|
+
.filter((l) => l.type === "layout" && layoutMatches(l))
|
|
38
|
+
.sort((a, b) => a.path.length - b.path.length); // root first, innermost last
|
|
39
|
+
|
|
40
|
+
const layoutServers = tree.layouts
|
|
41
|
+
.filter((l) => l.type === "layoutServer" && layoutMatches(l))
|
|
42
|
+
.sort((a, b) => a.path.length - b.path.length); // root first, innermost last
|
|
43
|
+
|
|
44
|
+
const pageServer = tree.routes.find(
|
|
45
|
+
(r) => r.type === "pageServer" && r.path === route.path,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return { route, params, layouts, layoutServers, pageServer };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function findClosestError(
|
|
55
|
+
errors: RouteFile[],
|
|
56
|
+
pathname: string,
|
|
57
|
+
): RouteFile | null {
|
|
58
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
59
|
+
|
|
60
|
+
// walk from most specific to root
|
|
61
|
+
while (segments.length >= 0) {
|
|
62
|
+
const prefix = `/${segments.join("/")}`;
|
|
63
|
+
const error = errors.find(
|
|
64
|
+
(e) => e.path === prefix || e.path === prefix + "/",
|
|
65
|
+
);
|
|
66
|
+
if (error) return error;
|
|
67
|
+
if (segments.length === 0) break;
|
|
68
|
+
segments.pop();
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function matchPattern(
|
|
74
|
+
pattern: string,
|
|
75
|
+
pathname: string,
|
|
76
|
+
): Record<string, string> | null {
|
|
77
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
78
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
79
|
+
|
|
80
|
+
const params: Record<string, string> = {};
|
|
81
|
+
let pi = 0;
|
|
82
|
+
let pj = 0;
|
|
83
|
+
while (pi < patternParts.length) {
|
|
84
|
+
const patternPart = patternParts[pi]!;
|
|
85
|
+
|
|
86
|
+
if (patternPart.startsWith("*")) {
|
|
87
|
+
// Rest param — captures zero or more remaining path segments joined by "/".
|
|
88
|
+
// Rest must be the last segment in the pattern.
|
|
89
|
+
params[patternPart.slice(1)] = pathParts.slice(pj).join("/");
|
|
90
|
+
return params;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (pj >= pathParts.length) return null;
|
|
94
|
+
const pathPart = pathParts[pj]!;
|
|
95
|
+
|
|
96
|
+
if (patternPart.startsWith(":")) {
|
|
97
|
+
params[patternPart.slice(1)] = pathPart;
|
|
98
|
+
} else if (patternPart !== pathPart) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
pi++;
|
|
102
|
+
pj++;
|
|
103
|
+
}
|
|
104
|
+
if (pj < pathParts.length) return null;
|
|
105
|
+
return params;
|
|
106
|
+
}
|
package/src/routing/scanner.ts
CHANGED
|
@@ -1,129 +1,135 @@
|
|
|
1
|
-
import { basename, join, relative } from "node:path";
|
|
2
|
-
|
|
3
|
-
export interface RouteFile {
|
|
4
|
-
path: string; // URL (ie /blog/:slug)
|
|
5
|
-
filePath: string; // absolute disk path;
|
|
6
|
-
clientPath: string; // Relative path (/src/routes/spells/+page.tsx)
|
|
7
|
-
type:
|
|
8
|
-
| "page"
|
|
9
|
-
| "pageServer"
|
|
10
|
-
| "layout"
|
|
11
|
-
| "layoutServer"
|
|
12
|
-
| "server"
|
|
13
|
-
| "error"
|
|
14
|
-
| "simple";
|
|
15
|
-
paramNames: string[]; // ["slug"] in /blog/:slug
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.replace(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
seen.
|
|
128
|
-
|
|
129
|
-
|
|
1
|
+
import { basename, join, relative } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface RouteFile {
|
|
4
|
+
path: string; // URL (ie /blog/:slug)
|
|
5
|
+
filePath: string; // absolute disk path;
|
|
6
|
+
clientPath: string; // Relative path (/src/routes/spells/+page.tsx)
|
|
7
|
+
type:
|
|
8
|
+
| "page"
|
|
9
|
+
| "pageServer"
|
|
10
|
+
| "layout"
|
|
11
|
+
| "layoutServer"
|
|
12
|
+
| "server"
|
|
13
|
+
| "error"
|
|
14
|
+
| "simple";
|
|
15
|
+
paramNames: string[]; // ["slug"] in /blog/:slug
|
|
16
|
+
restParamNames: string[]; // ["rest"] in /blog/*rest
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RouteTree {
|
|
20
|
+
routes: RouteFile[];
|
|
21
|
+
layouts: RouteFile[]; // layout + layout.server
|
|
22
|
+
servers: RouteFile[]; // +server.ts API routes
|
|
23
|
+
errors: RouteFile[]; // +error.tsx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function filePathToRoutePath(
|
|
27
|
+
filePath: string,
|
|
28
|
+
routesDir: string,
|
|
29
|
+
): { pattern: string; params: string[]; restParams: string[] } {
|
|
30
|
+
const rel = relative(routesDir, filePath)
|
|
31
|
+
.replace(/\\/g, "/") // windows
|
|
32
|
+
.replace(/\.(tsx?|jsx?)$/, "");
|
|
33
|
+
|
|
34
|
+
const params: string[] = [];
|
|
35
|
+
const restParams: string[] = [];
|
|
36
|
+
const pattern =
|
|
37
|
+
rel
|
|
38
|
+
.replace(/\/?(\+\w+(\.\w+)?|index)$/, "") // index/+page → directory
|
|
39
|
+
.replace(/\[(\.\.\.)?([^\]]+)\]/g, (_, dots, name) => {
|
|
40
|
+
// [...rest] → *rest (captures zero or more remaining path segments)
|
|
41
|
+
if (dots) {
|
|
42
|
+
restParams.push(name);
|
|
43
|
+
return `*${name}`;
|
|
44
|
+
}
|
|
45
|
+
// [param] → :param
|
|
46
|
+
params.push(name);
|
|
47
|
+
return `:${name}`;
|
|
48
|
+
})
|
|
49
|
+
.replace(/^\/?/, "/") || // ensure leading slash
|
|
50
|
+
"/";
|
|
51
|
+
|
|
52
|
+
return { pattern, params, restParams };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function scanRoutes(
|
|
56
|
+
routesDir: string,
|
|
57
|
+
viteRoot: string = routesDir,
|
|
58
|
+
): Promise<RouteTree> {
|
|
59
|
+
const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js}");
|
|
60
|
+
const routes: RouteFile[] = [];
|
|
61
|
+
const layouts: RouteFile[] = [];
|
|
62
|
+
const servers: RouteFile[] = [];
|
|
63
|
+
const errors: RouteFile[] = [];
|
|
64
|
+
|
|
65
|
+
for await (const file of glob.scan(routesDir)) {
|
|
66
|
+
const filePath = join(routesDir, file);
|
|
67
|
+
const name = basename(file).replace(/\.(tsx?|jsx?)$/, "");
|
|
68
|
+
const { pattern, params, restParams } = filePathToRoutePath(filePath, routesDir);
|
|
69
|
+
|
|
70
|
+
let type: RouteFile["type"];
|
|
71
|
+
if (name === "+page") type = "page";
|
|
72
|
+
else if (name === "+page.server") type = "pageServer";
|
|
73
|
+
else if (name === "+layout") type = "layout";
|
|
74
|
+
else if (name === "+layout.server") type = "layoutServer";
|
|
75
|
+
else if (name === "+server") type = "server";
|
|
76
|
+
else if (name === "+error") type = "error";
|
|
77
|
+
else type = "simple";
|
|
78
|
+
|
|
79
|
+
const clientPath = "/" + relative(viteRoot, filePath).replace(/\\/g, "/");
|
|
80
|
+
|
|
81
|
+
const routeFile: RouteFile = {
|
|
82
|
+
path: pattern,
|
|
83
|
+
filePath,
|
|
84
|
+
clientPath, // add this
|
|
85
|
+
type,
|
|
86
|
+
paramNames: params,
|
|
87
|
+
restParamNames: restParams,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (type === "simple" || type === "page" || type === "pageServer")
|
|
91
|
+
routes.push(routeFile);
|
|
92
|
+
if (type === "layout" || type === "layoutServer") layouts.push(routeFile);
|
|
93
|
+
if (type === "server") servers.push(routeFile);
|
|
94
|
+
if (type === "error") errors.push(routeFile);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// sort so static routes match before dynamic ones, rest params last
|
|
98
|
+
const scoreRoute = (r: RouteFile) => r.paramNames.length + r.restParamNames.length * 1000;
|
|
99
|
+
routes.sort((a, b) => scoreRoute(a) - scoreRoute(b));
|
|
100
|
+
servers.sort((a, b) => scoreRoute(a) - scoreRoute(b));
|
|
101
|
+
|
|
102
|
+
assertNoDuplicatePaths(
|
|
103
|
+
routes.filter((r) => r.type === "page" || r.type === "simple"),
|
|
104
|
+
"page",
|
|
105
|
+
);
|
|
106
|
+
assertNoDuplicatePaths(
|
|
107
|
+
routes.filter((r) => r.type === "pageServer"),
|
|
108
|
+
"page server",
|
|
109
|
+
);
|
|
110
|
+
assertNoDuplicatePaths(
|
|
111
|
+
layouts.filter((l) => l.type === "layout"),
|
|
112
|
+
"layout",
|
|
113
|
+
);
|
|
114
|
+
assertNoDuplicatePaths(
|
|
115
|
+
layouts.filter((l) => l.type === "layoutServer"),
|
|
116
|
+
"layout server",
|
|
117
|
+
);
|
|
118
|
+
assertNoDuplicatePaths(servers, "API route");
|
|
119
|
+
assertNoDuplicatePaths(errors, "error page");
|
|
120
|
+
|
|
121
|
+
return { routes, layouts, servers, errors };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assertNoDuplicatePaths(files: RouteFile[], label: string): void {
|
|
125
|
+
const seen = new Map<string, string>();
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const existing = seen.get(file.path);
|
|
128
|
+
if (existing) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Duplicate ${label} at "${file.path}": "${existing}" and "${file.clientPath}"`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
seen.set(file.path, file.clientPath);
|
|
134
|
+
}
|
|
135
|
+
}
|