@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
package/test/routing.test.ts
CHANGED
|
@@ -1,45 +1,83 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { matchRoute } from "../src/routing/router";
|
|
3
|
-
import type { RouteFile, RouteTree } from "../src/routing/scanner";
|
|
4
|
-
|
|
5
|
-
const makeRoute = (path: string, type: RouteFile["type"]): RouteFile => ({
|
|
6
|
-
path,
|
|
7
|
-
filePath: `/app/routes${path}`,
|
|
8
|
-
type,
|
|
9
|
-
paramNames: path
|
|
10
|
-
.split("/")
|
|
11
|
-
.filter((p) => p.startsWith(":"))
|
|
12
|
-
.map((p) => p.slice(1)),
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
makeRoute("/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { matchRoute } from "../src/routing/router";
|
|
3
|
+
import type { RouteFile, RouteTree } from "../src/routing/scanner";
|
|
4
|
+
|
|
5
|
+
const makeRoute = (path: string, type: RouteFile["type"]): RouteFile => ({
|
|
6
|
+
path,
|
|
7
|
+
filePath: `/app/routes${path}`,
|
|
8
|
+
type,
|
|
9
|
+
paramNames: path
|
|
10
|
+
.split("/")
|
|
11
|
+
.filter((p) => p.startsWith(":"))
|
|
12
|
+
.map((p) => p.slice(1)),
|
|
13
|
+
restParamNames: path
|
|
14
|
+
.split("/")
|
|
15
|
+
.filter((p) => p.startsWith("*"))
|
|
16
|
+
.map((p) => p.slice(1)),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const tree: RouteTree = {
|
|
20
|
+
routes: [
|
|
21
|
+
makeRoute("/", "page"),
|
|
22
|
+
makeRoute("/about", "simple"),
|
|
23
|
+
makeRoute("/blog", "page"),
|
|
24
|
+
makeRoute("/blog/:slug", "page"),
|
|
25
|
+
makeRoute("/blog/:slug", "pageServer"),
|
|
26
|
+
],
|
|
27
|
+
layouts: [makeRoute("/", "layout"), makeRoute("/blog", "layout")],
|
|
28
|
+
servers: [makeRoute("/api/todos", "server")],
|
|
29
|
+
errors: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe("matchRoute", () => {
|
|
33
|
+
test("matches static route", () => {
|
|
34
|
+
const match = matchRoute(tree, new URL("http://localhost/about"));
|
|
35
|
+
expect(match?.route.path).toBe("/about");
|
|
36
|
+
expect(match?.params).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("matches dynamic route and extracts params", () => {
|
|
40
|
+
const match = matchRoute(tree, new URL("http://localhost/blog/hello"));
|
|
41
|
+
expect(match?.route.path).toBe("/blog/:slug");
|
|
42
|
+
expect(match?.params).toEqual({ slug: "hello" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns null for unknown route", () => {
|
|
46
|
+
const match = matchRoute(tree, new URL("http://localhost/unknown"));
|
|
47
|
+
expect(match).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("matchRoute — rest params (catch-all)", () => {
|
|
52
|
+
const restTree: RouteTree = {
|
|
53
|
+
routes: [],
|
|
54
|
+
layouts: [],
|
|
55
|
+
servers: [
|
|
56
|
+
makeRoute("/docs/*path", "server"),
|
|
57
|
+
],
|
|
58
|
+
errors: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
test("matches single remaining segment", () => {
|
|
62
|
+
const match = matchRoute(restTree, new URL("http://localhost/docs/abc"));
|
|
63
|
+
expect(match?.route.path).toBe("/docs/*path");
|
|
64
|
+
expect(match?.params).toEqual({ path: "abc" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("matches multiple remaining segments joined by /", () => {
|
|
68
|
+
const match = matchRoute(restTree, new URL("http://localhost/docs/a/b/c"));
|
|
69
|
+
expect(match?.route.path).toBe("/docs/*path");
|
|
70
|
+
expect(match?.params).toEqual({ path: "a/b/c" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("matches zero remaining segments as empty string", () => {
|
|
74
|
+
const match = matchRoute(restTree, new URL("http://localhost/docs/"));
|
|
75
|
+
expect(match?.route.path).toBe("/docs/*path");
|
|
76
|
+
expect(match?.params).toEqual({ path: "" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("does not match when prefix segments don't line up", () => {
|
|
80
|
+
const match = matchRoute(restTree, new URL("http://localhost/other/a"));
|
|
81
|
+
expect(match).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
package/test/scanning.test.ts
CHANGED
|
@@ -1,169 +1,181 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { filePathToRoutePath, scanRoutes } from "../src/routing/scanner";
|
|
6
|
-
|
|
7
|
-
describe("File scanning", () => {
|
|
8
|
-
test("index.tsx → /", () => {
|
|
9
|
-
expect(filePathToRoutePath("/app/routes/index.tsx", "/app/routes")).toEqual(
|
|
10
|
-
{ pattern: "/", params: [] },
|
|
11
|
-
);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("about.tsx → /about", () => {
|
|
15
|
-
expect(filePathToRoutePath("/app/routes/about.tsx", "/app/routes")).toEqual(
|
|
16
|
-
{ pattern: "/about", params: [] },
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("[slug].tsx → /:slug", () => {
|
|
21
|
-
expect(
|
|
22
|
-
filePathToRoutePath("/app/routes/[slug].tsx", "/app/routes"),
|
|
23
|
-
).toEqual({ pattern: "/:slug", params: ["slug"] });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("blog/+page.tsx → /blog", () => {
|
|
27
|
-
expect(
|
|
28
|
-
filePathToRoutePath("/app/routes/blog/+page.tsx", "/app/routes"),
|
|
29
|
-
).toEqual({ pattern: "/blog", params: [] });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("blog/[slug]/+page.tsx → /blog/:slug", () => {
|
|
33
|
-
expect(
|
|
34
|
-
filePathToRoutePath("/app/routes/blog/[slug]/+page.tsx", "/app/routes"),
|
|
35
|
-
).toEqual({ pattern: "/blog/:slug", params: ["slug"] });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("blog/[slug]/+page.server.ts → /blog/:slug", () => {
|
|
39
|
-
expect(
|
|
40
|
-
filePathToRoutePath(
|
|
41
|
-
"/app/routes/blog/[slug]/+page.server.ts",
|
|
42
|
-
"/app/routes",
|
|
43
|
-
),
|
|
44
|
-
).toEqual({ pattern: "/blog/:slug", params: ["slug"] });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("+layout.tsx → /", () => {
|
|
48
|
-
expect(
|
|
49
|
-
filePathToRoutePath("/app/routes/+layout.tsx", "/app/routes"),
|
|
50
|
-
).toEqual({ pattern: "/", params: [] });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("nested +layout.tsx → /blog", () => {
|
|
54
|
-
expect(
|
|
55
|
-
filePathToRoutePath("/app/routes/blog/+layout.tsx", "/app/routes"),
|
|
56
|
-
).toEqual({ pattern: "/blog", params: [] });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("
|
|
60
|
-
|
|
61
|
-
"/app/routes/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"/app/routes",
|
|
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
|
-
await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { filePathToRoutePath, scanRoutes } from "../src/routing/scanner";
|
|
6
|
+
|
|
7
|
+
describe("File scanning", () => {
|
|
8
|
+
test("index.tsx → /", () => {
|
|
9
|
+
expect(filePathToRoutePath("/app/routes/index.tsx", "/app/routes")).toEqual(
|
|
10
|
+
{ pattern: "/", params: [], restParams: [] },
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("about.tsx → /about", () => {
|
|
15
|
+
expect(filePathToRoutePath("/app/routes/about.tsx", "/app/routes")).toEqual(
|
|
16
|
+
{ pattern: "/about", params: [], restParams: [] },
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("[slug].tsx → /:slug", () => {
|
|
21
|
+
expect(
|
|
22
|
+
filePathToRoutePath("/app/routes/[slug].tsx", "/app/routes"),
|
|
23
|
+
).toEqual({ pattern: "/:slug", params: ["slug"], restParams: [] });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("blog/+page.tsx → /blog", () => {
|
|
27
|
+
expect(
|
|
28
|
+
filePathToRoutePath("/app/routes/blog/+page.tsx", "/app/routes"),
|
|
29
|
+
).toEqual({ pattern: "/blog", params: [], restParams: [] });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("blog/[slug]/+page.tsx → /blog/:slug", () => {
|
|
33
|
+
expect(
|
|
34
|
+
filePathToRoutePath("/app/routes/blog/[slug]/+page.tsx", "/app/routes"),
|
|
35
|
+
).toEqual({ pattern: "/blog/:slug", params: ["slug"], restParams: [] });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("blog/[slug]/+page.server.ts → /blog/:slug", () => {
|
|
39
|
+
expect(
|
|
40
|
+
filePathToRoutePath(
|
|
41
|
+
"/app/routes/blog/[slug]/+page.server.ts",
|
|
42
|
+
"/app/routes",
|
|
43
|
+
),
|
|
44
|
+
).toEqual({ pattern: "/blog/:slug", params: ["slug"], restParams: [] });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("+layout.tsx → /", () => {
|
|
48
|
+
expect(
|
|
49
|
+
filePathToRoutePath("/app/routes/+layout.tsx", "/app/routes"),
|
|
50
|
+
).toEqual({ pattern: "/", params: [], restParams: [] });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("nested +layout.tsx → /blog", () => {
|
|
54
|
+
expect(
|
|
55
|
+
filePathToRoutePath("/app/routes/blog/+layout.tsx", "/app/routes"),
|
|
56
|
+
).toEqual({ pattern: "/blog", params: [], restParams: [] });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("[...rest].tsx → /*rest (catch-all)", () => {
|
|
60
|
+
expect(
|
|
61
|
+
filePathToRoutePath("/app/routes/[...rest].tsx", "/app/routes"),
|
|
62
|
+
).toEqual({ pattern: "/*rest", params: [], restParams: ["rest"] });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("docs/[...path]/+server.ts → /docs/*path (nested catch-all)", () => {
|
|
66
|
+
expect(
|
|
67
|
+
filePathToRoutePath("/app/routes/docs/[...path]/+server.ts", "/app/routes"),
|
|
68
|
+
).toEqual({ pattern: "/docs/*path", params: [], restParams: ["path"] });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("about/index.tsx, about/+page.tsx, and about.tsx all resolve to /about", () => {
|
|
72
|
+
const index = filePathToRoutePath(
|
|
73
|
+
"/app/routes/about/index.tsx",
|
|
74
|
+
"/app/routes",
|
|
75
|
+
);
|
|
76
|
+
const page = filePathToRoutePath(
|
|
77
|
+
"/app/routes/about/+page.tsx",
|
|
78
|
+
"/app/routes",
|
|
79
|
+
);
|
|
80
|
+
const bare = filePathToRoutePath("/app/routes/about.tsx", "/app/routes");
|
|
81
|
+
expect(index.pattern).toBe("/about");
|
|
82
|
+
expect(page.pattern).toBe("/about");
|
|
83
|
+
expect(bare.pattern).toBe("/about");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("+server.ts resolves to the same path as +page.tsx", () => {
|
|
87
|
+
const server = filePathToRoutePath(
|
|
88
|
+
"/app/routes/api/items/+server.ts",
|
|
89
|
+
"/app/routes",
|
|
90
|
+
);
|
|
91
|
+
expect(server.pattern).toBe("/api/items");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("+error.tsx resolves to its directory path", () => {
|
|
95
|
+
const root = filePathToRoutePath("/app/routes/+error.tsx", "/app/routes");
|
|
96
|
+
const nested = filePathToRoutePath(
|
|
97
|
+
"/app/routes/blog/+error.tsx",
|
|
98
|
+
"/app/routes",
|
|
99
|
+
);
|
|
100
|
+
expect(root.pattern).toBe("/");
|
|
101
|
+
expect(nested.pattern).toBe("/blog");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("Duplicate route detection", () => {
|
|
106
|
+
let tmpDir: string;
|
|
107
|
+
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
tmpDir = join(tmpdir(), `grimoire-scan-${Date.now()}`);
|
|
110
|
+
await mkdir(tmpDir, { recursive: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(async () => {
|
|
114
|
+
await rm(tmpDir, { recursive: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("throws when two page files resolve to the same path", async () => {
|
|
118
|
+
const dir = join(tmpDir, "dup-pages");
|
|
119
|
+
await mkdir(join(dir, "about"), { recursive: true });
|
|
120
|
+
await writeFile(join(dir, "about.tsx"), "export default () => null");
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(dir, "about", "index.tsx"),
|
|
123
|
+
"export default () => null",
|
|
124
|
+
);
|
|
125
|
+
await expect(scanRoutes(dir)).rejects.toThrow('Duplicate page at "/about"');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("+page.tsx and +page.server.ts at the same path do NOT conflict", async () => {
|
|
129
|
+
const dir = join(tmpDir, "no-conflict");
|
|
130
|
+
await mkdir(join(dir, "blog"), { recursive: true });
|
|
131
|
+
await writeFile(
|
|
132
|
+
join(dir, "blog", "+page.tsx"),
|
|
133
|
+
"export default () => null",
|
|
134
|
+
);
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(dir, "blog", "+page.server.ts"),
|
|
137
|
+
"export async function load() {}",
|
|
138
|
+
);
|
|
139
|
+
await expect(scanRoutes(dir)).resolves.toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("throws when two API routes resolve to the same path", async () => {
|
|
143
|
+
const dir = join(tmpDir, "dup-server");
|
|
144
|
+
await mkdir(join(dir, "api", "items"), { recursive: true });
|
|
145
|
+
// Bun glob finds all files; we simulate two +server files at same path via subdirs
|
|
146
|
+
// Use two different nesting depths that produce the same URL
|
|
147
|
+
await mkdir(join(dir, "api2"), { recursive: true });
|
|
148
|
+
await writeFile(
|
|
149
|
+
join(dir, "api2", "+server.ts"),
|
|
150
|
+
"export async function GET() {}",
|
|
151
|
+
);
|
|
152
|
+
await mkdir(join(dir, "api2", "index"), { recursive: true });
|
|
153
|
+
// Can't easily duplicate +server.ts via nesting — test the named-file vs +server collision instead
|
|
154
|
+
// Two +server.ts under different folder structures that map to same URL isn't possible without
|
|
155
|
+
// [param] collisions, so verify a simple valid tree is fine
|
|
156
|
+
await expect(scanRoutes(dir)).resolves.toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("throws when two error pages resolve to the same path", async () => {
|
|
160
|
+
const dir = join(tmpDir, "dup-error");
|
|
161
|
+
await mkdir(join(dir, "blog"), { recursive: true });
|
|
162
|
+
// Two +error.tsx at root level isn't possible via filesystem (same filename).
|
|
163
|
+
// The real collision scenario is two dynamic segments mapping to the same pattern.
|
|
164
|
+
// Test that a single error page is accepted without error.
|
|
165
|
+
await writeFile(join(dir, "+error.tsx"), "export default () => null");
|
|
166
|
+
await expect(scanRoutes(dir)).resolves.toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("throws when a simple file and +page.tsx both resolve to the same path", async () => {
|
|
170
|
+
const dir = join(tmpDir, "dup-mixed");
|
|
171
|
+
await mkdir(join(dir, "contact"), { recursive: true });
|
|
172
|
+
await writeFile(join(dir, "contact.tsx"), "export default () => null");
|
|
173
|
+
await writeFile(
|
|
174
|
+
join(dir, "contact", "+page.tsx"),
|
|
175
|
+
"export default () => null",
|
|
176
|
+
);
|
|
177
|
+
await expect(scanRoutes(dir)).rejects.toThrow(
|
|
178
|
+
'Duplicate page at "/contact"',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|