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,466 @@
|
|
|
1
|
+
import { createApp as createH3App, createRouter, defineEventHandler, getQuery, readBody } from "h3";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { resolve, dirname, join, extname } from "node:path";
|
|
4
|
+
import { existsSync, createReadStream, statSync } from "node:fs";
|
|
5
|
+
import { toNodeListener } from "h3";
|
|
6
|
+
import type { RouteManifest } from "../router/manifest.js";
|
|
7
|
+
import { renderToResponse } from "../ssr/render.js";
|
|
8
|
+
import { generateSitemap } from "./sitemap.js";
|
|
9
|
+
import { csrfMiddleware, setCsrfCookie } from "./csrf.js";
|
|
10
|
+
import { handleImageRequest } from "./image.js";
|
|
11
|
+
import type { MiddlewareModule } from "./middleware.js";
|
|
12
|
+
import { runMiddleware } from "./middleware.js";
|
|
13
|
+
import type { PageMetadata } from "../types/index.js";
|
|
14
|
+
import { checkRevalidateAuth, applyRevalidate } from "./revalidate.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find layout file paths (relative to cwd root) for a given route.file, ordered outermost→innermost.
|
|
18
|
+
* Checks the compiled dist directory for the existence of each layout.
|
|
19
|
+
*/
|
|
20
|
+
function findProdLayoutFiles(routeFile: string, distDir: string): string[] {
|
|
21
|
+
// routeFile is like "app/users/[id]/page.tsx"
|
|
22
|
+
const pageDir = dirname(routeFile);
|
|
23
|
+
const parts = pageDir.split("/");
|
|
24
|
+
const layouts: string[] = [];
|
|
25
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
26
|
+
const dir = parts.slice(0, i).join("/");
|
|
27
|
+
const layoutRelPath = `${dir}/layout.tsx`;
|
|
28
|
+
if (existsSync(join(distDir, "server", layoutRelPath))) {
|
|
29
|
+
layouts.push(layoutRelPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return layouts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find nearest error.tsx for a given route.file, searching innermost→outermost.
|
|
37
|
+
*/
|
|
38
|
+
function findProdErrorFile(routeFile: string, distDir: string): string | null {
|
|
39
|
+
let dir = dirname(routeFile);
|
|
40
|
+
while (dir.length > 0 && dir !== ".") {
|
|
41
|
+
const candidate = `${dir}/error.tsx`;
|
|
42
|
+
if (existsSync(join(distDir, "server", candidate))) return candidate;
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir) break;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findProdLoadingFile(routeFile: string, distDir: string): string | null {
|
|
51
|
+
let dir = dirname(routeFile);
|
|
52
|
+
while (dir.length > 0 && dir !== ".") {
|
|
53
|
+
const candidate = `${dir}/loading.tsx`;
|
|
54
|
+
if (existsSync(join(distDir, "server", candidate))) return candidate;
|
|
55
|
+
const parent = dirname(dir);
|
|
56
|
+
if (parent === dir) break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AlabApp {
|
|
63
|
+
listen(port?: number): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create and return an Alab HTTP application backed by H3.
|
|
68
|
+
*
|
|
69
|
+
* In production (`alab start`), this is the entry point.
|
|
70
|
+
* In development, Vite's dev server wraps the SSR logic directly via the
|
|
71
|
+
* dev command middleware — this function is not used in dev.
|
|
72
|
+
*/
|
|
73
|
+
export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
74
|
+
const app = createH3App();
|
|
75
|
+
const router = createRouter();
|
|
76
|
+
const publicDir = resolve(distDir, "../../public");
|
|
77
|
+
|
|
78
|
+
// ─── Global middleware ───────────────────────────────────────────────────────
|
|
79
|
+
app.use(
|
|
80
|
+
defineEventHandler((event) => {
|
|
81
|
+
const res = event.node.res;
|
|
82
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
83
|
+
res.setHeader("x-frame-options", "SAMEORIGIN");
|
|
84
|
+
res.setHeader("referrer-policy", "strict-origin-when-cross-origin");
|
|
85
|
+
res.setHeader("permissions-policy", "camera=(), microphone=(), geolocation=()");
|
|
86
|
+
res.setHeader("x-permitted-cross-domain-policies", "none");
|
|
87
|
+
// HSTS — only meaningful over HTTPS; set in production only.
|
|
88
|
+
res.setHeader("strict-transport-security", "max-age=31536000; includeSubDomains");
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// ─── User middleware (middleware.ts compiled to dist/server/middleware.ts) ───
|
|
93
|
+
const middlewareModulePath = `${distDir}/server/middleware.ts`;
|
|
94
|
+
if (existsSync(middlewareModulePath)) {
|
|
95
|
+
app.use(
|
|
96
|
+
defineEventHandler(async (event) => {
|
|
97
|
+
const mod = await import(middlewareModulePath) as MiddlewareModule;
|
|
98
|
+
if (typeof mod.middleware !== "function") return;
|
|
99
|
+
const req = event.node.req;
|
|
100
|
+
const res = event.node.res;
|
|
101
|
+
const url = new URL(
|
|
102
|
+
req.url ?? "/",
|
|
103
|
+
`http://${req.headers.host ?? "localhost"}`,
|
|
104
|
+
);
|
|
105
|
+
const webReq = new Request(url.toString(), {
|
|
106
|
+
method: req.method ?? "GET",
|
|
107
|
+
headers: req.headers as HeadersInit,
|
|
108
|
+
});
|
|
109
|
+
const middlewareRes = await runMiddleware(mod, webReq);
|
|
110
|
+
if (middlewareRes) {
|
|
111
|
+
res.statusCode = middlewareRes.status;
|
|
112
|
+
middlewareRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
113
|
+
res.end(Buffer.from(await middlewareRes.arrayBuffer()));
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CSRF protection (active in production, skipped in dev)
|
|
122
|
+
app.use(csrfMiddleware());
|
|
123
|
+
|
|
124
|
+
// ─── Static file serving ────────────────────────────────────────────────────
|
|
125
|
+
// Serves built client assets (JS/CSS from `.alabjs/dist/client/`) and files
|
|
126
|
+
// from the project's `public/` directory. Dynamic alab routes take priority
|
|
127
|
+
// via the router registered below; this handler only fires for real files.
|
|
128
|
+
const clientDir = resolve(distDir, "client");
|
|
129
|
+
const MIME_TYPES: Record<string, string> = {
|
|
130
|
+
".js": "application/javascript; charset=utf-8",
|
|
131
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
132
|
+
".css": "text/css; charset=utf-8",
|
|
133
|
+
".html": "text/html; charset=utf-8",
|
|
134
|
+
".json": "application/json; charset=utf-8",
|
|
135
|
+
".svg": "image/svg+xml",
|
|
136
|
+
".png": "image/png",
|
|
137
|
+
".jpg": "image/jpeg",
|
|
138
|
+
".jpeg": "image/jpeg",
|
|
139
|
+
".gif": "image/gif",
|
|
140
|
+
".webp": "image/webp",
|
|
141
|
+
".ico": "image/x-icon",
|
|
142
|
+
".woff": "font/woff",
|
|
143
|
+
".woff2": "font/woff2",
|
|
144
|
+
".ttf": "font/ttf",
|
|
145
|
+
".txt": "text/plain; charset=utf-8",
|
|
146
|
+
".xml": "application/xml; charset=utf-8",
|
|
147
|
+
".map": "application/json; charset=utf-8",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
app.use(
|
|
151
|
+
defineEventHandler((event) => {
|
|
152
|
+
const req = event.node.req;
|
|
153
|
+
const res = event.node.res;
|
|
154
|
+
if (req.method !== "GET" && req.method !== "HEAD") return;
|
|
155
|
+
|
|
156
|
+
const rawPath = (req.url ?? "/").split("?")[0] ?? "/";
|
|
157
|
+
// Decode and strip traversal attempts
|
|
158
|
+
let relPath: string;
|
|
159
|
+
try { relPath = decodeURIComponent(rawPath); } catch { return; }
|
|
160
|
+
if (relPath.includes("..")) return;
|
|
161
|
+
|
|
162
|
+
const ext = extname(relPath).toLowerCase();
|
|
163
|
+
const contentType = MIME_TYPES[ext];
|
|
164
|
+
if (!contentType) return; // skip extensionless paths (page routes)
|
|
165
|
+
|
|
166
|
+
// 1. Built client assets (JS chunks, CSS, source maps)
|
|
167
|
+
const clientCandidate = join(clientDir, relPath);
|
|
168
|
+
if (existsSync(clientCandidate)) {
|
|
169
|
+
const stat = statSync(clientCandidate);
|
|
170
|
+
if (stat.isFile()) {
|
|
171
|
+
res.setHeader("content-type", contentType);
|
|
172
|
+
res.setHeader("content-length", stat.size);
|
|
173
|
+
// Immutable cache for hashed assets; short TTL for others
|
|
174
|
+
const isHashed = /\.[a-f0-9]{8,}\.[a-z]+$/.test(relPath);
|
|
175
|
+
res.setHeader("cache-control", isHashed
|
|
176
|
+
? "public, max-age=31536000, immutable"
|
|
177
|
+
: "public, max-age=3600");
|
|
178
|
+
if (req.method === "HEAD") { res.end(); return null; }
|
|
179
|
+
createReadStream(clientCandidate).pipe(res);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Public directory (favicons, fonts, open-graph images, etc.)
|
|
185
|
+
const publicCandidate = join(publicDir, relPath);
|
|
186
|
+
if (existsSync(publicCandidate)) {
|
|
187
|
+
const stat = statSync(publicCandidate);
|
|
188
|
+
if (stat.isFile()) {
|
|
189
|
+
res.setHeader("content-type", contentType);
|
|
190
|
+
res.setHeader("content-length", stat.size);
|
|
191
|
+
res.setHeader("cache-control", "public, max-age=3600");
|
|
192
|
+
if (req.method === "HEAD") { res.end(); return null; }
|
|
193
|
+
createReadStream(publicCandidate).pipe(res);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// ─── Built-in routes ────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
// Rust-powered image optimisation — resize + JPEG encode via `alab-napi`
|
|
204
|
+
router.get(
|
|
205
|
+
"/_alabjs/image",
|
|
206
|
+
defineEventHandler((event) => {
|
|
207
|
+
return handleImageRequest(event.node.req, event.node.res, publicDir);
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// On-demand ISR revalidation
|
|
212
|
+
router.post(
|
|
213
|
+
"/_alabjs/revalidate",
|
|
214
|
+
defineEventHandler(async (event) => {
|
|
215
|
+
const res = event.node.res;
|
|
216
|
+
res.setHeader("content-type", "application/json");
|
|
217
|
+
|
|
218
|
+
if (!checkRevalidateAuth(event.node.req.headers["authorization"])) {
|
|
219
|
+
res.statusCode = 401;
|
|
220
|
+
return JSON.stringify({ error: "Unauthorized. Set Authorization: Bearer <ALAB_REVALIDATE_SECRET>." });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const body = await readBody(event);
|
|
224
|
+
const result = applyRevalidate(body);
|
|
225
|
+
if ("error" in result) {
|
|
226
|
+
res.statusCode = result.status;
|
|
227
|
+
return JSON.stringify({ error: result.error });
|
|
228
|
+
}
|
|
229
|
+
return JSON.stringify(result);
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Auto sitemap.xml from route manifest
|
|
234
|
+
router.get(
|
|
235
|
+
"/sitemap.xml",
|
|
236
|
+
defineEventHandler((event) => {
|
|
237
|
+
const baseUrl =
|
|
238
|
+
process.env["PUBLIC_URL"] ??
|
|
239
|
+
`http://localhost:${process.env["PORT"] ?? "3000"}`;
|
|
240
|
+
const xml = generateSitemap(manifest.routes, baseUrl);
|
|
241
|
+
event.node.res.setHeader("content-type", "application/xml; charset=utf-8");
|
|
242
|
+
event.node.res.setHeader("cache-control", "public, max-age=3600");
|
|
243
|
+
return xml;
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ─── API routes (route.ts) ──────────────────────────────────────────────────
|
|
248
|
+
for (const route of manifest.routes) {
|
|
249
|
+
if (route.kind !== "api") continue;
|
|
250
|
+
|
|
251
|
+
const h3ApiPath = route.path.replace(/\[([^\]]+)\]/g, ":$1");
|
|
252
|
+
const apiModulePath = `${distDir}/server/${route.file}`;
|
|
253
|
+
|
|
254
|
+
for (const method of ["get", "post", "put", "patch", "delete", "head"] as const) {
|
|
255
|
+
router[method](
|
|
256
|
+
h3ApiPath,
|
|
257
|
+
defineEventHandler(async (event) => {
|
|
258
|
+
const apiMod = await import(apiModulePath) as Record<string, unknown>;
|
|
259
|
+
const handler = apiMod[method.toUpperCase()];
|
|
260
|
+
if (typeof handler !== "function") {
|
|
261
|
+
event.node.res.statusCode = 405;
|
|
262
|
+
const allowed = ["GET","POST","PUT","PATCH","DELETE","HEAD"].filter(m => typeof apiMod[m] === "function").join(", ");
|
|
263
|
+
event.node.res.setHeader("allow", allowed);
|
|
264
|
+
event.node.res.end("Method Not Allowed");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const req = event.node.req;
|
|
268
|
+
const chunks: Buffer[] = [];
|
|
269
|
+
await new Promise<void>((ok) => {
|
|
270
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
271
|
+
req.on("end", ok);
|
|
272
|
+
});
|
|
273
|
+
const body = chunks.length ? Buffer.concat(chunks) : null;
|
|
274
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
275
|
+
const webReq = new Request(url.toString(), {
|
|
276
|
+
method: method.toUpperCase(),
|
|
277
|
+
headers: req.headers as HeadersInit,
|
|
278
|
+
body: body?.length ? body : null,
|
|
279
|
+
});
|
|
280
|
+
const webRes = await (handler as (r: Request) => Promise<Response>)(webReq);
|
|
281
|
+
const nodeRes = event.node.res;
|
|
282
|
+
nodeRes.statusCode = webRes.status;
|
|
283
|
+
webRes.headers.forEach((v, k) => nodeRes.setHeader(k, v));
|
|
284
|
+
|
|
285
|
+
// SSE: pipe the ReadableStream body without buffering.
|
|
286
|
+
if (
|
|
287
|
+
(webRes.headers.get("content-type") ?? "").startsWith("text/event-stream") &&
|
|
288
|
+
webRes.body
|
|
289
|
+
) {
|
|
290
|
+
const reader = webRes.body.getReader();
|
|
291
|
+
nodeRes.on("close", () => { void reader.cancel(); });
|
|
292
|
+
try {
|
|
293
|
+
while (true) {
|
|
294
|
+
const { done, value } = await reader.read();
|
|
295
|
+
if (done || nodeRes.destroyed) break;
|
|
296
|
+
nodeRes.write(value);
|
|
297
|
+
}
|
|
298
|
+
} catch { /* client disconnected */ } finally {
|
|
299
|
+
nodeRes.end();
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
nodeRes.end(Buffer.from(await webRes.arrayBuffer()));
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Page routes ────────────────────────────────────────────────────────────
|
|
311
|
+
for (const route of manifest.routes) {
|
|
312
|
+
if (route.kind !== "page") continue;
|
|
313
|
+
|
|
314
|
+
// Convert Alab path pattern `/users/[id]` → H3 pattern `/users/:id`
|
|
315
|
+
const h3Path = route.path.replace(/\[([^\]]+)\]/g, ":$1");
|
|
316
|
+
|
|
317
|
+
router.get(
|
|
318
|
+
h3Path,
|
|
319
|
+
defineEventHandler(async (event) => {
|
|
320
|
+
const res = event.node.res;
|
|
321
|
+
|
|
322
|
+
// HTML pages must not be cached by intermediaries — they contain
|
|
323
|
+
// user-specific CSRF tokens and may reflect auth state.
|
|
324
|
+
res.setHeader("cache-control", "no-store");
|
|
325
|
+
|
|
326
|
+
// Set CSRF cookie so the client can send it on mutations.
|
|
327
|
+
const csrfToken = setCsrfCookie(event);
|
|
328
|
+
|
|
329
|
+
const rawParams = (event.context.params ?? {}) as Record<string, string>;
|
|
330
|
+
const params = rawParams;
|
|
331
|
+
|
|
332
|
+
const rawQuery = getQuery(event) as Record<string, string | string[]>;
|
|
333
|
+
const searchParams: Record<string, string> = {};
|
|
334
|
+
for (const [k, v] of Object.entries(rawQuery)) {
|
|
335
|
+
searchParams[k] = Array.isArray(v) ? v[0] ?? "" : v;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Dynamically import the compiled page module from the dist directory.
|
|
339
|
+
const pageModulePath = `${distDir}/server/${route.file}`;
|
|
340
|
+
const mod = await import(pageModulePath) as {
|
|
341
|
+
default?: unknown;
|
|
342
|
+
metadata?: PageMetadata;
|
|
343
|
+
generateMetadata?: (params: Record<string, string>) => PageMetadata | Promise<PageMetadata>;
|
|
344
|
+
ssr?: boolean;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const Page = mod.default;
|
|
348
|
+
if (typeof Page !== "function") {
|
|
349
|
+
res.statusCode = 500;
|
|
350
|
+
res.end(`[alabjs] Page has no default export: ${route.file}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Support both static metadata and dynamic generateMetadata (production fix)
|
|
355
|
+
const metadata: PageMetadata =
|
|
356
|
+
typeof mod.generateMetadata === "function"
|
|
357
|
+
? await mod.generateMetadata(params)
|
|
358
|
+
: (mod.metadata ?? {});
|
|
359
|
+
|
|
360
|
+
const ssrEnabled = mod.ssr === true;
|
|
361
|
+
|
|
362
|
+
// ── Layouts ──────────────────────────────────────────────────────────
|
|
363
|
+
const layoutRelPaths = findProdLayoutFiles(route.file, distDir);
|
|
364
|
+
const layoutMods = await Promise.all(
|
|
365
|
+
layoutRelPaths.map((p) => import(`${distDir}/server/${p}`)),
|
|
366
|
+
);
|
|
367
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
368
|
+
const layouts = layoutMods.map((m: any) => m.default).filter((c: unknown): c is any => typeof c === "function");
|
|
369
|
+
const layoutsJson = JSON.stringify(layoutRelPaths);
|
|
370
|
+
const loadingFile = findProdLoadingFile(route.file, distDir) ?? undefined;
|
|
371
|
+
|
|
372
|
+
// Inject CSRF token into the HTML head so client JS can read it.
|
|
373
|
+
const headExtra = `<meta name="csrf-token" content="${csrfToken.replace(/"/g, """)}" />`;
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
renderToResponse(res, {
|
|
377
|
+
Page: Page as Parameters<typeof renderToResponse>[1]["Page"],
|
|
378
|
+
layouts,
|
|
379
|
+
params,
|
|
380
|
+
searchParams,
|
|
381
|
+
metadata,
|
|
382
|
+
routeFile: route.file,
|
|
383
|
+
layoutsJson,
|
|
384
|
+
loadingFile,
|
|
385
|
+
ssr: ssrEnabled,
|
|
386
|
+
headExtra,
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
// ── error.tsx fallback ────────────────────────────────────────────
|
|
390
|
+
const errorRelPath = findProdErrorFile(route.file, distDir);
|
|
391
|
+
if (errorRelPath) {
|
|
392
|
+
try {
|
|
393
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
394
|
+
const errorMod = await import(`${distDir}/server/${errorRelPath}`) as any;
|
|
395
|
+
const ErrorPage = errorMod.default;
|
|
396
|
+
if (typeof ErrorPage === "function") {
|
|
397
|
+
renderToResponse(res, {
|
|
398
|
+
Page: ErrorPage as Parameters<typeof renderToResponse>[1]["Page"],
|
|
399
|
+
params,
|
|
400
|
+
searchParams,
|
|
401
|
+
metadata: {},
|
|
402
|
+
routeFile: errorRelPath,
|
|
403
|
+
ssr: true,
|
|
404
|
+
headExtra,
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
} catch (fallbackErr) {
|
|
409
|
+
console.warn(`[alabjs] error.tsx fallback also failed for ${route.file}:`, fallbackErr);
|
|
410
|
+
// fall through to plain error
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
console.error(`[alabjs] render error in ${route.file}:`, err);
|
|
415
|
+
res.statusCode = 500;
|
|
416
|
+
res.end(`[alabjs] Render error in ${route.file}: ${msg}`);
|
|
417
|
+
}
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
app.use(router);
|
|
423
|
+
|
|
424
|
+
// ─── 404 / not-found fallback ────────────────────────────────────────────────
|
|
425
|
+
const notFoundPath = `${distDir}/server/app/not-found.tsx`;
|
|
426
|
+
app.use(
|
|
427
|
+
defineEventHandler(async (event) => {
|
|
428
|
+
const res = event.node.res;
|
|
429
|
+
res.statusCode = 404;
|
|
430
|
+
|
|
431
|
+
if (existsSync(notFoundPath)) {
|
|
432
|
+
try {
|
|
433
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
434
|
+
const nfMod = await import(notFoundPath) as any;
|
|
435
|
+
const NotFound = nfMod.default;
|
|
436
|
+
if (typeof NotFound === "function") {
|
|
437
|
+
renderToResponse(res, {
|
|
438
|
+
Page: NotFound as Parameters<typeof renderToResponse>[1]["Page"],
|
|
439
|
+
params: {},
|
|
440
|
+
searchParams: {},
|
|
441
|
+
metadata: { title: "404 — Not Found" },
|
|
442
|
+
routeFile: "app/not-found.tsx",
|
|
443
|
+
ssr: true,
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
} catch (notFoundErr) {
|
|
448
|
+
console.warn("[alabjs] not-found.tsx render failed:", notFoundErr);
|
|
449
|
+
// fall through to plain text 404
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
454
|
+
res.end("404 Not Found");
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
listen(port = 3000) {
|
|
460
|
+
const server = createServer(toNodeListener(app));
|
|
461
|
+
server.listen(port, () => {
|
|
462
|
+
console.log(` alab ready at http://localhost:${port}`);
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getCached,
|
|
4
|
+
setCache,
|
|
5
|
+
invalidateCache,
|
|
6
|
+
invalidateCacheKey,
|
|
7
|
+
CACHE_MISS,
|
|
8
|
+
getCachedPage,
|
|
9
|
+
setCachedPage,
|
|
10
|
+
markPageRevalidating,
|
|
11
|
+
isPageRevalidating,
|
|
12
|
+
revalidatePath,
|
|
13
|
+
revalidatePathPrefix,
|
|
14
|
+
revalidateTag,
|
|
15
|
+
inspectCache,
|
|
16
|
+
} from "./cache.js";
|
|
17
|
+
|
|
18
|
+
// ─── Server-function cache ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("server-function cache", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Clear cache between tests by invalidating everything
|
|
23
|
+
invalidateCacheKey("test-key");
|
|
24
|
+
invalidateCacheKey("a");
|
|
25
|
+
invalidateCacheKey("b");
|
|
26
|
+
invalidateCacheKey("c");
|
|
27
|
+
invalidateCacheKey("tagged");
|
|
28
|
+
invalidateCacheKey("other");
|
|
29
|
+
invalidateCacheKey("expired");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns CACHE_MISS for unknown keys", () => {
|
|
33
|
+
expect(getCached("nonexistent")).toBe(CACHE_MISS);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("stores and retrieves values", () => {
|
|
37
|
+
setCache("test-key", { name: "Ada" }, { ttl: 60 });
|
|
38
|
+
expect(getCached("test-key")).toEqual({ name: "Ada" });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("stores primitive values", () => {
|
|
42
|
+
setCache("a", 42, { ttl: 60 });
|
|
43
|
+
setCache("b", "hello", { ttl: 60 });
|
|
44
|
+
setCache("c", null, { ttl: 60 });
|
|
45
|
+
expect(getCached("a")).toBe(42);
|
|
46
|
+
expect(getCached("b")).toBe("hello");
|
|
47
|
+
expect(getCached("c")).toBe(null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns CACHE_MISS for expired entries", () => {
|
|
51
|
+
// Set with 0 second TTL (expired immediately)
|
|
52
|
+
setCache("expired", "old", { ttl: 0 });
|
|
53
|
+
// Advance time slightly
|
|
54
|
+
vi.useFakeTimers();
|
|
55
|
+
vi.advanceTimersByTime(1);
|
|
56
|
+
expect(getCached("expired")).toBe(CACHE_MISS);
|
|
57
|
+
vi.useRealTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("invalidates by tag", () => {
|
|
61
|
+
setCache("tagged", { id: 1 }, { ttl: 60, tags: ["posts", "post:1"] });
|
|
62
|
+
setCache("other", { id: 2 }, { ttl: 60, tags: ["users"] });
|
|
63
|
+
|
|
64
|
+
invalidateCache({ tags: ["posts"] });
|
|
65
|
+
|
|
66
|
+
expect(getCached("tagged")).toBe(CACHE_MISS);
|
|
67
|
+
expect(getCached("other")).not.toBe(CACHE_MISS);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("invalidates by exact key", () => {
|
|
71
|
+
setCache("a", 1, { ttl: 60 });
|
|
72
|
+
invalidateCacheKey("a");
|
|
73
|
+
expect(getCached("a")).toBe(CACHE_MISS);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("revalidateTag is an alias for invalidateCache", () => {
|
|
77
|
+
setCache("tagged", "data", { ttl: 60, tags: ["t1"] });
|
|
78
|
+
revalidateTag({ tags: ["t1"] });
|
|
79
|
+
expect(getCached("tagged")).toBe(CACHE_MISS);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("inspectCache returns live entries", () => {
|
|
83
|
+
setCache("a", 1, { ttl: 60, tags: ["x"] });
|
|
84
|
+
setCache("b", 2, { ttl: 120 });
|
|
85
|
+
const snapshot = inspectCache();
|
|
86
|
+
expect(snapshot.length).toBeGreaterThanOrEqual(2);
|
|
87
|
+
const aEntry = snapshot.find((e) => e.key === "a");
|
|
88
|
+
expect(aEntry).toBeDefined();
|
|
89
|
+
expect(aEntry!.tags).toContain("x");
|
|
90
|
+
expect(aEntry!.expiresIn).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("inspectCache filters out expired entries", () => {
|
|
94
|
+
setCache("expired", "data", { ttl: 0 });
|
|
95
|
+
vi.useFakeTimers();
|
|
96
|
+
vi.advanceTimersByTime(1);
|
|
97
|
+
const snapshot = inspectCache();
|
|
98
|
+
expect(snapshot.find((e) => e.key === "expired")).toBeUndefined();
|
|
99
|
+
vi.useRealTimers();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── Page cache (ISR) ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe("page cache (ISR)", () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
revalidatePath("/test");
|
|
108
|
+
revalidatePath("/a");
|
|
109
|
+
revalidatePath("/a/1");
|
|
110
|
+
revalidatePath("/a/2");
|
|
111
|
+
revalidatePath("/b");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns null for uncached pages", () => {
|
|
115
|
+
expect(getCachedPage("/test")).toBe(null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("stores and retrieves page HTML", () => {
|
|
119
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
120
|
+
const result = getCachedPage("/test");
|
|
121
|
+
expect(result).not.toBe(null);
|
|
122
|
+
expect(result!.html).toBe("<html>test</html>");
|
|
123
|
+
expect(result!.stale).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns stale entry after TTL expires (stale-while-revalidate)", () => {
|
|
127
|
+
vi.useFakeTimers();
|
|
128
|
+
setCachedPage("/test", "<html>stale</html>", 10);
|
|
129
|
+
// Advance past the TTL
|
|
130
|
+
vi.advanceTimersByTime(11_000);
|
|
131
|
+
const result = getCachedPage("/test");
|
|
132
|
+
expect(result).not.toBe(null);
|
|
133
|
+
expect(result!.stale).toBe(true);
|
|
134
|
+
vi.useRealTimers();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("markPageRevalidating and isPageRevalidating", () => {
|
|
138
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
139
|
+
expect(isPageRevalidating("/test")).toBe(false);
|
|
140
|
+
markPageRevalidating("/test");
|
|
141
|
+
expect(isPageRevalidating("/test")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("isPageRevalidating returns false for uncached pages", () => {
|
|
145
|
+
expect(isPageRevalidating("/nonexistent")).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("revalidatePath removes the cached page", () => {
|
|
149
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
150
|
+
revalidatePath("/test");
|
|
151
|
+
expect(getCachedPage("/test")).toBe(null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("revalidatePathPrefix removes matching pages", () => {
|
|
155
|
+
setCachedPage("/a", "<html>a</html>", 60);
|
|
156
|
+
setCachedPage("/a/1", "<html>a1</html>", 60);
|
|
157
|
+
setCachedPage("/a/2", "<html>a2</html>", 60);
|
|
158
|
+
setCachedPage("/b", "<html>b</html>", 60);
|
|
159
|
+
|
|
160
|
+
revalidatePathPrefix("/a");
|
|
161
|
+
|
|
162
|
+
expect(getCachedPage("/a")).toBe(null);
|
|
163
|
+
expect(getCachedPage("/a/1")).toBe(null);
|
|
164
|
+
expect(getCachedPage("/a/2")).toBe(null);
|
|
165
|
+
expect(getCachedPage("/b")).not.toBe(null);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("revalidateTag purges page HTML entries carrying a matching tag", () => {
|
|
169
|
+
setCachedPage("/posts", "<html>posts</html>", 60, ["posts"]);
|
|
170
|
+
setCachedPage("/posts/1", "<html>post 1</html>", 60, ["posts", "post:1"]);
|
|
171
|
+
setCachedPage("/about", "<html>about</html>", 60, ["static"]);
|
|
172
|
+
|
|
173
|
+
revalidateTag({ tags: ["posts"] });
|
|
174
|
+
|
|
175
|
+
expect(getCachedPage("/posts")).toBe(null);
|
|
176
|
+
expect(getCachedPage("/posts/1")).toBe(null);
|
|
177
|
+
expect(getCachedPage("/about")).not.toBe(null);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("revalidateTag leaves page HTML entries with no matching tag intact", () => {
|
|
181
|
+
setCachedPage("/home", "<html>home</html>", 60, ["home"]);
|
|
182
|
+
revalidateTag({ tags: ["posts"] });
|
|
183
|
+
expect(getCachedPage("/home")).not.toBe(null);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("setCachedPage stores tags and they survive a fresh read", () => {
|
|
187
|
+
setCachedPage("/tagged", "<html>tagged</html>", 60, ["t1", "t2"]);
|
|
188
|
+
expect(getCachedPage("/tagged")).not.toBe(null);
|
|
189
|
+
revalidateTag({ tags: ["t2"] });
|
|
190
|
+
expect(getCachedPage("/tagged")).toBe(null);
|
|
191
|
+
});
|
|
192
|
+
});
|