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,173 @@
|
|
|
1
|
+
import { build as viteBuild } from "vite";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
5
|
+
/** Run `tsc --noEmit` in the project and resolve when it exits. */
|
|
6
|
+
function runTypecheck(cwd) {
|
|
7
|
+
return new Promise((ok, fail) => {
|
|
8
|
+
const tscPath = resolve(cwd, "node_modules/.bin/tsc");
|
|
9
|
+
const bin = existsSync(tscPath) ? tscPath : "tsc";
|
|
10
|
+
const child = spawn(bin, ["--noEmit"], { cwd, stdio: "inherit", shell: true });
|
|
11
|
+
child.on("close", (code) => {
|
|
12
|
+
if (code === 0)
|
|
13
|
+
ok();
|
|
14
|
+
else
|
|
15
|
+
fail(new Error(`[alabjs] TypeScript type errors found (tsc --noEmit exited ${code})`));
|
|
16
|
+
});
|
|
17
|
+
child.on("error", fail);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SPA mode: emit a single index.html shell + hashed client assets.
|
|
22
|
+
* No SSR build step — React hydrates from scratch on the client.
|
|
23
|
+
*/
|
|
24
|
+
async function buildSpa(cwd) {
|
|
25
|
+
const outDir = resolve(cwd, ".alabjs/dist/spa");
|
|
26
|
+
// Emit a minimal index.html if the project doesn't have one.
|
|
27
|
+
// We do this BEFORE calling viteBuild so Vite can resolve it as an entry point.
|
|
28
|
+
const indexPath = resolve(cwd, "index.html");
|
|
29
|
+
let temporaryIndex = false;
|
|
30
|
+
if (!existsSync(indexPath)) {
|
|
31
|
+
temporaryIndex = true;
|
|
32
|
+
const spa = `<!doctype html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="UTF-8" />
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
37
|
+
<link rel="stylesheet" href="/app/globals.css" />
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div id="alabjs-root"></div>
|
|
41
|
+
<script type="module" src="/@alabjs/client"></script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>`;
|
|
44
|
+
writeFileSync(indexPath, spa, "utf8");
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await viteBuild({
|
|
48
|
+
root: cwd,
|
|
49
|
+
plugins: [(await import("alabjs-vite-plugin")).alabPlugin({ mode: "build" })],
|
|
50
|
+
build: {
|
|
51
|
+
outDir,
|
|
52
|
+
// Client-only — no SSR entry, no server manifest needed.
|
|
53
|
+
ssrManifest: false,
|
|
54
|
+
rolldownOptions: {
|
|
55
|
+
input: indexPath,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
// If we created a temporary index.html, remove it after the build.
|
|
62
|
+
if (temporaryIndex) {
|
|
63
|
+
const { rmSync } = await import("node:fs");
|
|
64
|
+
rmSync(indexPath, { force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
console.log("\n alab SPA build complete → .alabjs/dist/spa");
|
|
68
|
+
console.log(" alab deploy the spa/ directory to any static host (Netlify, GitHub Pages, S3)\n");
|
|
69
|
+
}
|
|
70
|
+
export async function build({ cwd, skipTypecheck = false, mode = "ssr", analyze = false }) {
|
|
71
|
+
if (mode === "spa") {
|
|
72
|
+
console.log(" alab building SPA (client-only)...\n");
|
|
73
|
+
const tasks = [buildSpa(cwd)];
|
|
74
|
+
if (!skipTypecheck) {
|
|
75
|
+
console.log(" alab type-checking...");
|
|
76
|
+
tasks.push(runTypecheck(cwd));
|
|
77
|
+
}
|
|
78
|
+
await Promise.all(tasks);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(" alab building for production...\n");
|
|
82
|
+
// Run type checking and Vite bundling in parallel.
|
|
83
|
+
// Type errors abort the build before wrangler/deploy picks up bad output.
|
|
84
|
+
// Bundle visualizer — try rolldown-plugin-visualizer first (faster, Rust-native),
|
|
85
|
+
// fall back to rollup-plugin-visualizer (same API, works on both bundlers).
|
|
86
|
+
let visualizerPlugin = null;
|
|
87
|
+
if (analyze) {
|
|
88
|
+
const reportPath = resolve(cwd, ".alabjs/report.html");
|
|
89
|
+
const vizOpts = {
|
|
90
|
+
filename: reportPath,
|
|
91
|
+
open: true,
|
|
92
|
+
gzipSize: true,
|
|
93
|
+
brotliSize: true,
|
|
94
|
+
title: "Alab — bundle analysis",
|
|
95
|
+
};
|
|
96
|
+
let loaded = false;
|
|
97
|
+
for (const pkg of ["rolldown-plugin-visualizer", "rollup-plugin-visualizer"]) {
|
|
98
|
+
try {
|
|
99
|
+
const { visualizer } = await import(pkg);
|
|
100
|
+
visualizerPlugin = visualizer(vizOpts);
|
|
101
|
+
console.log(` alab bundle report → .alabjs/report.html (${pkg})\n`);
|
|
102
|
+
loaded = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = String(err);
|
|
107
|
+
// Only silently skip "module not found"; anything else is unexpected.
|
|
108
|
+
if (!msg.includes("Cannot find module") && !msg.includes("ERR_MODULE_NOT_FOUND")) {
|
|
109
|
+
console.warn(` alab warning: failed to load visualizer (${pkg}): ${msg}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!loaded) {
|
|
114
|
+
console.warn(" alab warning: install rolldown-plugin-visualizer or rollup-plugin-visualizer to enable --analyze.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const tasks = [
|
|
118
|
+
viteBuild({
|
|
119
|
+
root: cwd,
|
|
120
|
+
plugins: [
|
|
121
|
+
(await import("alabjs-vite-plugin")).alabPlugin({ mode: "build" }),
|
|
122
|
+
...(visualizerPlugin ? [visualizerPlugin] : []),
|
|
123
|
+
],
|
|
124
|
+
build: {
|
|
125
|
+
outDir: resolve(cwd, ".alabjs/dist"),
|
|
126
|
+
ssrManifest: true,
|
|
127
|
+
rolldownOptions: {
|
|
128
|
+
// In SSR mode, we don't use an index.html as the entry point.
|
|
129
|
+
// Instead, we bundle the virtual client module as the main browser asset.
|
|
130
|
+
input: "/@alabjs/client",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
];
|
|
135
|
+
if (!skipTypecheck) {
|
|
136
|
+
console.log(" alab type-checking...");
|
|
137
|
+
tasks.push(runTypecheck(cwd));
|
|
138
|
+
}
|
|
139
|
+
await Promise.all(tasks);
|
|
140
|
+
// Bundle the offline service worker as a separate iife chunk.
|
|
141
|
+
// Output: .alabjs/dist/client/_alabjs/offline-sw.js (served at /_alabjs/offline-sw.js)
|
|
142
|
+
await buildOfflineSw(cwd);
|
|
143
|
+
console.log("\n alab build complete → .alabjs/dist");
|
|
144
|
+
}
|
|
145
|
+
/** Compile the offline service worker to a standalone iife bundle. */
|
|
146
|
+
async function buildOfflineSw(cwd) {
|
|
147
|
+
const swEntry = new URL("../client/offline-sw.js", import.meta.url).pathname;
|
|
148
|
+
const outDir = resolve(cwd, ".alabjs/dist/client/_alabjs");
|
|
149
|
+
try {
|
|
150
|
+
await viteBuild({
|
|
151
|
+
root: cwd,
|
|
152
|
+
configFile: false,
|
|
153
|
+
build: {
|
|
154
|
+
outDir,
|
|
155
|
+
emptyOutDir: false,
|
|
156
|
+
lib: {
|
|
157
|
+
entry: swEntry,
|
|
158
|
+
name: "AlabOfflineSW",
|
|
159
|
+
formats: ["iife"],
|
|
160
|
+
fileName: () => "offline-sw.js",
|
|
161
|
+
},
|
|
162
|
+
minify: true,
|
|
163
|
+
rolldownOptions: { output: { inlineDynamicImports: true } },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// Non-fatal: offline SW is a progressive enhancement
|
|
169
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
170
|
+
console.warn(` alab warning: could not build offline service worker: ${msg}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAsBA,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoCD,wBAAsB,GAAG,CAAC,EAAE,GAAG,EAAE,IAAW,EAAE,IAAkB,EAAE,EAAE,UAAU,iBA8b7E"}
|
|
@@ -0,0 +1,447 @@
|
|
|
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 { scanDevRoutes, matchDevRoute, findLayoutFiles, findErrorFile, findLoadingFile, scanDevApiRoutes, matchDevApiRoute, } from "../ssr/router-dev.js";
|
|
6
|
+
import { htmlShellBefore, htmlShellAfter } from "../ssr/html.js";
|
|
7
|
+
import { generateSitemap } from "../server/sitemap.js";
|
|
8
|
+
import { handleImageRequest } from "../server/image.js";
|
|
9
|
+
import { runMiddleware } from "../server/middleware.js";
|
|
10
|
+
import { getCachedPage, setCachedPage, markPageRevalidating, isPageRevalidating, } from "../server/cache.js";
|
|
11
|
+
import { checkRevalidateAuth, applyRevalidate } from "../server/revalidate.js";
|
|
12
|
+
/** Recursively find all *.server.ts / *.server.tsx files under a directory. */
|
|
13
|
+
function findServerFiles(dir) {
|
|
14
|
+
const results = [];
|
|
15
|
+
try {
|
|
16
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
+
const full = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
results.push(...findServerFiles(full));
|
|
20
|
+
}
|
|
21
|
+
else if (/\.server\.(ts|tsx)$/.test(entry.name)) {
|
|
22
|
+
results.push(full);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const code = err.code;
|
|
28
|
+
if (code !== "ENOENT") {
|
|
29
|
+
console.warn(`[alabjs] warning: failed to scan ${dir}:`, err.message ?? err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
/** Read and JSON-parse the request body. Returns undefined on empty or invalid JSON. */
|
|
35
|
+
async function readJsonBody(req) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
req.on("data", (c) => chunks.push(c));
|
|
39
|
+
req.on("end", () => {
|
|
40
|
+
try {
|
|
41
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
resolve(undefined);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
req.on("error", () => resolve(undefined));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function dev({ cwd, port = 3000, host = "localhost" }) {
|
|
51
|
+
console.log(" alab starting dev server...\n");
|
|
52
|
+
const appDir = resolve(cwd, "app");
|
|
53
|
+
const vite = await createServer({
|
|
54
|
+
root: cwd,
|
|
55
|
+
appType: "custom",
|
|
56
|
+
server: { port, host },
|
|
57
|
+
plugins: [
|
|
58
|
+
(await import("alabjs-vite-plugin")).alabPlugin(),
|
|
59
|
+
],
|
|
60
|
+
ssr: {
|
|
61
|
+
// Externalize react packages so Node.js loads them natively (avoids
|
|
62
|
+
// CJS/ESM mismatch in Vite's module runner for react-dom/server).
|
|
63
|
+
external: ["react", "react-dom", "react-dom/server", "react-dom/server.node"],
|
|
64
|
+
noExternal: ["alab", "alabjs-vite-plugin"],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// ─── Alab SSR + built-in route middleware ────────────────────────────────────
|
|
68
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
69
|
+
const rawUrl = req.url ?? "/";
|
|
70
|
+
const pathname = rawUrl.split("?")[0] ?? "/";
|
|
71
|
+
// Pass Vite-internal requests through
|
|
72
|
+
if (pathname.startsWith("/@") ||
|
|
73
|
+
pathname.startsWith("/__vite") ||
|
|
74
|
+
pathname.startsWith("/node_modules") ||
|
|
75
|
+
pathname.startsWith("/@alab")) {
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
// Apply security headers to every alab-handled response.
|
|
79
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
80
|
+
res.setHeader("x-frame-options", "SAMEORIGIN");
|
|
81
|
+
res.setHeader("referrer-policy", "strict-origin-when-cross-origin");
|
|
82
|
+
res.setHeader("permissions-policy", "camera=(), microphone=(), geolocation=()");
|
|
83
|
+
res.setHeader("x-permitted-cross-domain-policies", "none");
|
|
84
|
+
try {
|
|
85
|
+
// ── User middleware (middleware.ts at project root) ───────────────────────
|
|
86
|
+
const middlewareFile = resolve(cwd, "middleware.ts");
|
|
87
|
+
const { existsSync: fsExists } = await import("node:fs");
|
|
88
|
+
if (fsExists(middlewareFile)) {
|
|
89
|
+
const middlewareMod = await vite.ssrLoadModule(middlewareFile);
|
|
90
|
+
if (typeof middlewareMod.middleware === "function") {
|
|
91
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
92
|
+
const webReq = new Request(url.toString(), {
|
|
93
|
+
method: req.method ?? "GET",
|
|
94
|
+
headers: req.headers,
|
|
95
|
+
});
|
|
96
|
+
const middlewareRes = await runMiddleware(middlewareMod, webReq);
|
|
97
|
+
if (middlewareRes) {
|
|
98
|
+
res.statusCode = middlewareRes.status;
|
|
99
|
+
middlewareRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
100
|
+
res.end(Buffer.from(await middlewareRes.arrayBuffer()));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── /_alabjs/data/:fnName — GET data from a defineServerFn (useServerData) ─
|
|
106
|
+
// ── /_alabjs/fn/:fnName — POST mutation via defineServerFn stub ───────────
|
|
107
|
+
if (pathname.startsWith("/_alabjs/data/") || pathname.startsWith("/_alabjs/fn/")) {
|
|
108
|
+
const fnName = pathname.startsWith("/_alabjs/data/")
|
|
109
|
+
? pathname.slice("/_alabjs/data/".length)
|
|
110
|
+
: pathname.slice("/_alabjs/fn/".length);
|
|
111
|
+
const serverFiles = findServerFiles(appDir);
|
|
112
|
+
let found = false;
|
|
113
|
+
for (const file of serverFiles) {
|
|
114
|
+
const mod = await vite.ssrLoadModule(file);
|
|
115
|
+
if (typeof mod[fnName] === "function") {
|
|
116
|
+
found = true;
|
|
117
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
118
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
119
|
+
const input = req.method === "POST" ? await readJsonBody(req) : undefined;
|
|
120
|
+
const ctx = {
|
|
121
|
+
params,
|
|
122
|
+
query: params,
|
|
123
|
+
headers: req.headers,
|
|
124
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
125
|
+
url: rawUrl,
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
const result = await mod[fnName](ctx, input);
|
|
129
|
+
res.statusCode = 200;
|
|
130
|
+
res.setHeader("content-type", "application/json");
|
|
131
|
+
res.end(JSON.stringify(result));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// Zod validation errors from defineServerFn get HTTP 422
|
|
135
|
+
const zodError = err?.["zodError"];
|
|
136
|
+
if (zodError) {
|
|
137
|
+
res.statusCode = 422;
|
|
138
|
+
res.setHeader("content-type", "application/json");
|
|
139
|
+
res.end(JSON.stringify({ zodError }));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
res.statusCode = 500;
|
|
143
|
+
res.setHeader("content-type", "application/json");
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
145
|
+
res.end(JSON.stringify({ error: msg }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!found) {
|
|
152
|
+
res.statusCode = 404;
|
|
153
|
+
res.setHeader("content-type", "application/json");
|
|
154
|
+
res.end(JSON.stringify({ error: `[alabjs] server function not found: ${fnName}` }));
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// ── /_alabjs/image — Rust-powered image optimisation ───────────────────────
|
|
159
|
+
if (pathname === "/_alabjs/image") {
|
|
160
|
+
const publicDir = resolve(cwd, "public");
|
|
161
|
+
await handleImageRequest(req, res, publicDir);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// ── /sitemap.xml ────────────────────────────────────────────────────────
|
|
165
|
+
if (pathname === "/sitemap.xml") {
|
|
166
|
+
const devRoutes = scanDevRoutes(appDir);
|
|
167
|
+
const manifestRoutes = devRoutes.map((r) => ({
|
|
168
|
+
path: r.file
|
|
169
|
+
.replace(appDir, "")
|
|
170
|
+
.replace(/\/page\.(tsx|ts)$/, "") || "/",
|
|
171
|
+
file: r.file.replace(cwd + "/", ""),
|
|
172
|
+
kind: "page",
|
|
173
|
+
ssr: r.ssr,
|
|
174
|
+
params: r.paramNames,
|
|
175
|
+
}));
|
|
176
|
+
const xml = generateSitemap(manifestRoutes, `http://${host}:${port}`);
|
|
177
|
+
res.statusCode = 200;
|
|
178
|
+
res.setHeader("content-type", "application/xml; charset=utf-8");
|
|
179
|
+
res.end(xml);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// ── On-demand ISR revalidation ────────────────────────────────────────────
|
|
183
|
+
if (pathname === "/_alabjs/revalidate") {
|
|
184
|
+
if (req.method !== "POST") {
|
|
185
|
+
res.statusCode = 405;
|
|
186
|
+
res.setHeader("allow", "POST");
|
|
187
|
+
res.setHeader("content-type", "application/json");
|
|
188
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!checkRevalidateAuth(req.headers["authorization"])) {
|
|
192
|
+
res.statusCode = 401;
|
|
193
|
+
res.setHeader("content-type", "application/json");
|
|
194
|
+
res.end(JSON.stringify({ error: "Unauthorized. Set Authorization: Bearer <ALAB_REVALIDATE_SECRET>." }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const chunks = [];
|
|
198
|
+
await new Promise((ok) => {
|
|
199
|
+
req.on("data", (c) => chunks.push(c));
|
|
200
|
+
req.on("end", ok);
|
|
201
|
+
});
|
|
202
|
+
let body;
|
|
203
|
+
try {
|
|
204
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
body = null;
|
|
208
|
+
}
|
|
209
|
+
const result = applyRevalidate(body);
|
|
210
|
+
res.statusCode = "error" in result ? result.status : 200;
|
|
211
|
+
res.setHeader("content-type", "application/json");
|
|
212
|
+
res.end(JSON.stringify("error" in result ? { error: result.error } : result));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// ── API routes (route.ts) ─────────────────────────────────────────────────
|
|
216
|
+
const apiRoutes = scanDevApiRoutes(appDir);
|
|
217
|
+
const matchedApi = matchDevApiRoute(apiRoutes, pathname);
|
|
218
|
+
if (matchedApi) {
|
|
219
|
+
const apiMod = await vite.ssrLoadModule(matchedApi.route.file);
|
|
220
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
221
|
+
const handler = apiMod[method];
|
|
222
|
+
if (typeof handler !== "function") {
|
|
223
|
+
res.statusCode = 405;
|
|
224
|
+
res.setHeader("allow", Object.keys(apiMod).filter(k => /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/.test(k)).join(", "));
|
|
225
|
+
res.end("Method Not Allowed");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const url = new URL(rawUrl, `http://${host}:${port}`);
|
|
229
|
+
const chunks = [];
|
|
230
|
+
await new Promise((ok) => {
|
|
231
|
+
req.on("data", (c) => chunks.push(c));
|
|
232
|
+
req.on("end", ok);
|
|
233
|
+
});
|
|
234
|
+
const body = chunks.length ? Buffer.concat(chunks) : null;
|
|
235
|
+
const webReq = new Request(url.toString(), {
|
|
236
|
+
method,
|
|
237
|
+
headers: req.headers,
|
|
238
|
+
body: body?.length ? body : null,
|
|
239
|
+
});
|
|
240
|
+
const webRes = await handler(webReq);
|
|
241
|
+
res.statusCode = webRes.status;
|
|
242
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
243
|
+
// SSE: pipe the ReadableStream body without buffering.
|
|
244
|
+
if ((webRes.headers.get("content-type") ?? "").startsWith("text/event-stream") &&
|
|
245
|
+
webRes.body) {
|
|
246
|
+
const reader = webRes.body.getReader();
|
|
247
|
+
res.on("close", () => { void reader.cancel(); });
|
|
248
|
+
try {
|
|
249
|
+
while (true) {
|
|
250
|
+
const { done, value } = await reader.read();
|
|
251
|
+
if (done || res.destroyed)
|
|
252
|
+
break;
|
|
253
|
+
res.write(value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { /* client disconnected */ }
|
|
257
|
+
finally {
|
|
258
|
+
res.end();
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
res.end(Buffer.from(await webRes.arrayBuffer()));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// ── Page routes ──────────────────────────────────────────────────────────
|
|
266
|
+
const routes = scanDevRoutes(appDir);
|
|
267
|
+
const matched = matchDevRoute(routes, pathname);
|
|
268
|
+
// ── Not-found page ────────────────────────────────────────────────────────
|
|
269
|
+
if (!matched) {
|
|
270
|
+
const wantsHtml = (req.headers["accept"] ?? "").includes("text/html");
|
|
271
|
+
if (!wantsHtml)
|
|
272
|
+
return next();
|
|
273
|
+
const notFoundFile = resolve(appDir, "not-found.tsx");
|
|
274
|
+
const { existsSync } = await import("node:fs");
|
|
275
|
+
if (existsSync(notFoundFile)) {
|
|
276
|
+
const nfMod = await vite.ssrLoadModule(notFoundFile);
|
|
277
|
+
const NotFound = nfMod["default"];
|
|
278
|
+
if (typeof NotFound === "function") {
|
|
279
|
+
const { createElement } = await import("react");
|
|
280
|
+
const { renderToPipeableStream } = await import("react-dom/server.node");
|
|
281
|
+
const nfContent = await new Promise((ok, fail) => {
|
|
282
|
+
let html = "";
|
|
283
|
+
const sink = new Writable({
|
|
284
|
+
write(chunk, _enc, cb) { html += chunk.toString(); cb(); },
|
|
285
|
+
});
|
|
286
|
+
sink.on("finish", () => ok(html));
|
|
287
|
+
const { pipe } = renderToPipeableStream(createElement(NotFound, {}), {
|
|
288
|
+
onAllReady() { pipe(sink); },
|
|
289
|
+
onError(e) { fail(e); },
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
const shell = htmlShellBefore({ metadata: { title: "404 — Not Found" }, paramsJson: "{}", searchParamsJson: "{}", routeFile: "app/not-found.tsx", ssr: true });
|
|
293
|
+
const rawHtml = `${shell}${nfContent}${htmlShellAfter({})}`;
|
|
294
|
+
const html = await vite.transformIndexHtml(pathname, rawHtml);
|
|
295
|
+
res.statusCode = 404;
|
|
296
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
297
|
+
res.end(html);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return next();
|
|
302
|
+
}
|
|
303
|
+
const { route, params } = matched;
|
|
304
|
+
const mod = await vite.ssrLoadModule(route.file);
|
|
305
|
+
const Page = mod.default;
|
|
306
|
+
if (typeof Page !== "function") {
|
|
307
|
+
vite.ssrFixStacktrace(new Error(`[alabjs] Page module has no default export: ${route.file}`));
|
|
308
|
+
return next();
|
|
309
|
+
}
|
|
310
|
+
// Support both static `export const metadata` and dynamic `export async function generateMetadata`.
|
|
311
|
+
const metadata = typeof mod.generateMetadata === "function"
|
|
312
|
+
? await mod.generateMetadata(params)
|
|
313
|
+
: (mod.metadata ?? {});
|
|
314
|
+
// Make the server's base URL available to useServerData during SSR,
|
|
315
|
+
// so it can construct an absolute URL for its internal fetch call.
|
|
316
|
+
process.env["ALAB_ORIGIN"] = `http://${host}:${port}`;
|
|
317
|
+
const ssrEnabled = mod.ssr === true;
|
|
318
|
+
const searchParams = Object.fromEntries(new URLSearchParams(rawUrl.includes("?") ? rawUrl.split("?")[1] : "").entries());
|
|
319
|
+
// ── Layouts + loading file ────────────────────────────────────────────────
|
|
320
|
+
const layoutFiles = findLayoutFiles(route.file, appDir);
|
|
321
|
+
const layoutMods = await Promise.all(layoutFiles.map((f) => vite.ssrLoadModule(f)));
|
|
322
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
323
|
+
const layoutComponents = layoutMods.map((m) => m["default"]).filter((c) => typeof c === "function");
|
|
324
|
+
const layoutsJson = JSON.stringify(layoutFiles.map((f) => f.replace(cwd + "/", "")));
|
|
325
|
+
const loadingFileAbs = findLoadingFile(route.file, appDir);
|
|
326
|
+
const loadingFile = loadingFileAbs ? loadingFileAbs.replace(cwd + "/", "") : undefined;
|
|
327
|
+
const { renderToPipeableStream } = await import("react-dom/server.node");
|
|
328
|
+
const { createElement } = await import("react");
|
|
329
|
+
// Clear SSR promise cache so each request gets fresh data but re-renders
|
|
330
|
+
// within the same renderToPipeableStream pass reuse the same promise.
|
|
331
|
+
const alabClient = await vite.ssrLoadModule("alabjs/client");
|
|
332
|
+
alabClient._clearALabSSRCache?.();
|
|
333
|
+
// Build element tree: Page wrapped by layouts outermost→innermost
|
|
334
|
+
const buildTree = (PageComp) => {
|
|
335
|
+
let el = createElement(PageComp, { params, searchParams });
|
|
336
|
+
for (let i = layoutComponents.length - 1; i >= 0; i--) {
|
|
337
|
+
el = createElement(layoutComponents[i], {}, el);
|
|
338
|
+
}
|
|
339
|
+
return el;
|
|
340
|
+
};
|
|
341
|
+
let ssrContent = "";
|
|
342
|
+
if (ssrEnabled) {
|
|
343
|
+
try {
|
|
344
|
+
ssrContent = await new Promise((ok, fail) => {
|
|
345
|
+
let html = "";
|
|
346
|
+
const sink = new Writable({
|
|
347
|
+
write(chunk, _enc, cb) {
|
|
348
|
+
html += chunk.toString();
|
|
349
|
+
cb();
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
sink.on("finish", () => ok(html));
|
|
353
|
+
const { pipe } = renderToPipeableStream(buildTree(Page), {
|
|
354
|
+
onAllReady() { pipe(sink); },
|
|
355
|
+
onError(err) { fail(err); },
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (ssrErr) {
|
|
360
|
+
// ── error.tsx fallback ──────────────────────────────────────────────
|
|
361
|
+
const errorFile = findErrorFile(route.file, appDir);
|
|
362
|
+
if (errorFile) {
|
|
363
|
+
try {
|
|
364
|
+
const errorMod = await vite.ssrLoadModule(errorFile);
|
|
365
|
+
const ErrorPage = errorMod["default"];
|
|
366
|
+
if (typeof ErrorPage === "function") {
|
|
367
|
+
ssrContent = await new Promise((ok, fail) => {
|
|
368
|
+
let html = "";
|
|
369
|
+
const sink = new Writable({
|
|
370
|
+
write(chunk, _enc, cb) { html += chunk.toString(); cb(); },
|
|
371
|
+
});
|
|
372
|
+
sink.on("finish", () => ok(html));
|
|
373
|
+
const { pipe } = renderToPipeableStream(createElement(ErrorPage, { error: ssrErr, reset: () => { } }), { onAllReady() { pipe(sink); }, onError(e) { fail(e); } });
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (errorPageErr) {
|
|
378
|
+
console.error("[alabjs] error.tsx SSR render failed:", errorPageErr);
|
|
379
|
+
// fall through to plain text error
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!ssrContent) {
|
|
383
|
+
res.statusCode = 500;
|
|
384
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
385
|
+
res.end(`[alabjs] SSR error: ${String(ssrErr)}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const routeFile = route.file.replace(cwd, "").replace(/^\//, "");
|
|
391
|
+
// ── Render helper (used for both fresh render + background revalidation) ─
|
|
392
|
+
const revalidateSecs = typeof mod.revalidate === "number" ? mod.revalidate : null;
|
|
393
|
+
const renderPageHtml = async () => {
|
|
394
|
+
const shellBefore = htmlShellBefore({
|
|
395
|
+
metadata,
|
|
396
|
+
paramsJson: JSON.stringify(params),
|
|
397
|
+
searchParamsJson: JSON.stringify(searchParams),
|
|
398
|
+
routeFile,
|
|
399
|
+
layoutsJson,
|
|
400
|
+
loadingFile,
|
|
401
|
+
ssr: ssrEnabled,
|
|
402
|
+
});
|
|
403
|
+
const shellAfter = htmlShellAfter({});
|
|
404
|
+
const rawHtml = `${shellBefore}${ssrContent}${shellAfter}`;
|
|
405
|
+
return vite.transformIndexHtml(pathname, rawHtml);
|
|
406
|
+
};
|
|
407
|
+
// ── ISR: serve cached page if available ──────────────────────────────────
|
|
408
|
+
if (revalidateSecs !== null) {
|
|
409
|
+
const cached = getCachedPage(pathname);
|
|
410
|
+
if (cached) {
|
|
411
|
+
res.statusCode = 200;
|
|
412
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
413
|
+
res.setHeader("x-alab-cache", cached.stale ? "stale" : "hit");
|
|
414
|
+
res.end(cached.html);
|
|
415
|
+
// Background revalidation for stale entries
|
|
416
|
+
if (cached.stale && !isPageRevalidating(pathname)) {
|
|
417
|
+
markPageRevalidating(pathname);
|
|
418
|
+
void renderPageHtml().then((fresh) => {
|
|
419
|
+
setCachedPage(pathname, fresh, revalidateSecs);
|
|
420
|
+
}).catch((revalErr) => {
|
|
421
|
+
console.warn(`[alabjs] ISR revalidation failed for ${pathname}:`, revalErr);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const html = await renderPageHtml();
|
|
428
|
+
// Store in ISR cache if page exports `revalidate`
|
|
429
|
+
if (revalidateSecs !== null) {
|
|
430
|
+
setCachedPage(pathname, html, revalidateSecs);
|
|
431
|
+
}
|
|
432
|
+
res.statusCode = 200;
|
|
433
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
434
|
+
if (revalidateSecs !== null)
|
|
435
|
+
res.setHeader("x-alab-cache", "miss");
|
|
436
|
+
res.end(html);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
console.error(`[alabjs] unhandled error on ${pathname}:`, err);
|
|
440
|
+
vite.ssrFixStacktrace(err);
|
|
441
|
+
next(err);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
await vite.listen();
|
|
445
|
+
vite.printUrls();
|
|
446
|
+
}
|
|
447
|
+
//# sourceMappingURL=dev.js.map
|