alabjs 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/dist/adapters/cloudflare.d.ts +31 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +30 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +22 -0
- package/dist/adapters/deno.d.ts.map +1 -0
- package/dist/adapters/deno.js +21 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/web.d.ts +47 -0
- package/dist/adapters/web.d.ts.map +1 -0
- package/dist/adapters/web.js +212 -0
- package/dist/adapters/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/hooks.d.ts +119 -0
- package/dist/client/hooks.d.ts.map +1 -0
- package/dist/client/hooks.js +220 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/client/hooks.test.d.ts +2 -0
- package/dist/client/hooks.test.d.ts.map +1 -0
- package/dist/client/hooks.test.js +45 -0
- package/dist/client/hooks.test.js.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/offline.d.ts +52 -0
- package/dist/client/offline.d.ts.map +1 -0
- package/dist/client/offline.js +90 -0
- package/dist/client/offline.js.map +1 -0
- package/dist/client/provider.d.ts +12 -0
- package/dist/client/provider.d.ts.map +1 -0
- package/dist/client/provider.js +10 -0
- package/dist/client/provider.js.map +1 -0
- package/dist/commands/build.d.ts +18 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +173 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +447 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/info.d.ts +6 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +92 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/ssg.d.ts +8 -0
- package/dist/commands/ssg.d.ts.map +1 -0
- package/dist/commands/ssg.js +124 -0
- package/dist/commands/ssg.js.map +1 -0
- package/dist/commands/start.d.ts +7 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +26 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/test.d.ts +24 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +87 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +38 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +46 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/Font.d.ts +57 -0
- package/dist/components/Font.d.ts.map +1 -0
- package/dist/components/Font.js +33 -0
- package/dist/components/Font.js.map +1 -0
- package/dist/components/Image.d.ts +74 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +85 -0
- package/dist/components/Image.js.map +1 -0
- package/dist/components/Link.d.ts +23 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +48 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/Script.d.ts +37 -0
- package/dist/components/Script.d.ts.map +1 -0
- package/dist/components/Script.js +70 -0
- package/dist/components/Script.js.map +1 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +6 -0
- package/dist/components/index.js.map +1 -0
- package/dist/i18n/i18n.test.d.ts +2 -0
- package/dist/i18n/i18n.test.d.ts.map +1 -0
- package/dist/i18n/i18n.test.js +132 -0
- package/dist/i18n/i18n.test.js.map +1 -0
- package/dist/i18n/index.d.ts +135 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +189 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/router/code-router.d.ts +204 -0
- package/dist/router/code-router.d.ts.map +1 -0
- package/dist/router/code-router.js +258 -0
- package/dist/router/code-router.js.map +1 -0
- package/dist/router/code-router.test.d.ts +2 -0
- package/dist/router/code-router.test.d.ts.map +1 -0
- package/dist/router/code-router.test.js +128 -0
- package/dist/router/code-router.test.js.map +1 -0
- package/dist/router/index.d.ts +4 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +2 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/manifest.d.ts +12 -0
- package/dist/router/manifest.d.ts.map +1 -0
- package/dist/router/manifest.js +2 -0
- package/dist/router/manifest.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +407 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/cache.d.ts +99 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +161 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/cache.test.d.ts +2 -0
- package/dist/server/cache.test.d.ts.map +1 -0
- package/dist/server/cache.test.js +150 -0
- package/dist/server/cache.test.js.map +1 -0
- package/dist/server/csrf.d.ts +28 -0
- package/dist/server/csrf.d.ts.map +1 -0
- package/dist/server/csrf.js +66 -0
- package/dist/server/csrf.js.map +1 -0
- package/dist/server/csrf.test.d.ts +2 -0
- package/dist/server/csrf.test.d.ts.map +1 -0
- package/dist/server/csrf.test.js +154 -0
- package/dist/server/csrf.test.js.map +1 -0
- package/dist/server/image.d.ts +18 -0
- package/dist/server/image.d.ts.map +1 -0
- package/dist/server/image.js +97 -0
- package/dist/server/image.js.map +1 -0
- package/dist/server/index.d.ts +57 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +58 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +53 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +80 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test.d.ts +2 -0
- package/dist/server/middleware.test.d.ts.map +1 -0
- package/dist/server/middleware.test.js +125 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/revalidate.d.ts +49 -0
- package/dist/server/revalidate.d.ts.map +1 -0
- package/dist/server/revalidate.js +62 -0
- package/dist/server/revalidate.js.map +1 -0
- package/dist/server/revalidate.test.d.ts +2 -0
- package/dist/server/revalidate.test.d.ts.map +1 -0
- package/dist/server/revalidate.test.js +93 -0
- package/dist/server/revalidate.test.js.map +1 -0
- package/dist/server/server-fn.test.d.ts +2 -0
- package/dist/server/server-fn.test.d.ts.map +1 -0
- package/dist/server/server-fn.test.js +105 -0
- package/dist/server/server-fn.test.js.map +1 -0
- package/dist/server/sitemap.d.ts +9 -0
- package/dist/server/sitemap.d.ts.map +1 -0
- package/dist/server/sitemap.js +26 -0
- package/dist/server/sitemap.js.map +1 -0
- package/dist/server/sitemap.test.d.ts +2 -0
- package/dist/server/sitemap.test.d.ts.map +1 -0
- package/dist/server/sitemap.test.js +61 -0
- package/dist/server/sitemap.test.js.map +1 -0
- package/dist/server/sse.d.ts +59 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +91 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/sse.test.d.ts +2 -0
- package/dist/server/sse.test.d.ts.map +1 -0
- package/dist/server/sse.test.js +68 -0
- package/dist/server/sse.test.js.map +1 -0
- package/dist/signals/index.d.ts +101 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/index.js +149 -0
- package/dist/signals/index.js.map +1 -0
- package/dist/signals/signals.test.d.ts +2 -0
- package/dist/signals/signals.test.d.ts.map +1 -0
- package/dist/signals/signals.test.js +146 -0
- package/dist/signals/signals.test.js.map +1 -0
- package/dist/ssr/html.d.ts +27 -0
- package/dist/ssr/html.d.ts.map +1 -0
- package/dist/ssr/html.js +107 -0
- package/dist/ssr/html.js.map +1 -0
- package/dist/ssr/html.test.d.ts +2 -0
- package/dist/ssr/html.test.d.ts.map +1 -0
- package/dist/ssr/html.test.js +178 -0
- package/dist/ssr/html.test.js.map +1 -0
- package/dist/ssr/render.d.ts +46 -0
- package/dist/ssr/render.d.ts.map +1 -0
- package/dist/ssr/render.js +87 -0
- package/dist/ssr/render.js.map +1 -0
- package/dist/ssr/router-dev.d.ts +60 -0
- package/dist/ssr/router-dev.d.ts.map +1 -0
- package/dist/ssr/router-dev.js +205 -0
- package/dist/ssr/router-dev.js.map +1 -0
- package/dist/ssr/router-dev.test.d.ts +2 -0
- package/dist/ssr/router-dev.test.d.ts.map +1 -0
- package/dist/ssr/router-dev.test.js +189 -0
- package/dist/ssr/router-dev.test.js.map +1 -0
- package/dist/test/index.d.ts +93 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +146 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/napi.d.ts +15 -0
- package/dist/types/napi.d.ts.map +1 -0
- package/dist/types/napi.js +2 -0
- package/dist/types/napi.js.map +1 -0
- package/package.json +107 -0
- package/src/adapters/cloudflare.ts +30 -0
- package/src/adapters/deno.ts +21 -0
- package/src/adapters/web.ts +259 -0
- package/src/cli.ts +68 -0
- package/src/client/hooks.test.ts +54 -0
- package/src/client/hooks.ts +329 -0
- package/src/client/index.ts +5 -0
- package/src/client/offline-sw.ts +191 -0
- package/src/client/offline.ts +114 -0
- package/src/client/provider.tsx +14 -0
- package/src/commands/build.ts +201 -0
- package/src/commands/dev.ts +509 -0
- package/src/commands/info.ts +111 -0
- package/src/commands/ssg.ts +177 -0
- package/src/commands/start.ts +32 -0
- package/src/commands/test.ts +102 -0
- package/src/components/ErrorBoundary.tsx +73 -0
- package/src/components/Font.tsx +100 -0
- package/src/components/Image.tsx +141 -0
- package/src/components/Link.tsx +64 -0
- package/src/components/Script.tsx +97 -0
- package/src/components/index.ts +9 -0
- package/src/i18n/i18n.test.tsx +169 -0
- package/src/i18n/index.tsx +256 -0
- package/src/index.ts +10 -0
- package/src/router/code-router.test.ts +146 -0
- package/src/router/code-router.tsx +459 -0
- package/src/router/index.ts +18 -0
- package/src/router/manifest.ts +13 -0
- package/src/server/app.ts +466 -0
- package/src/server/cache.test.ts +192 -0
- package/src/server/cache.ts +195 -0
- package/src/server/csrf.test.ts +199 -0
- package/src/server/csrf.ts +80 -0
- package/src/server/image.ts +112 -0
- package/src/server/index.ts +144 -0
- package/src/server/middleware.test.ts +151 -0
- package/src/server/middleware.ts +95 -0
- package/src/server/revalidate.test.ts +106 -0
- package/src/server/revalidate.ts +75 -0
- package/src/server/server-fn.test.ts +127 -0
- package/src/server/sitemap.test.ts +68 -0
- package/src/server/sitemap.ts +30 -0
- package/src/server/sse.test.ts +81 -0
- package/src/server/sse.ts +110 -0
- package/src/signals/index.ts +177 -0
- package/src/signals/signals.test.ts +164 -0
- package/src/ssr/html.test.ts +200 -0
- package/src/ssr/html.ts +140 -0
- package/src/ssr/render.ts +144 -0
- package/src/ssr/router-dev.test.ts +230 -0
- package/src/ssr/router-dev.ts +229 -0
- package/src/test/index.ts +206 -0
- package/src/types/compiler.d.ts +25 -0
- package/src/types/index.ts +147 -0
- package/src/types/napi.ts +20 -0
- package/src/types/plugins.d.ts +3 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
scanDevRoutes,
|
|
7
|
+
matchDevRoute,
|
|
8
|
+
findLayoutFiles,
|
|
9
|
+
findErrorFile,
|
|
10
|
+
findLoadingFile,
|
|
11
|
+
scanDevApiRoutes,
|
|
12
|
+
matchDevApiRoute,
|
|
13
|
+
} from "./router-dev.js";
|
|
14
|
+
|
|
15
|
+
let appDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
appDir = mkdtempSync(join(tmpdir(), "alab-test-"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
rmSync(appDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function createFile(relPath: string, content = "export default function Page() { return null; }") {
|
|
26
|
+
const full = join(appDir, relPath);
|
|
27
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
28
|
+
writeFileSync(full, content, "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── scanDevRoutes ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("scanDevRoutes", () => {
|
|
34
|
+
it("finds page.tsx at root", () => {
|
|
35
|
+
createFile("page.tsx");
|
|
36
|
+
const routes = scanDevRoutes(appDir);
|
|
37
|
+
expect(routes).toHaveLength(1);
|
|
38
|
+
expect(routes[0]!.pattern.test("/")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("finds nested page.tsx", () => {
|
|
42
|
+
createFile("about/page.tsx");
|
|
43
|
+
const routes = scanDevRoutes(appDir);
|
|
44
|
+
expect(routes).toHaveLength(1);
|
|
45
|
+
expect(routes[0]!.pattern.test("/about")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("finds dynamic route [id]", () => {
|
|
49
|
+
createFile("users/[id]/page.tsx");
|
|
50
|
+
const routes = scanDevRoutes(appDir);
|
|
51
|
+
expect(routes).toHaveLength(1);
|
|
52
|
+
expect(routes[0]!.pattern.test("/users/42")).toBe(true);
|
|
53
|
+
expect(routes[0]!.paramNames).toEqual(["id"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("sorts static routes before dynamic ones", () => {
|
|
57
|
+
createFile("users/new/page.tsx");
|
|
58
|
+
createFile("users/[id]/page.tsx");
|
|
59
|
+
const routes = scanDevRoutes(appDir);
|
|
60
|
+
expect(routes).toHaveLength(2);
|
|
61
|
+
// Static route (0 params) should come first
|
|
62
|
+
expect(routes[0]!.paramNames).toHaveLength(0);
|
|
63
|
+
expect(routes[1]!.paramNames).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("finds page.ts files too", () => {
|
|
67
|
+
createFile("page.ts");
|
|
68
|
+
const routes = scanDevRoutes(appDir);
|
|
69
|
+
expect(routes).toHaveLength(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns empty for empty directory", () => {
|
|
73
|
+
const routes = scanDevRoutes(appDir);
|
|
74
|
+
expect(routes).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── matchDevRoute ────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("matchDevRoute", () => {
|
|
81
|
+
it("matches a static route", () => {
|
|
82
|
+
createFile("about/page.tsx");
|
|
83
|
+
const routes = scanDevRoutes(appDir);
|
|
84
|
+
const match = matchDevRoute(routes, "/about");
|
|
85
|
+
expect(match).not.toBe(null);
|
|
86
|
+
expect(match!.params).toEqual({});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("matches root route", () => {
|
|
90
|
+
createFile("page.tsx");
|
|
91
|
+
const routes = scanDevRoutes(appDir);
|
|
92
|
+
const match = matchDevRoute(routes, "/");
|
|
93
|
+
expect(match).not.toBe(null);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("extracts dynamic params", () => {
|
|
97
|
+
createFile("users/[id]/page.tsx");
|
|
98
|
+
const routes = scanDevRoutes(appDir);
|
|
99
|
+
const match = matchDevRoute(routes, "/users/42");
|
|
100
|
+
expect(match).not.toBe(null);
|
|
101
|
+
expect(match!.params).toEqual({ id: "42" });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns null for unmatched path", () => {
|
|
105
|
+
createFile("about/page.tsx");
|
|
106
|
+
const routes = scanDevRoutes(appDir);
|
|
107
|
+
const match = matchDevRoute(routes, "/nonexistent");
|
|
108
|
+
expect(match).toBe(null);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("decodes URI components in params", () => {
|
|
112
|
+
createFile("posts/[slug]/page.tsx");
|
|
113
|
+
const routes = scanDevRoutes(appDir);
|
|
114
|
+
const match = matchDevRoute(routes, "/posts/hello%20world");
|
|
115
|
+
expect(match).not.toBe(null);
|
|
116
|
+
expect(match!.params["slug"]).toBe("hello world");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("matches with optional trailing slash", () => {
|
|
120
|
+
createFile("about/page.tsx");
|
|
121
|
+
const routes = scanDevRoutes(appDir);
|
|
122
|
+
const match = matchDevRoute(routes, "/about/");
|
|
123
|
+
expect(match).not.toBe(null);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── findLayoutFiles ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("findLayoutFiles", () => {
|
|
130
|
+
it("finds root layout", () => {
|
|
131
|
+
createFile("layout.tsx");
|
|
132
|
+
createFile("page.tsx");
|
|
133
|
+
const layouts = findLayoutFiles(join(appDir, "page.tsx"), appDir);
|
|
134
|
+
expect(layouts).toHaveLength(1);
|
|
135
|
+
expect(layouts[0]).toContain("layout.tsx");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("finds nested layouts ordered outermost first", () => {
|
|
139
|
+
createFile("layout.tsx");
|
|
140
|
+
createFile("dashboard/layout.tsx");
|
|
141
|
+
createFile("dashboard/settings/page.tsx");
|
|
142
|
+
const layouts = findLayoutFiles(join(appDir, "dashboard/settings/page.tsx"), appDir);
|
|
143
|
+
expect(layouts).toHaveLength(2);
|
|
144
|
+
// Root layout should come first
|
|
145
|
+
expect(layouts[0]).toContain(join(appDir, "layout.tsx"));
|
|
146
|
+
expect(layouts[1]).toContain(join(appDir, "dashboard/layout.tsx"));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns empty when no layouts exist", () => {
|
|
150
|
+
createFile("about/page.tsx");
|
|
151
|
+
const layouts = findLayoutFiles(join(appDir, "about/page.tsx"), appDir);
|
|
152
|
+
expect(layouts).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ─── findErrorFile ────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe("findErrorFile", () => {
|
|
159
|
+
it("finds nearest error.tsx", () => {
|
|
160
|
+
createFile("error.tsx");
|
|
161
|
+
createFile("dashboard/page.tsx");
|
|
162
|
+
const errorFile = findErrorFile(join(appDir, "dashboard/page.tsx"), appDir);
|
|
163
|
+
expect(errorFile).not.toBe(null);
|
|
164
|
+
expect(errorFile).toContain("error.tsx");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns null when no error file exists", () => {
|
|
168
|
+
createFile("page.tsx");
|
|
169
|
+
const errorFile = findErrorFile(join(appDir, "page.tsx"), appDir);
|
|
170
|
+
expect(errorFile).toBe(null);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("prefers the innermost error.tsx", () => {
|
|
174
|
+
createFile("error.tsx");
|
|
175
|
+
createFile("dashboard/error.tsx");
|
|
176
|
+
createFile("dashboard/settings/page.tsx");
|
|
177
|
+
const errorFile = findErrorFile(join(appDir, "dashboard/settings/page.tsx"), appDir);
|
|
178
|
+
expect(errorFile).toContain(join("dashboard", "error.tsx"));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── findLoadingFile ──────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("findLoadingFile", () => {
|
|
185
|
+
it("finds nearest loading.tsx", () => {
|
|
186
|
+
createFile("loading.tsx");
|
|
187
|
+
createFile("page.tsx");
|
|
188
|
+
const loadingFile = findLoadingFile(join(appDir, "page.tsx"), appDir);
|
|
189
|
+
expect(loadingFile).not.toBe(null);
|
|
190
|
+
expect(loadingFile).toContain("loading.tsx");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns null when no loading file exists", () => {
|
|
194
|
+
createFile("page.tsx");
|
|
195
|
+
const loadingFile = findLoadingFile(join(appDir, "page.tsx"), appDir);
|
|
196
|
+
expect(loadingFile).toBe(null);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ─── API routes ───────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
describe("scanDevApiRoutes", () => {
|
|
203
|
+
it("finds route.ts files", () => {
|
|
204
|
+
createFile("api/health/route.ts");
|
|
205
|
+
const routes = scanDevApiRoutes(appDir);
|
|
206
|
+
expect(routes).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns empty when no route files", () => {
|
|
210
|
+
createFile("page.tsx");
|
|
211
|
+
const routes = scanDevApiRoutes(appDir);
|
|
212
|
+
expect(routes).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("matchDevApiRoute", () => {
|
|
217
|
+
it("matches static API route", () => {
|
|
218
|
+
createFile("api/health/route.ts");
|
|
219
|
+
const routes = scanDevApiRoutes(appDir);
|
|
220
|
+
const match = matchDevApiRoute(routes, "/api/health");
|
|
221
|
+
expect(match).not.toBe(null);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("returns null for unmatched API path", () => {
|
|
225
|
+
createFile("api/health/route.ts");
|
|
226
|
+
const routes = scanDevApiRoutes(appDir);
|
|
227
|
+
const match = matchDevApiRoute(routes, "/api/nonexistent");
|
|
228
|
+
expect(match).toBe(null);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface DevRoute {
|
|
5
|
+
/** Regex that matches the URL pathname for this route. */
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
/** Ordered list of param names, matching regex capture groups. */
|
|
8
|
+
paramNames: string[];
|
|
9
|
+
/** Absolute path to the page module on disk. */
|
|
10
|
+
file: string;
|
|
11
|
+
/** Whether SSR is enabled (opt-out via `export const ssr = false`). */
|
|
12
|
+
ssr: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan `appDir` (the project's `app/` directory) for `page.tsx` / `page.ts`
|
|
17
|
+
* files and build a list of matchable routes for the dev server middleware.
|
|
18
|
+
*
|
|
19
|
+
* Only TypeScript files are considered — Alab is TypeScript-only.
|
|
20
|
+
*/
|
|
21
|
+
export function scanDevRoutes(appDir: string): DevRoute[] {
|
|
22
|
+
const routes: DevRoute[] = [];
|
|
23
|
+
collectRoutes(appDir, appDir, routes);
|
|
24
|
+
// Sort so that static segments beat dynamic ones (/users/new before /users/[id]).
|
|
25
|
+
routes.sort((a, b) => {
|
|
26
|
+
const aScore = paramScore(a.paramNames.length);
|
|
27
|
+
const bScore = paramScore(b.paramNames.length);
|
|
28
|
+
return aScore - bScore;
|
|
29
|
+
});
|
|
30
|
+
return routes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectRoutes(appDir: string, dir: string, out: DevRoute[]): void {
|
|
34
|
+
let entries: string[];
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(dir);
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const full = join(dir, entry);
|
|
43
|
+
const stat = statSync(full);
|
|
44
|
+
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
collectRoutes(appDir, full, out);
|
|
47
|
+
} else if (entry === "page.tsx" || entry === "page.ts") {
|
|
48
|
+
const rel = relative(appDir, full);
|
|
49
|
+
const urlPath = relFileToUrlPath(rel);
|
|
50
|
+
const { pattern, paramNames } = urlPathToRegex(urlPath);
|
|
51
|
+
out.push({ pattern, paramNames, file: full, ssr: false });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert a relative file path (from appDir) to a URL path pattern.
|
|
58
|
+
*
|
|
59
|
+
* Examples:
|
|
60
|
+
* page.tsx → /
|
|
61
|
+
* about/page.tsx → /about
|
|
62
|
+
* users/[id]/page.tsx → /users/[id]
|
|
63
|
+
* users/[id]/posts/page.tsx → /users/[id]/posts
|
|
64
|
+
*/
|
|
65
|
+
function relFileToUrlPath(rel: string): string {
|
|
66
|
+
// Normalise Windows separators
|
|
67
|
+
const parts = rel.split(sep).join("/").split("/");
|
|
68
|
+
// Remove the trailing `page.tsx` / `page.ts`
|
|
69
|
+
parts.pop();
|
|
70
|
+
if (parts.length === 0) return "/";
|
|
71
|
+
return "/" + parts.join("/");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert an Alab URL path pattern to a regex and extract param names.
|
|
76
|
+
*
|
|
77
|
+
* `[id]` → capture group `([^/]+)`, param name `id`
|
|
78
|
+
*/
|
|
79
|
+
function urlPathToRegex(urlPath: string): { pattern: RegExp; paramNames: string[] } {
|
|
80
|
+
const paramNames: string[] = [];
|
|
81
|
+
const regexStr = urlPath
|
|
82
|
+
.split("/")
|
|
83
|
+
.map((segment) => {
|
|
84
|
+
const match = /^\[(.+)\]$/.exec(segment);
|
|
85
|
+
if (match) {
|
|
86
|
+
paramNames.push(match[1]!);
|
|
87
|
+
return "([^/]+)";
|
|
88
|
+
}
|
|
89
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90
|
+
})
|
|
91
|
+
.join("/");
|
|
92
|
+
|
|
93
|
+
// Match exact path (ignoring trailing slash for non-root paths)
|
|
94
|
+
const pattern = urlPath === "/" ? /^\/$/ : new RegExp(`^${regexStr}\\/?$`);
|
|
95
|
+
return { pattern, paramNames };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Lower score = higher priority (static routes beat dynamic ones). */
|
|
99
|
+
function paramScore(paramCount: number): number {
|
|
100
|
+
return paramCount;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find all layout.tsx files that apply to a given page file, ordered outermost → innermost.
|
|
105
|
+
*
|
|
106
|
+
* Given `app/dashboard/users/page.tsx`, returns:
|
|
107
|
+
* [ "app/layout.tsx", "app/dashboard/layout.tsx" ] (only those that exist)
|
|
108
|
+
*/
|
|
109
|
+
export function findLayoutFiles(pageFile: string, appDir: string): string[] {
|
|
110
|
+
const layouts: string[] = [];
|
|
111
|
+
let dir = dirname(pageFile);
|
|
112
|
+
|
|
113
|
+
// Collect directories from page dir up to (and including) appDir
|
|
114
|
+
const dirs: string[] = [];
|
|
115
|
+
while (dir.length >= appDir.length) {
|
|
116
|
+
dirs.unshift(dir); // prepend so we get outermost first
|
|
117
|
+
if (dir === appDir) break;
|
|
118
|
+
dir = dirname(dir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const d of dirs) {
|
|
122
|
+
const candidate = join(d, "layout.tsx");
|
|
123
|
+
if (existsSync(candidate)) layouts.push(candidate);
|
|
124
|
+
}
|
|
125
|
+
return layouts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Find the nearest error.tsx file for a given page file, searching innermost → outermost.
|
|
130
|
+
* Returns absolute path or null if none found.
|
|
131
|
+
*/
|
|
132
|
+
export function findErrorFile(pageFile: string, appDir: string): string | null {
|
|
133
|
+
let dir = dirname(pageFile);
|
|
134
|
+
while (dir.length >= appDir.length) {
|
|
135
|
+
const candidate = join(dir, "error.tsx");
|
|
136
|
+
if (existsSync(candidate)) return candidate;
|
|
137
|
+
if (dir === appDir) break;
|
|
138
|
+
dir = dirname(dir);
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Find the nearest loading.tsx file for a given page file, searching innermost → outermost.
|
|
145
|
+
* Returns absolute path or null if none found.
|
|
146
|
+
*/
|
|
147
|
+
export function findLoadingFile(pageFile: string, appDir: string): string | null {
|
|
148
|
+
let dir = dirname(pageFile);
|
|
149
|
+
while (dir.length >= appDir.length) {
|
|
150
|
+
const candidate = join(dir, "loading.tsx");
|
|
151
|
+
if (existsSync(candidate)) return candidate;
|
|
152
|
+
if (dir === appDir) break;
|
|
153
|
+
dir = dirname(dir);
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface DevApiRoute {
|
|
159
|
+
pattern: RegExp;
|
|
160
|
+
paramNames: string[];
|
|
161
|
+
file: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Scan `appDir` for `route.ts` / `route.tsx` files and build a list of API routes.
|
|
166
|
+
* These handle HTTP method exports: GET, POST, PUT, PATCH, DELETE.
|
|
167
|
+
*/
|
|
168
|
+
export function scanDevApiRoutes(appDir: string): DevApiRoute[] {
|
|
169
|
+
const routes: DevApiRoute[] = [];
|
|
170
|
+
collectApiRoutes(appDir, appDir, routes);
|
|
171
|
+
routes.sort((a, b) => a.paramNames.length - b.paramNames.length);
|
|
172
|
+
return routes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function collectApiRoutes(appDir: string, dir: string, out: DevApiRoute[]): void {
|
|
176
|
+
let entries: string[];
|
|
177
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const full = join(dir, entry);
|
|
180
|
+
const stat = statSync(full);
|
|
181
|
+
if (stat.isDirectory()) {
|
|
182
|
+
collectApiRoutes(appDir, full, out);
|
|
183
|
+
} else if (entry === "route.ts" || entry === "route.tsx") {
|
|
184
|
+
const rel = relative(appDir, full);
|
|
185
|
+
const urlPath = relFileToUrlPath(rel.replace(/route\.(tsx?)$/, "page.$1"));
|
|
186
|
+
const { pattern, paramNames } = urlPathToRegex(urlPath);
|
|
187
|
+
out.push({ pattern, paramNames, file: full });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Match a pathname against the API route list.
|
|
194
|
+
*/
|
|
195
|
+
export function matchDevApiRoute(
|
|
196
|
+
routes: DevApiRoute[],
|
|
197
|
+
pathname: string,
|
|
198
|
+
): { route: DevApiRoute; params: Record<string, string> } | null {
|
|
199
|
+
for (const route of routes) {
|
|
200
|
+
const m = route.pattern.exec(pathname);
|
|
201
|
+
if (m) {
|
|
202
|
+
const params: Record<string, string> = {};
|
|
203
|
+
route.paramNames.forEach((name, i) => { params[name] = decodeURIComponent(m[i + 1] ?? ""); });
|
|
204
|
+
return { route, params };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Match an incoming URL pathname against the dev route list.
|
|
212
|
+
* Returns the matched route and extracted params, or `null` if no match.
|
|
213
|
+
*/
|
|
214
|
+
export function matchDevRoute(
|
|
215
|
+
routes: DevRoute[],
|
|
216
|
+
pathname: string,
|
|
217
|
+
): { route: DevRoute; params: Record<string, string> } | null {
|
|
218
|
+
for (const route of routes) {
|
|
219
|
+
const m = route.pattern.exec(pathname);
|
|
220
|
+
if (m) {
|
|
221
|
+
const params: Record<string, string> = {};
|
|
222
|
+
route.paramNames.forEach((name, i) => {
|
|
223
|
+
params[name] = decodeURIComponent(m[i + 1] ?? "");
|
|
224
|
+
});
|
|
225
|
+
return { route, params };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alab test utilities — zero setup required.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { renderPage, mockServerFn } from "alabjs/test";
|
|
7
|
+
* import { getUser } from "./app/users/[id]/page.server.js";
|
|
8
|
+
*
|
|
9
|
+
* mockServerFn(getUser, { id: "1", name: "Alice" });
|
|
10
|
+
* const { html, status } = await renderPage("/users/1");
|
|
11
|
+
* expect(html).toContain("Alice");
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createElement, type ComponentType } from "react";
|
|
16
|
+
import { renderToString } from "react-dom/server";
|
|
17
|
+
import type { ServerFn } from "../types/index.js";
|
|
18
|
+
|
|
19
|
+
// ─── mockServerFn ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Registry of mocked server functions — keyed by function name. */
|
|
22
|
+
const _mocks = new Map<string, unknown>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mock a `defineServerFn` function for the duration of a test.
|
|
26
|
+
*
|
|
27
|
+
* The mock is applied globally for the current test process. Use
|
|
28
|
+
* `clearMocks()` in `afterEach` if you need isolation between tests,
|
|
29
|
+
* or rely on Vitest's built-in mock isolation.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { getUser } from "./page.server.js";
|
|
34
|
+
* mockServerFn(getUser, { id: "1", name: "Alice", email: "alice@example.com" });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function mockServerFn<T extends ServerFn<any, any, any>>(
|
|
38
|
+
fn: T,
|
|
39
|
+
returnValue: T extends ServerFn<any, infer O, any> ? O : never,
|
|
40
|
+
): void {
|
|
41
|
+
_mocks.set((fn as unknown as { name: string }).name, returnValue);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mock a server function with a custom handler (for dynamic responses).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* mockServerFnWith(getUser, async ({ params }) => {
|
|
50
|
+
* if (params.id === "1") return { name: "Alice" };
|
|
51
|
+
* throw new Error("Not found");
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function mockServerFnWith<T extends ServerFn<any, any, any>>(
|
|
56
|
+
fn: T,
|
|
57
|
+
handler: T extends ServerFn<infer I, infer O, any>
|
|
58
|
+
? (ctx: { params: Record<string, string> }, input: I) => Promise<O>
|
|
59
|
+
: never,
|
|
60
|
+
): void {
|
|
61
|
+
_mocks.set((fn as unknown as { name: string }).name, handler);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remove all server function mocks. Call in `afterEach` for test isolation. */
|
|
65
|
+
export function clearMocks(): void {
|
|
66
|
+
_mocks.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @internal Used by Alab's dev server to resolve mocked functions in tests. */
|
|
70
|
+
export function _getMock(fnName: string): unknown | undefined {
|
|
71
|
+
return _mocks.get(fnName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── renderPage ───────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface RenderPageResult {
|
|
77
|
+
/** Full HTML string including the Alab shell. */
|
|
78
|
+
html: string;
|
|
79
|
+
/** HTTP status code (200, 404, 500, etc.). */
|
|
80
|
+
status: number;
|
|
81
|
+
/** Whether the render threw an error. */
|
|
82
|
+
error: Error | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RenderPageOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Params to inject — useful when the route path has dynamic segments.
|
|
88
|
+
* @example `{ id: "42" }` for `/users/[id]`
|
|
89
|
+
*/
|
|
90
|
+
params?: Record<string, string>;
|
|
91
|
+
/** Search params to inject. */
|
|
92
|
+
searchParams?: Record<string, string>;
|
|
93
|
+
/**
|
|
94
|
+
* Request headers forwarded to middleware and server functions.
|
|
95
|
+
*/
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render a page component to an HTML string for integration testing.
|
|
101
|
+
*
|
|
102
|
+
* Automatically applies any `mockServerFn` mocks registered before this call.
|
|
103
|
+
* Does not start an HTTP server — renders entirely in-process.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const { html, status } = await renderPage("/users/1", { params: { id: "1" } });
|
|
108
|
+
* expect(status).toBe(200);
|
|
109
|
+
* expect(html).toContain('<h1>Alice</h1>');
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export async function renderPage(
|
|
113
|
+
path: string,
|
|
114
|
+
options: RenderPageOptions = {},
|
|
115
|
+
): Promise<RenderPageResult> {
|
|
116
|
+
const { params = {}, searchParams = {} } = options;
|
|
117
|
+
|
|
118
|
+
// Dynamically resolve the page module from the file-system.
|
|
119
|
+
// In test environments, Vitest handles module resolution via the Vite plugin.
|
|
120
|
+
const appDir = new URL("../../app", import.meta.url).pathname;
|
|
121
|
+
|
|
122
|
+
// Convert URL path to a candidate file path.
|
|
123
|
+
// e.g. "/users/1" → try "app/users/[id]/page.tsx" based on path segments.
|
|
124
|
+
const segments = path.split("/").filter(Boolean);
|
|
125
|
+
const candidates = buildCandidatePaths(appDir, segments, params);
|
|
126
|
+
|
|
127
|
+
let PageComponent: ComponentType<{ params: Record<string, string>; searchParams: Record<string, string> }> | null = null;
|
|
128
|
+
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
try {
|
|
131
|
+
const mod = await import(candidate) as { default?: unknown };
|
|
132
|
+
if (typeof mod.default === "function") {
|
|
133
|
+
PageComponent = mod.default as unknown as typeof PageComponent;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Try next candidate.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!PageComponent) {
|
|
142
|
+
return { html: "", status: 404, error: new Error(`[alabjs/test] No page found for path: ${path}`) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const html = renderToString(createElement(PageComponent, { params, searchParams }));
|
|
147
|
+
return { html, status: 200, error: null };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
html: "",
|
|
151
|
+
status: 500,
|
|
152
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render a specific component to HTML (lower-level than `renderPage`).
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* import UserPage from "./app/users/[id]/page.js";
|
|
163
|
+
* const { html } = await renderComponent(UserPage, { params: { id: "1" }, searchParams: {} });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export async function renderComponent<P extends Record<string, unknown>>(
|
|
167
|
+
Component: ComponentType<P>,
|
|
168
|
+
props: P,
|
|
169
|
+
): Promise<RenderPageResult> {
|
|
170
|
+
try {
|
|
171
|
+
const html = renderToString(createElement(Component, props));
|
|
172
|
+
return { html, status: 200, error: null };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
html: "",
|
|
176
|
+
status: 500,
|
|
177
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function buildCandidatePaths(
|
|
185
|
+
appDir: string,
|
|
186
|
+
segments: string[],
|
|
187
|
+
params: Record<string, string>,
|
|
188
|
+
): string[] {
|
|
189
|
+
const paramValues = Object.values(params);
|
|
190
|
+
const candidates: string[] = [];
|
|
191
|
+
|
|
192
|
+
// Try resolving dynamic segments in reverse (known params replace segments).
|
|
193
|
+
const resolvedSegments = segments.map((seg) => {
|
|
194
|
+
// If this segment matches a known param value, try both [param] and the value.
|
|
195
|
+
const matchingParam = paramValues.find((v) => v === seg);
|
|
196
|
+
return matchingParam ? `[${Object.keys(params).find((k) => params[k] === matchingParam) ?? seg}]` : seg;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const extensions = [".tsx", ".ts", ".jsx", ".js"];
|
|
200
|
+
for (const ext of extensions) {
|
|
201
|
+
candidates.push(`${appDir}/${resolvedSegments.join("/")}/page${ext}`);
|
|
202
|
+
candidates.push(`${appDir}/${segments.join("/")}/page${ext}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return candidates;
|
|
206
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declaration for the @alabjs/compiler native binding.
|
|
3
|
+
* The actual module is a napi-rs .node binary loaded at runtime.
|
|
4
|
+
* It is an optional dependency — the framework falls back to esbuild if absent.
|
|
5
|
+
*/
|
|
6
|
+
declare module "@alabjs/compiler" {
|
|
7
|
+
/** Compile a TypeScript/TSX source string to JavaScript. Returns JSON `{ code, map }`. */
|
|
8
|
+
export function compileSource(source: string, filename: string, minify: boolean): string;
|
|
9
|
+
/** Check a source file for server-boundary violations. Returns JSON array. */
|
|
10
|
+
export function checkBoundary(source: string, filename: string): string;
|
|
11
|
+
/** Scan an app/ directory and build the route manifest. Returns JSON `{ routes }`. */
|
|
12
|
+
export function buildRoutes(appDir: string): string;
|
|
13
|
+
/** Optimise an image buffer — decode, resize, and encode to WebP/JPEG/PNG. */
|
|
14
|
+
export function optimizeImage(
|
|
15
|
+
input: Buffer,
|
|
16
|
+
quality?: number | null,
|
|
17
|
+
width?: number | null,
|
|
18
|
+
height?: number | null,
|
|
19
|
+
format?: string | null,
|
|
20
|
+
): Promise<Buffer>;
|
|
21
|
+
/** Scan a `.server.ts` source for `defineServerFn` exports. Returns JSON `Array<{ name, endpoint }>`. */
|
|
22
|
+
export function extractServerFns(source: string, filename: string): string;
|
|
23
|
+
/** Return an ES module fetch-stub for a server function (used in client bundles). */
|
|
24
|
+
export function serverFnStub(name: string, endpoint: string): string;
|
|
25
|
+
}
|