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,509 @@
|
|
|
1
|
+
import { createServer } from "vite";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
import { Writable } from "node:stream";
|
|
5
|
+
import type { IncomingMessage } from "node:http";
|
|
6
|
+
import {
|
|
7
|
+
scanDevRoutes, matchDevRoute,
|
|
8
|
+
findLayoutFiles, findErrorFile, findLoadingFile,
|
|
9
|
+
scanDevApiRoutes, matchDevApiRoute,
|
|
10
|
+
} from "../ssr/router-dev.js";
|
|
11
|
+
import { htmlShellBefore, htmlShellAfter } from "../ssr/html.js";
|
|
12
|
+
import { generateSitemap } from "../server/sitemap.js";
|
|
13
|
+
import { handleImageRequest } from "../server/image.js";
|
|
14
|
+
import type { MiddlewareModule } from "../server/middleware.js";
|
|
15
|
+
import { runMiddleware } from "../server/middleware.js";
|
|
16
|
+
import {
|
|
17
|
+
getCachedPage, setCachedPage, markPageRevalidating, isPageRevalidating,
|
|
18
|
+
} from "../server/cache.js";
|
|
19
|
+
import { checkRevalidateAuth, applyRevalidate } from "../server/revalidate.js";
|
|
20
|
+
import type { PageMetadata } from "../types/index.js";
|
|
21
|
+
import type { Route } from "../router/manifest.js";
|
|
22
|
+
|
|
23
|
+
interface DevOptions {
|
|
24
|
+
cwd: string;
|
|
25
|
+
port?: number;
|
|
26
|
+
host?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Recursively find all *.server.ts / *.server.tsx files under a directory. */
|
|
30
|
+
function findServerFiles(dir: string): string[] {
|
|
31
|
+
const results: string[] = [];
|
|
32
|
+
try {
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full = join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
results.push(...findServerFiles(full));
|
|
37
|
+
} else if (/\.server\.(ts|tsx)$/.test(entry.name)) {
|
|
38
|
+
results.push(full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
43
|
+
if (code !== "ENOENT") {
|
|
44
|
+
console.warn(`[alabjs] warning: failed to scan ${dir}:`, (err as Error).message ?? err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read and JSON-parse the request body. Returns undefined on empty or invalid JSON. */
|
|
51
|
+
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const chunks: Buffer[] = [];
|
|
54
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
55
|
+
req.on("end", () => {
|
|
56
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
|
|
57
|
+
catch { resolve(undefined); }
|
|
58
|
+
});
|
|
59
|
+
req.on("error", () => resolve(undefined));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function dev({ cwd, port = 3000, host = "localhost" }: DevOptions) {
|
|
64
|
+
console.log(" alab starting dev server...\n");
|
|
65
|
+
|
|
66
|
+
const appDir = resolve(cwd, "app");
|
|
67
|
+
|
|
68
|
+
const vite = await createServer({
|
|
69
|
+
root: cwd,
|
|
70
|
+
appType: "custom",
|
|
71
|
+
server: { port, host },
|
|
72
|
+
plugins: [
|
|
73
|
+
(await import("alabjs-vite-plugin")).alabPlugin(),
|
|
74
|
+
],
|
|
75
|
+
ssr: {
|
|
76
|
+
// Externalize react packages so Node.js loads them natively (avoids
|
|
77
|
+
// CJS/ESM mismatch in Vite's module runner for react-dom/server).
|
|
78
|
+
external: ["react", "react-dom", "react-dom/server", "react-dom/server.node"],
|
|
79
|
+
noExternal: ["alab", "alabjs-vite-plugin"],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── Alab SSR + built-in route middleware ────────────────────────────────────
|
|
84
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
85
|
+
const rawUrl = req.url ?? "/";
|
|
86
|
+
const pathname = rawUrl.split("?")[0] ?? "/";
|
|
87
|
+
|
|
88
|
+
// Pass Vite-internal requests through
|
|
89
|
+
if (
|
|
90
|
+
pathname.startsWith("/@") ||
|
|
91
|
+
pathname.startsWith("/__vite") ||
|
|
92
|
+
pathname.startsWith("/node_modules") ||
|
|
93
|
+
pathname.startsWith("/@alab")
|
|
94
|
+
) {
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply security headers to every alab-handled response.
|
|
99
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
100
|
+
res.setHeader("x-frame-options", "SAMEORIGIN");
|
|
101
|
+
res.setHeader("referrer-policy", "strict-origin-when-cross-origin");
|
|
102
|
+
res.setHeader("permissions-policy", "camera=(), microphone=(), geolocation=()");
|
|
103
|
+
res.setHeader("x-permitted-cross-domain-policies", "none");
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// ── User middleware (middleware.ts at project root) ───────────────────────
|
|
107
|
+
const middlewareFile = resolve(cwd, "middleware.ts");
|
|
108
|
+
const { existsSync: fsExists } = await import("node:fs");
|
|
109
|
+
if (fsExists(middlewareFile)) {
|
|
110
|
+
const middlewareMod = await vite.ssrLoadModule(middlewareFile) as MiddlewareModule;
|
|
111
|
+
if (typeof middlewareMod.middleware === "function") {
|
|
112
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
113
|
+
const webReq = new Request(url.toString(), {
|
|
114
|
+
method: req.method ?? "GET",
|
|
115
|
+
headers: req.headers as HeadersInit,
|
|
116
|
+
});
|
|
117
|
+
const middlewareRes = await runMiddleware(middlewareMod, webReq);
|
|
118
|
+
if (middlewareRes) {
|
|
119
|
+
res.statusCode = middlewareRes.status;
|
|
120
|
+
middlewareRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
121
|
+
res.end(Buffer.from(await middlewareRes.arrayBuffer()));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── /_alabjs/data/:fnName — GET data from a defineServerFn (useServerData) ─
|
|
128
|
+
// ── /_alabjs/fn/:fnName — POST mutation via defineServerFn stub ───────────
|
|
129
|
+
if (pathname.startsWith("/_alabjs/data/") || pathname.startsWith("/_alabjs/fn/")) {
|
|
130
|
+
const fnName = pathname.startsWith("/_alabjs/data/")
|
|
131
|
+
? pathname.slice("/_alabjs/data/".length)
|
|
132
|
+
: pathname.slice("/_alabjs/fn/".length);
|
|
133
|
+
|
|
134
|
+
const serverFiles = findServerFiles(appDir);
|
|
135
|
+
let found = false;
|
|
136
|
+
for (const file of serverFiles) {
|
|
137
|
+
const mod = await vite.ssrLoadModule(file);
|
|
138
|
+
if (typeof mod[fnName] === "function") {
|
|
139
|
+
found = true;
|
|
140
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
141
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
142
|
+
const input = req.method === "POST" ? await readJsonBody(req) : undefined;
|
|
143
|
+
const ctx = {
|
|
144
|
+
params,
|
|
145
|
+
query: params,
|
|
146
|
+
headers: req.headers as Record<string, string>,
|
|
147
|
+
method: (req.method ?? "GET").toUpperCase() as "GET" | "POST",
|
|
148
|
+
url: rawUrl,
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
const result = await (mod[fnName] as (c: unknown, i: unknown) => Promise<unknown>)(ctx, input);
|
|
152
|
+
res.statusCode = 200;
|
|
153
|
+
res.setHeader("content-type", "application/json");
|
|
154
|
+
res.end(JSON.stringify(result));
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Zod validation errors from defineServerFn get HTTP 422
|
|
157
|
+
const zodError = (err as Record<string, unknown>)?.["zodError"];
|
|
158
|
+
if (zodError) {
|
|
159
|
+
res.statusCode = 422;
|
|
160
|
+
res.setHeader("content-type", "application/json");
|
|
161
|
+
res.end(JSON.stringify({ zodError }));
|
|
162
|
+
} else {
|
|
163
|
+
res.statusCode = 500;
|
|
164
|
+
res.setHeader("content-type", "application/json");
|
|
165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
166
|
+
res.end(JSON.stringify({ error: msg }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!found) {
|
|
173
|
+
res.statusCode = 404;
|
|
174
|
+
res.setHeader("content-type", "application/json");
|
|
175
|
+
res.end(JSON.stringify({ error: `[alabjs] server function not found: ${fnName}` }));
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── /_alabjs/image — Rust-powered image optimisation ───────────────────────
|
|
181
|
+
if (pathname === "/_alabjs/image") {
|
|
182
|
+
const publicDir = resolve(cwd, "public");
|
|
183
|
+
await handleImageRequest(req, res, publicDir);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── /sitemap.xml ────────────────────────────────────────────────────────
|
|
188
|
+
if (pathname === "/sitemap.xml") {
|
|
189
|
+
const devRoutes = scanDevRoutes(appDir);
|
|
190
|
+
const manifestRoutes: Route[] = devRoutes.map((r) => ({
|
|
191
|
+
path: r.file
|
|
192
|
+
.replace(appDir, "")
|
|
193
|
+
.replace(/\/page\.(tsx|ts)$/, "") || "/",
|
|
194
|
+
file: r.file.replace(cwd + "/", ""),
|
|
195
|
+
kind: "page" as const,
|
|
196
|
+
ssr: r.ssr,
|
|
197
|
+
params: r.paramNames,
|
|
198
|
+
}));
|
|
199
|
+
const xml = generateSitemap(manifestRoutes, `http://${host}:${port}`);
|
|
200
|
+
res.statusCode = 200;
|
|
201
|
+
res.setHeader("content-type", "application/xml; charset=utf-8");
|
|
202
|
+
res.end(xml);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── On-demand ISR revalidation ────────────────────────────────────────────
|
|
207
|
+
if (pathname === "/_alabjs/revalidate") {
|
|
208
|
+
if (req.method !== "POST") {
|
|
209
|
+
res.statusCode = 405;
|
|
210
|
+
res.setHeader("allow", "POST");
|
|
211
|
+
res.setHeader("content-type", "application/json");
|
|
212
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!checkRevalidateAuth(req.headers["authorization"])) {
|
|
216
|
+
res.statusCode = 401;
|
|
217
|
+
res.setHeader("content-type", "application/json");
|
|
218
|
+
res.end(JSON.stringify({ error: "Unauthorized. Set Authorization: Bearer <ALAB_REVALIDATE_SECRET>." }));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const chunks: Buffer[] = [];
|
|
222
|
+
await new Promise<void>((ok) => {
|
|
223
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
224
|
+
req.on("end", ok);
|
|
225
|
+
});
|
|
226
|
+
let body: unknown;
|
|
227
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString()); }
|
|
228
|
+
catch { body = null; }
|
|
229
|
+
const result = applyRevalidate(body);
|
|
230
|
+
res.statusCode = "error" in result ? result.status : 200;
|
|
231
|
+
res.setHeader("content-type", "application/json");
|
|
232
|
+
res.end(JSON.stringify("error" in result ? { error: result.error } : result));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── API routes (route.ts) ─────────────────────────────────────────────────
|
|
237
|
+
const apiRoutes = scanDevApiRoutes(appDir);
|
|
238
|
+
const matchedApi = matchDevApiRoute(apiRoutes, pathname);
|
|
239
|
+
if (matchedApi) {
|
|
240
|
+
const apiMod = await vite.ssrLoadModule(matchedApi.route.file) as Record<string, unknown>;
|
|
241
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
242
|
+
const handler = apiMod[method];
|
|
243
|
+
if (typeof handler !== "function") {
|
|
244
|
+
res.statusCode = 405;
|
|
245
|
+
res.setHeader("allow", Object.keys(apiMod).filter(k => /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/.test(k)).join(", "));
|
|
246
|
+
res.end("Method Not Allowed");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
250
|
+
const chunks: Buffer[] = [];
|
|
251
|
+
await new Promise<void>((ok) => {
|
|
252
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
253
|
+
req.on("end", ok);
|
|
254
|
+
});
|
|
255
|
+
const body = chunks.length ? Buffer.concat(chunks) : null;
|
|
256
|
+
const webReq = new Request(url.toString(), {
|
|
257
|
+
method,
|
|
258
|
+
headers: req.headers as HeadersInit,
|
|
259
|
+
body: body?.length ? body : null,
|
|
260
|
+
});
|
|
261
|
+
const webRes = await (handler as (r: Request) => Promise<Response>)(webReq);
|
|
262
|
+
res.statusCode = webRes.status;
|
|
263
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
264
|
+
|
|
265
|
+
// SSE: pipe the ReadableStream body without buffering.
|
|
266
|
+
if (
|
|
267
|
+
(webRes.headers.get("content-type") ?? "").startsWith("text/event-stream") &&
|
|
268
|
+
webRes.body
|
|
269
|
+
) {
|
|
270
|
+
const reader = webRes.body.getReader();
|
|
271
|
+
res.on("close", () => { void reader.cancel(); });
|
|
272
|
+
try {
|
|
273
|
+
while (true) {
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
if (done || res.destroyed) break;
|
|
276
|
+
res.write(value);
|
|
277
|
+
}
|
|
278
|
+
} catch { /* client disconnected */ } finally {
|
|
279
|
+
res.end();
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
res.end(Buffer.from(await webRes.arrayBuffer()));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Page routes ──────────────────────────────────────────────────────────
|
|
289
|
+
const routes = scanDevRoutes(appDir);
|
|
290
|
+
const matched = matchDevRoute(routes, pathname);
|
|
291
|
+
|
|
292
|
+
// ── Not-found page ────────────────────────────────────────────────────────
|
|
293
|
+
if (!matched) {
|
|
294
|
+
const wantsHtml = (req.headers["accept"] ?? "").includes("text/html");
|
|
295
|
+
if (!wantsHtml) return next();
|
|
296
|
+
|
|
297
|
+
const notFoundFile = resolve(appDir, "not-found.tsx");
|
|
298
|
+
const { existsSync } = await import("node:fs");
|
|
299
|
+
if (existsSync(notFoundFile)) {
|
|
300
|
+
const nfMod = await vite.ssrLoadModule(notFoundFile) as Record<string, unknown>;
|
|
301
|
+
const NotFound = nfMod["default"];
|
|
302
|
+
if (typeof NotFound === "function") {
|
|
303
|
+
const { createElement } = await import("react") as { createElement: (t: unknown, p: unknown) => unknown };
|
|
304
|
+
const { renderToPipeableStream } = await import("react-dom/server.node") as {
|
|
305
|
+
renderToPipeableStream: (el: unknown, opts: { onAllReady: () => void; onError: (e: unknown) => void }) => { pipe: (d: Writable) => void };
|
|
306
|
+
};
|
|
307
|
+
const nfContent = await new Promise<string>((ok, fail) => {
|
|
308
|
+
let html = "";
|
|
309
|
+
const sink = new Writable({
|
|
310
|
+
write(chunk: Buffer, _enc: string, cb: () => void) { html += chunk.toString(); cb(); },
|
|
311
|
+
});
|
|
312
|
+
sink.on("finish", () => ok(html));
|
|
313
|
+
const { pipe } = renderToPipeableStream(createElement(NotFound, {}), {
|
|
314
|
+
onAllReady() { pipe(sink); },
|
|
315
|
+
onError(e) { fail(e); },
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
const shell = htmlShellBefore({ metadata: { title: "404 — Not Found" }, paramsJson: "{}", searchParamsJson: "{}", routeFile: "app/not-found.tsx", ssr: true });
|
|
319
|
+
const rawHtml = `${shell}${nfContent}${htmlShellAfter({})}`;
|
|
320
|
+
const html = await vite.transformIndexHtml(pathname, rawHtml);
|
|
321
|
+
res.statusCode = 404;
|
|
322
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
323
|
+
res.end(html);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return next();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { route, params } = matched;
|
|
331
|
+
|
|
332
|
+
const mod = await vite.ssrLoadModule(route.file) as {
|
|
333
|
+
default?: unknown;
|
|
334
|
+
metadata?: PageMetadata;
|
|
335
|
+
generateMetadata?: (params: Record<string, string>) => PageMetadata | Promise<PageMetadata>;
|
|
336
|
+
ssr?: boolean;
|
|
337
|
+
/** ISR: seconds before a cached page is considered stale. Omit to disable caching. */
|
|
338
|
+
revalidate?: number;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const Page = mod.default;
|
|
342
|
+
if (typeof Page !== "function") {
|
|
343
|
+
vite.ssrFixStacktrace(new Error(`[alabjs] Page module has no default export: ${route.file}`));
|
|
344
|
+
return next();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Support both static `export const metadata` and dynamic `export async function generateMetadata`.
|
|
348
|
+
const metadata: PageMetadata =
|
|
349
|
+
typeof mod.generateMetadata === "function"
|
|
350
|
+
? await mod.generateMetadata(params)
|
|
351
|
+
: (mod.metadata ?? {});
|
|
352
|
+
|
|
353
|
+
// Make the server's base URL available to useServerData during SSR,
|
|
354
|
+
// so it can construct an absolute URL for its internal fetch call.
|
|
355
|
+
process.env["ALAB_ORIGIN"] = `http://${host}:${port}`;
|
|
356
|
+
|
|
357
|
+
const ssrEnabled = mod.ssr === true;
|
|
358
|
+
|
|
359
|
+
const searchParams = Object.fromEntries(
|
|
360
|
+
new URLSearchParams(rawUrl.includes("?") ? rawUrl.split("?")[1] : "").entries(),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// ── Layouts + loading file ────────────────────────────────────────────────
|
|
364
|
+
const layoutFiles = findLayoutFiles(route.file, appDir);
|
|
365
|
+
const layoutMods = await Promise.all(layoutFiles.map((f) => vite.ssrLoadModule(f)));
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367
|
+
const layoutComponents = layoutMods.map((m) => m["default"]).filter((c): c is any => typeof c === "function");
|
|
368
|
+
const layoutsJson = JSON.stringify(layoutFiles.map((f) => f.replace(cwd + "/", "")));
|
|
369
|
+
const loadingFileAbs = findLoadingFile(route.file, appDir);
|
|
370
|
+
const loadingFile = loadingFileAbs ? loadingFileAbs.replace(cwd + "/", "") : undefined;
|
|
371
|
+
|
|
372
|
+
const { renderToPipeableStream } = await import("react-dom/server.node") as {
|
|
373
|
+
renderToPipeableStream: (el: unknown, opts: {
|
|
374
|
+
onAllReady: () => void;
|
|
375
|
+
onError: (err: unknown) => void;
|
|
376
|
+
}) => { pipe: (dest: Writable) => void };
|
|
377
|
+
};
|
|
378
|
+
const { createElement } = await import("react") as {
|
|
379
|
+
createElement: (type: unknown, props: unknown, ...children: unknown[]) => unknown;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Clear SSR promise cache so each request gets fresh data but re-renders
|
|
383
|
+
// within the same renderToPipeableStream pass reuse the same promise.
|
|
384
|
+
const alabClient = await vite.ssrLoadModule("alabjs/client") as {
|
|
385
|
+
_clearALabSSRCache?: () => void;
|
|
386
|
+
};
|
|
387
|
+
alabClient._clearALabSSRCache?.();
|
|
388
|
+
|
|
389
|
+
// Build element tree: Page wrapped by layouts outermost→innermost
|
|
390
|
+
const buildTree = (PageComp: unknown): unknown => {
|
|
391
|
+
let el = createElement(PageComp, { params, searchParams });
|
|
392
|
+
for (let i = layoutComponents.length - 1; i >= 0; i--) {
|
|
393
|
+
el = createElement(layoutComponents[i], {}, el);
|
|
394
|
+
}
|
|
395
|
+
return el;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
let ssrContent = "";
|
|
399
|
+
if (ssrEnabled) {
|
|
400
|
+
try {
|
|
401
|
+
ssrContent = await new Promise<string>((ok, fail) => {
|
|
402
|
+
let html = "";
|
|
403
|
+
const sink = new Writable({
|
|
404
|
+
write(chunk: Buffer, _enc: string, cb: () => void) {
|
|
405
|
+
html += chunk.toString();
|
|
406
|
+
cb();
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
sink.on("finish", () => ok(html));
|
|
410
|
+
const { pipe } = renderToPipeableStream(buildTree(Page), {
|
|
411
|
+
onAllReady() { pipe(sink); },
|
|
412
|
+
onError(err) { fail(err); },
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
} catch (ssrErr) {
|
|
416
|
+
// ── error.tsx fallback ──────────────────────────────────────────────
|
|
417
|
+
const errorFile = findErrorFile(route.file, appDir);
|
|
418
|
+
if (errorFile) {
|
|
419
|
+
try {
|
|
420
|
+
const errorMod = await vite.ssrLoadModule(errorFile) as Record<string, unknown>;
|
|
421
|
+
const ErrorPage = errorMod["default"];
|
|
422
|
+
if (typeof ErrorPage === "function") {
|
|
423
|
+
ssrContent = await new Promise<string>((ok, fail) => {
|
|
424
|
+
let html = "";
|
|
425
|
+
const sink = new Writable({
|
|
426
|
+
write(chunk: Buffer, _enc: string, cb: () => void) { html += chunk.toString(); cb(); },
|
|
427
|
+
});
|
|
428
|
+
sink.on("finish", () => ok(html));
|
|
429
|
+
const { pipe } = renderToPipeableStream(
|
|
430
|
+
createElement(ErrorPage, { error: ssrErr, reset: () => {} }),
|
|
431
|
+
{ onAllReady() { pipe(sink); }, onError(e) { fail(e); } },
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
} catch (errorPageErr) {
|
|
436
|
+
console.error("[alabjs] error.tsx SSR render failed:", errorPageErr);
|
|
437
|
+
// fall through to plain text error
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!ssrContent) {
|
|
441
|
+
res.statusCode = 500;
|
|
442
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
443
|
+
res.end(`[alabjs] SSR error: ${String(ssrErr)}`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const routeFile = route.file.replace(cwd, "").replace(/^\//, "");
|
|
450
|
+
|
|
451
|
+
// ── Render helper (used for both fresh render + background revalidation) ─
|
|
452
|
+
const revalidateSecs = typeof mod.revalidate === "number" ? mod.revalidate : null;
|
|
453
|
+
const renderPageHtml = async (): Promise<string> => {
|
|
454
|
+
const shellBefore = htmlShellBefore({
|
|
455
|
+
metadata,
|
|
456
|
+
paramsJson: JSON.stringify(params),
|
|
457
|
+
searchParamsJson: JSON.stringify(searchParams),
|
|
458
|
+
routeFile,
|
|
459
|
+
layoutsJson,
|
|
460
|
+
loadingFile,
|
|
461
|
+
ssr: ssrEnabled,
|
|
462
|
+
});
|
|
463
|
+
const shellAfter = htmlShellAfter({});
|
|
464
|
+
const rawHtml = `${shellBefore}${ssrContent}${shellAfter}`;
|
|
465
|
+
return vite.transformIndexHtml(pathname, rawHtml);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// ── ISR: serve cached page if available ──────────────────────────────────
|
|
469
|
+
if (revalidateSecs !== null) {
|
|
470
|
+
const cached = getCachedPage(pathname);
|
|
471
|
+
if (cached) {
|
|
472
|
+
res.statusCode = 200;
|
|
473
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
474
|
+
res.setHeader("x-alab-cache", cached.stale ? "stale" : "hit");
|
|
475
|
+
res.end(cached.html);
|
|
476
|
+
// Background revalidation for stale entries
|
|
477
|
+
if (cached.stale && !isPageRevalidating(pathname)) {
|
|
478
|
+
markPageRevalidating(pathname);
|
|
479
|
+
void renderPageHtml().then((fresh) => {
|
|
480
|
+
setCachedPage(pathname, fresh, revalidateSecs);
|
|
481
|
+
}).catch((revalErr: unknown) => {
|
|
482
|
+
console.warn(`[alabjs] ISR revalidation failed for ${pathname}:`, revalErr);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const html = await renderPageHtml();
|
|
490
|
+
|
|
491
|
+
// Store in ISR cache if page exports `revalidate`
|
|
492
|
+
if (revalidateSecs !== null) {
|
|
493
|
+
setCachedPage(pathname, html, revalidateSecs);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
res.statusCode = 200;
|
|
497
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
498
|
+
if (revalidateSecs !== null) res.setHeader("x-alab-cache", "miss");
|
|
499
|
+
res.end(html);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error(`[alabjs] unhandled error on ${pathname}:`, err);
|
|
502
|
+
vite.ssrFixStacktrace(err as Error);
|
|
503
|
+
next(err);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
await vite.listen();
|
|
508
|
+
vite.printUrls();
|
|
509
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import type { AlabNapi } from "../types/napi.js";
|
|
4
|
+
|
|
5
|
+
interface InfoOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Recursively find all files in a directory matching a predicate. */
|
|
10
|
+
async function findFiles(dir: string, match: (name: string) => boolean): Promise<string[]> {
|
|
11
|
+
const results: string[] = [];
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
15
|
+
} catch {
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const full = resolve(dir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
results.push(...(await findFiles(full, match)));
|
|
22
|
+
} else if (match(entry.name)) {
|
|
23
|
+
results.push(full);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function info({ cwd }: InfoOptions) {
|
|
30
|
+
let napi: AlabNapi;
|
|
31
|
+
try {
|
|
32
|
+
napi = (await import("@alabjs/compiler")) as AlabNapi;
|
|
33
|
+
} catch {
|
|
34
|
+
console.error(
|
|
35
|
+
" alab Rust compiler not built. Run `cargo build --release -p alab-napi && bash scripts/copy-napi-binary.sh`.",
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const appDir = resolve(cwd, "app");
|
|
41
|
+
|
|
42
|
+
// ── Route manifest ────────────────────────────────────────────────────────
|
|
43
|
+
const manifestJson = napi.buildRoutes(appDir);
|
|
44
|
+
const manifest = JSON.parse(manifestJson) as {
|
|
45
|
+
routes: Array<{ path: string; kind: string; ssr: boolean }>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
console.log("\n alab route manifest\n");
|
|
49
|
+
if (manifest.routes.length === 0) {
|
|
50
|
+
console.log(" (no routes found — add page.tsx files under app/)\n");
|
|
51
|
+
} else {
|
|
52
|
+
const rows = manifest.routes.map((r) => ({
|
|
53
|
+
path: r.path,
|
|
54
|
+
kind: r.kind,
|
|
55
|
+
ssr: r.ssr ? "yes" : "no",
|
|
56
|
+
}));
|
|
57
|
+
console.table(rows);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Server functions ──────────────────────────────────────────────────────
|
|
61
|
+
const serverFiles = await findFiles(appDir, (n) => n.endsWith(".server.ts") || n.endsWith(".server.tsx"));
|
|
62
|
+
|
|
63
|
+
if (serverFiles.length > 0) {
|
|
64
|
+
console.log(" alab server functions\n");
|
|
65
|
+
|
|
66
|
+
let totalFns = 0;
|
|
67
|
+
for (const file of serverFiles) {
|
|
68
|
+
const source = await readFile(file, "utf8");
|
|
69
|
+
const fnsJson = napi.extractServerFns(source, file);
|
|
70
|
+
const fns = JSON.parse(fnsJson) as Array<{ name: string; endpoint: string }>;
|
|
71
|
+
|
|
72
|
+
const rel = file.replace(appDir, "app");
|
|
73
|
+
if (fns.length === 0) {
|
|
74
|
+
console.log(` ${rel} (no defineServerFn exports)`);
|
|
75
|
+
} else {
|
|
76
|
+
for (const fn of fns) {
|
|
77
|
+
console.log(` ${rel} ${fn.name} → POST ${fn.endpoint}`);
|
|
78
|
+
totalFns++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.log(`\n ${totalFns} server function${totalFns === 1 ? "" : "s"} across ${serverFiles.length} file${serverFiles.length === 1 ? "" : "s"}\n`);
|
|
83
|
+
} else {
|
|
84
|
+
console.log(" alab no .server.ts files found\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Boundary check ────────────────────────────────────────────────────────
|
|
88
|
+
const pageFiles = await findFiles(
|
|
89
|
+
appDir,
|
|
90
|
+
(n) => (n.endsWith(".tsx") || n.endsWith(".ts")) && !n.includes(".server."),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
let violationCount = 0;
|
|
94
|
+
for (const file of pageFiles) {
|
|
95
|
+
const source = await readFile(file, "utf8");
|
|
96
|
+
const violationsJson = napi.checkBoundary(source, file);
|
|
97
|
+
const violations = JSON.parse(violationsJson) as Array<{
|
|
98
|
+
import: string;
|
|
99
|
+
source: string;
|
|
100
|
+
offset: number;
|
|
101
|
+
}>;
|
|
102
|
+
for (const v of violations) {
|
|
103
|
+
if (violationCount === 0) console.log(" alab boundary violations\n");
|
|
104
|
+
console.log(` ⚠ ${v.source.replace(appDir, "app")} imports server module "${v.import}"`);
|
|
105
|
+
violationCount++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (violationCount > 0) {
|
|
109
|
+
console.log(`\n ${violationCount} violation${violationCount === 1 ? "" : "s"} — fix by using \`import type\` or removing runtime server imports\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|