@anaemia/core 0.4.0 → 0.5.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.
Files changed (74) hide show
  1. package/dist/runtime/context.d.ts.map +1 -1
  2. package/dist/runtime/context.js +4 -3
  3. package/dist/runtime/entry-client.jsx +2 -1
  4. package/dist/runtime/entry-server.d.ts +1 -2
  5. package/dist/runtime/entry-server.d.ts.map +1 -1
  6. package/dist/runtime/entry-server.jsx +18 -331
  7. package/dist/runtime/resources.d.ts.map +1 -1
  8. package/dist/runtime/resources.js +2 -1
  9. package/dist/runtime/route-data.d.ts.map +1 -1
  10. package/dist/runtime/route-data.js +4 -3
  11. package/dist/runtime/route-request.d.ts.map +1 -1
  12. package/dist/runtime/route-request.js +2 -1
  13. package/dist/runtime/rpc-client.d.ts.map +1 -1
  14. package/dist/runtime/rpc-client.js +7 -6
  15. package/dist/runtime/server/app.d.ts +22 -0
  16. package/dist/runtime/server/app.d.ts.map +1 -0
  17. package/dist/runtime/server/app.js +21 -0
  18. package/dist/runtime/server/assets.d.ts +5 -0
  19. package/dist/runtime/server/assets.d.ts.map +1 -0
  20. package/dist/runtime/server/assets.js +48 -0
  21. package/dist/runtime/server/boot.d.ts +4 -0
  22. package/dist/runtime/server/boot.d.ts.map +1 -0
  23. package/dist/runtime/server/boot.js +6 -0
  24. package/dist/runtime/server/env.d.ts +3 -0
  25. package/dist/runtime/server/env.d.ts.map +1 -0
  26. package/dist/runtime/server/env.js +14 -0
  27. package/dist/runtime/server/guards.d.ts +29 -0
  28. package/dist/runtime/server/guards.d.ts.map +1 -0
  29. package/dist/runtime/server/guards.js +12 -0
  30. package/dist/runtime/server/html.d.ts +15 -0
  31. package/dist/runtime/server/html.d.ts.map +1 -0
  32. package/dist/runtime/server/html.js +62 -0
  33. package/dist/runtime/server/hydration.d.ts +3 -0
  34. package/dist/runtime/server/hydration.d.ts.map +1 -0
  35. package/dist/runtime/server/hydration.js +20 -0
  36. package/dist/runtime/server/manifest.d.ts +14 -0
  37. package/dist/runtime/server/manifest.d.ts.map +1 -0
  38. package/dist/runtime/server/manifest.js +59 -0
  39. package/dist/runtime/server/render-request.d.ts +21 -0
  40. package/dist/runtime/server/render-request.d.ts.map +1 -0
  41. package/dist/runtime/server/render-request.jsx +170 -0
  42. package/dist/runtime/server/route-match.d.ts +14 -0
  43. package/dist/runtime/server/route-match.d.ts.map +1 -0
  44. package/dist/runtime/server/route-match.js +35 -0
  45. package/dist/runtime/server/rpc.d.ts +3 -0
  46. package/dist/runtime/server/rpc.d.ts.map +1 -0
  47. package/dist/runtime/server/rpc.js +32 -0
  48. package/dist/runtime/server/types.d.ts +33 -0
  49. package/dist/runtime/server/types.d.ts.map +1 -0
  50. package/dist/runtime/server/types.js +1 -0
  51. package/dist/runtime/shared/constants.d.ts +8 -0
  52. package/dist/runtime/shared/constants.d.ts.map +1 -0
  53. package/dist/runtime/shared/constants.js +7 -0
  54. package/package.json +5 -2
  55. package/src/runtime/context.ts +4 -6
  56. package/src/runtime/entry-client.tsx +2 -1
  57. package/src/runtime/entry-server.tsx +19 -397
  58. package/src/runtime/resources.ts +2 -1
  59. package/src/runtime/route-data.ts +5 -4
  60. package/src/runtime/route-request.ts +2 -1
  61. package/src/runtime/rpc-client.ts +8 -7
  62. package/src/runtime/server/app.ts +44 -0
  63. package/src/runtime/server/assets.ts +69 -0
  64. package/src/runtime/server/boot.ts +9 -0
  65. package/src/runtime/server/env.ts +17 -0
  66. package/src/runtime/server/guards.ts +26 -0
  67. package/src/runtime/server/html.ts +84 -0
  68. package/src/runtime/server/hydration.ts +26 -0
  69. package/src/runtime/server/manifest.ts +69 -0
  70. package/src/runtime/server/render-request.tsx +230 -0
  71. package/src/runtime/server/route-match.ts +45 -0
  72. package/src/runtime/server/rpc.ts +34 -0
  73. package/src/runtime/server/types.ts +36 -0
  74. package/src/runtime/shared/constants.ts +7 -0
@@ -0,0 +1,69 @@
1
+ import type { Context, Hono } from "hono";
2
+ import { serveStatic } from "@hono/node-server/serve-static";
3
+ import type { RuntimeEnv } from "./types.js";
4
+
5
+ function setNoCacheHeaders(c: Context) {
6
+ c.header("Cache-Control", "no-cache, no-store, must-revalidate");
7
+ c.header("Pragma", "no-cache");
8
+ c.header("Expires", "0");
9
+ }
10
+
11
+ async function proxyDevAsset(c: Context, targetUrl: string, notFoundMessage: string, failureMessage: string) {
12
+ try {
13
+ const response = await fetch(targetUrl);
14
+ if (!response.ok) return c.text(notFoundMessage, 404);
15
+
16
+ const contentType = response.headers.get("content-type");
17
+ if (contentType) c.header("content-type", contentType);
18
+
19
+ setNoCacheHeaders(c);
20
+
21
+ return c.body(await response.arrayBuffer());
22
+ } catch {
23
+ return c.text(failureMessage, 500);
24
+ }
25
+ }
26
+
27
+ export function registerAssetRoutes(app: Hono, env: RuntimeEnv) {
28
+ if (env.isDev) {
29
+ app.get("/assets/*", (c) =>
30
+ proxyDevAsset(
31
+ c,
32
+ `${env.devServerUrl}${c.req.path}`,
33
+ "asset not found in Rspack memory",
34
+ "failed to connect to Rspack dev server asset bridge",
35
+ ),
36
+ );
37
+ } else {
38
+ app.use("/assets/*", async (c, next) => {
39
+ await next();
40
+ if (c.res.ok) c.res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
41
+ });
42
+
43
+ app.use(
44
+ "/assets/*",
45
+ serveStatic({
46
+ root: env.clientDistPath,
47
+ }),
48
+ );
49
+ }
50
+
51
+ app.use(async (c, next) => {
52
+ const requestPath = c.req.path;
53
+ if (env.isDev && requestPath.includes(".hot-update.")) {
54
+ return proxyDevAsset(
55
+ c,
56
+ `${env.devServerUrl}${requestPath}`,
57
+ "hot update not found",
58
+ "failed to fetch hot update",
59
+ );
60
+ }
61
+
62
+ await next();
63
+ });
64
+ }
65
+
66
+ export function setDevResponseCacheHeaders(c: Context, env: RuntimeEnv) {
67
+ if (!env.isDev) return;
68
+ setNoCacheHeaders(c);
69
+ }
@@ -0,0 +1,9 @@
1
+ import { serve } from "@hono/node-server";
2
+ import type { Hono } from "hono";
3
+ import type { RuntimeEnv } from "./types.js";
4
+
5
+ export function serveServer(app: Hono, env: RuntimeEnv) {
6
+ serve({ fetch: app.fetch, port: env.port }, (info) => {
7
+ console.log(`[anaemia framework] server live at http://localhost:${info.port}`);
8
+ });
9
+ }
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ import type { RuntimeEnv } from "./types.js";
3
+
4
+ export function createRuntimeEnv(processEnv: NodeJS.ProcessEnv = process.env): RuntimeEnv {
5
+ const port = Number(processEnv.PORT) || 3000;
6
+ const isDev = processEnv.NODE_ENV !== "production";
7
+ const devPort = Number(processEnv.RSPACK_DEV_PORT) || 4445;
8
+
9
+ return {
10
+ port,
11
+ isDev,
12
+ devServerUrl: `http://localhost:${devPort}`,
13
+ templatePath: path.resolve(process.cwd(), "./dist/client/index.html"),
14
+ manifestPath: path.resolve(process.cwd(), "./dist/route-manifest.json"),
15
+ clientDistPath: path.resolve(process.cwd(), "./dist/client"),
16
+ };
17
+ }
@@ -0,0 +1,26 @@
1
+ export type GuardFn = (ctx: {
2
+ params: Record<string, string>;
3
+ request: Request;
4
+ url: string;
5
+ }) =>
6
+ | void
7
+ | undefined
8
+ | { redirect: string; status?: 301 | 302 | 307 | 308 }
9
+ | { status: number; body?: string }
10
+ | Promise<void | undefined | { redirect: string; status?: number } | { status: number; body?: string }>;
11
+
12
+ export async function runGuards(
13
+ serverGuardRegistry: Map<string, (() => Promise<GuardFn[]>)[]>,
14
+ pattern: string,
15
+ ctx: { params: Record<string, string>; request: Request; url: string },
16
+ ) {
17
+ const chain = serverGuardRegistry.get(pattern) ?? [];
18
+ for (const loadGuards of chain) {
19
+ const guards = await loadGuards();
20
+ for (const guard of guards) {
21
+ const result = await guard(ctx);
22
+ if (result && ("redirect" in result || "status" in result)) return result;
23
+ }
24
+ }
25
+ return null;
26
+ }
@@ -0,0 +1,84 @@
1
+ import { ENTRY_ATTRIBUTE } from "../shared/constants.js";
2
+ import type { ChunkAssets, RouteManifest } from "./types.js";
3
+
4
+ const ENTRY_TAG_REGEX = /(<([a-zA-Z0-9-]+)[^>]*anaemia-entry[^>]*>)(.*?)(<\/\2>)/is;
5
+
6
+ function normalizeAssetUrl(url: unknown): string {
7
+ if (!url || typeof url !== "string") return "";
8
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
9
+ return url.startsWith("/") ? url : `/${url}`;
10
+ }
11
+
12
+ function chunkAssetTags(chunk: ChunkAssets | undefined): { scripts: string; styles: string } {
13
+ if (!chunk) return { scripts: "", styles: "" };
14
+
15
+ const scripts =
16
+ (chunk.js ?? "")
17
+ ? (Array.isArray(chunk.js) ? chunk.js : [chunk.js])
18
+ .map((jsFile) => `<script type="module" src="${normalizeAssetUrl(jsFile)}"></script>\n`)
19
+ .join("")
20
+ : "";
21
+
22
+ const styles =
23
+ (chunk.css ?? "")
24
+ ? (Array.isArray(chunk.css) ? chunk.css : [chunk.css])
25
+ .map((cssFile) => `<link rel="stylesheet" href="${normalizeAssetUrl(cssFile)}">\n`)
26
+ .join("")
27
+ : "";
28
+
29
+ return { scripts, styles };
30
+ }
31
+
32
+ export function getRouteAssetTags(manifest: RouteManifest, activeChunk: string): { scripts: string; styles: string } {
33
+ const chunkNames = ["client", "commons", "vendors"];
34
+ if (activeChunk && activeChunk !== "client") chunkNames.push(activeChunk);
35
+
36
+ return chunkNames.reduce(
37
+ (assetTags, chunkName) => {
38
+ const tags = chunkAssetTags(manifest.chunks[chunkName]);
39
+ return {
40
+ scripts: assetTags.scripts + tags.scripts,
41
+ styles: assetTags.styles + tags.styles,
42
+ };
43
+ },
44
+ { scripts: "", styles: "" },
45
+ );
46
+ }
47
+
48
+ export function createDevNoCacheHeadTags(isDev: boolean): string {
49
+ return isDev
50
+ ? `<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n<meta http-equiv="Pragma" content="no-cache">\n<meta http-equiv="Expires" content="0">\n`
51
+ : "";
52
+ }
53
+
54
+ export function createHtmlStreamShell(args: { template: string; headInjections: string; bodyInjections: string }): {
55
+ beforeEntry: string;
56
+ afterEntry: string;
57
+ } {
58
+ const templateWithHead = args.template.replace("<head>", `<head>${args.headInjections}`);
59
+ const entryMatch = ENTRY_TAG_REGEX.exec(templateWithHead);
60
+
61
+ if (entryMatch) {
62
+ const [fullMatch, openTag, _tagName, _inner, closeTag] = entryMatch;
63
+ const beforeEntry = `${templateWithHead.slice(0, entryMatch.index)}${openTag}`;
64
+ const afterEntry = `${closeTag}${templateWithHead.slice(entryMatch.index + fullMatch.length)}`.replace(
65
+ "</body>",
66
+ `${args.bodyInjections}</body>`,
67
+ );
68
+
69
+ return { beforeEntry, afterEntry };
70
+ }
71
+
72
+ const bodyCloseIndex = templateWithHead.lastIndexOf("</body>");
73
+ if (bodyCloseIndex >= 0) {
74
+ return {
75
+ beforeEntry: `${templateWithHead.slice(0, bodyCloseIndex)}<div ${ENTRY_ATTRIBUTE}>`,
76
+ afterEntry: `</div>${args.bodyInjections}${templateWithHead.slice(bodyCloseIndex)}`,
77
+ };
78
+ }
79
+
80
+ return {
81
+ beforeEntry: `${templateWithHead}<div ${ENTRY_ATTRIBUTE}>`,
82
+ afterEntry: `</div>${args.bodyInjections}`,
83
+ };
84
+ }
@@ -0,0 +1,26 @@
1
+ import { generateHydrationScript } from "solid-js/web";
2
+ import { ANAEMIA_DATA_SCRIPT_ID, LOADER_DATA_KEY, SERVER_FUNCTION_DATA_KEY } from "../shared/constants.js";
3
+
4
+ function serializeJsonForHtml(value: unknown): string {
5
+ return JSON.stringify(value)
6
+ .replace(/&/g, "\\u0026")
7
+ .replace(/</g, "\\u003c")
8
+ .replace(/>/g, "\\u003e")
9
+ .replace(/\//g, "\\u002f");
10
+ }
11
+
12
+ export function createHydrationDataScript(store: Map<string, unknown>): string {
13
+ const rawStorePayload = Object.fromEntries(store);
14
+ const finalHydrationStatePayload = {
15
+ [LOADER_DATA_KEY]: rawStorePayload[LOADER_DATA_KEY] || {},
16
+ [SERVER_FUNCTION_DATA_KEY]: rawStorePayload[SERVER_FUNCTION_DATA_KEY] || {},
17
+ };
18
+
19
+ return `<script id="${ANAEMIA_DATA_SCRIPT_ID}" type="application/json">${serializeJsonForHtml(
20
+ finalHydrationStatePayload,
21
+ )}</script>\n`;
22
+ }
23
+
24
+ export function createHydrationRuntimeScript(): string {
25
+ return generateHydrationScript();
26
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import type { RouteManifest, RuntimeEnv } from "./types.js";
3
+ import { sortRoutes } from "./route-match.js";
4
+
5
+ export type ManifestSnapshot = {
6
+ template: string;
7
+ manifest: RouteManifest | null;
8
+ sortedRoutes: RouteManifest["routes"];
9
+ staticRoutes: Set<string>; // routes safe to cache indefinitely
10
+ loaderRoutes: Set<string>; // routes that need loader execution
11
+ guardRoutes: Set<string>; // routes that need guard execution
12
+ };
13
+
14
+ export function createManifestStore(env: RuntimeEnv) {
15
+ let memoizedHtmlTemplate = "";
16
+ let memoizedManifest: RouteManifest | null = null;
17
+ let memoizedSortedRoutes: RouteManifest["routes"] = [];
18
+
19
+ const load = async () => {
20
+ if (env.isDev) {
21
+ try {
22
+ memoizedHtmlTemplate = await fetch(`${env.devServerUrl}/index.html`).then((response) => {
23
+ if (!response.ok) throw new Error(`index.html fetch failed: ${response.status}`);
24
+ return response.text();
25
+ });
26
+ } catch (err) {
27
+ console.error("[anaemia engine sync error - HTML fetch failed]:", err);
28
+ memoizedHtmlTemplate = "";
29
+ }
30
+
31
+ try {
32
+ if (fs.existsSync(env.manifestPath)) {
33
+ memoizedManifest = JSON.parse(fs.readFileSync(env.manifestPath, "utf-8")) as RouteManifest;
34
+ } else {
35
+ memoizedManifest = { routes: [], chunks: {}, errors: {} };
36
+ }
37
+ } catch (err) {
38
+ console.error("[anaemia engine sync error - manifest read failed]:", err);
39
+ memoizedManifest = { routes: [], chunks: {}, errors: {} };
40
+ }
41
+ } else {
42
+ try {
43
+ if (fs.existsSync(env.templatePath)) memoizedHtmlTemplate = fs.readFileSync(env.templatePath, "utf-8");
44
+ if (fs.existsSync(env.manifestPath)) {
45
+ memoizedManifest = JSON.parse(fs.readFileSync(env.manifestPath, "utf-8")) as RouteManifest;
46
+ }
47
+ } catch {
48
+ console.warn("build assets not fully initialized during bootstrapping cycle.");
49
+ }
50
+ }
51
+
52
+ memoizedSortedRoutes = memoizedManifest ? sortRoutes(memoizedManifest.routes) : [];
53
+ };
54
+
55
+ const getSnapshot = (): ManifestSnapshot => {
56
+ const routes = memoizedManifest?.routes ?? [];
57
+
58
+ return {
59
+ template: memoizedHtmlTemplate,
60
+ manifest: memoizedManifest,
61
+ sortedRoutes: memoizedSortedRoutes,
62
+ staticRoutes: new Set(routes.filter((r) => r.isStatic).map((r) => r.urlPattern)),
63
+ loaderRoutes: new Set(routes.filter((r) => r.hasLoader).map((r) => r.urlPattern)),
64
+ guardRoutes: new Set(routes.filter((r) => r.hasGuard).map((r) => r.urlPattern)),
65
+ };
66
+ };
67
+
68
+ return { load, getSnapshot };
69
+ }
@@ -0,0 +1,230 @@
1
+ import { Router } from "@solidjs/router";
2
+ import type { Component } from "solid-js";
3
+ import type { Context } from "hono";
4
+ import type { ContentfulStatusCode, RedirectStatusCode, StatusCode } from "hono/utils/http-status";
5
+ import { renderToStream } from "solid-js/web";
6
+ import { ssrStorage } from "../context.js";
7
+ import { LOADER_DATA_KEY } from "../shared/constants.js";
8
+ import { setDevResponseCacheHeaders } from "./assets.js";
9
+ import { runGuards, type GuardFn } from "./guards.js";
10
+ import { createDevNoCacheHeadTags, createHtmlStreamShell, getRouteAssetTags } from "./html.js";
11
+ import { createHydrationDataScript, createHydrationRuntimeScript } from "./hydration.js";
12
+ import { matchRoute } from "./route-match.js";
13
+ import type { RouteManifest, RuntimeEnv } from "./types.js";
14
+ import type { ManifestSnapshot } from "./manifest.js";
15
+
16
+ const staticCache = new Map<string, string>();
17
+
18
+ type ServerLoader = (args: { params: Record<string, string>; request: Request }) => unknown | Promise<unknown>;
19
+
20
+ type RenderRequestOptions = {
21
+ App: Component;
22
+ env: RuntimeEnv;
23
+ preloadActiveClientRoute: (path: string) => unknown | Promise<unknown>;
24
+ serverLoaderRegistry: Map<string, ServerLoader>;
25
+ serverGuardRegistry: Map<string, (() => Promise<GuardFn[]>)[]>;
26
+ getManifestSnapshot: () => ManifestSnapshot;
27
+ loadManifestAndTemplate: () => Promise<void>;
28
+ };
29
+
30
+ type SolidStream = ReturnType<typeof renderToStream>;
31
+
32
+ function renderApp(App: Component, url: string): SolidStream {
33
+ return renderToStream(() => (
34
+ <Router url={url}>
35
+ <App />
36
+ </Router>
37
+ ));
38
+ }
39
+
40
+ async function render500(args: {
41
+ App: Component;
42
+ error: unknown;
43
+ env: RuntimeEnv;
44
+ manifest: RouteManifest;
45
+ serverLoaderRegistry: Map<string, ServerLoader>;
46
+ store: Map<string, unknown>;
47
+ }): Promise<SolidStream | string> {
48
+ const error500Pattern = args.manifest.errors?.["500"];
49
+ if (!error500Pattern) {
50
+ const stack = args.error instanceof Error ? args.error.stack : String(args.error);
51
+ return `<h1>500 Internal Server Error</h1><pre>${args.env.isDev ? stack : ""}</pre>`;
52
+ }
53
+
54
+ const error500Loader = args.serverLoaderRegistry.get(error500Pattern);
55
+ if (error500Loader) {
56
+ const message = args.error instanceof Error ? args.error.message : String(args.error);
57
+ const stack = args.error instanceof Error ? args.error.stack : undefined;
58
+ args.store.set(LOADER_DATA_KEY, { message, stack: args.env.isDev ? stack : undefined });
59
+
60
+ try {
61
+ return await ssrStorage.run(args.store, async () => renderApp(args.App, error500Pattern));
62
+ } catch {
63
+ return `<h1>500 Internal Server Error</h1>`;
64
+ }
65
+ }
66
+
67
+ const stack = args.error instanceof Error ? args.error.stack : String(args.error);
68
+ return `<h1>500 Internal Server Error</h1><pre>${args.env.isDev ? stack : ""}</pre>`;
69
+ }
70
+
71
+ function createHtmlResponseStream(args: {
72
+ beforeEntry: string;
73
+ renderStream: SolidStream | string;
74
+ afterEntry: () => string;
75
+ store: Map<string, unknown>;
76
+ onComplete?: (html: string) => void; // called with full HTML when done
77
+ }): ReadableStream<Uint8Array> {
78
+ const encoder = new TextEncoder();
79
+ const collected: string[] = [];
80
+ const shouldCollect = Boolean(args.onComplete);
81
+
82
+ return new ReadableStream<Uint8Array>({
83
+ start(controller) {
84
+ const enqueue = (chunk: string) => {
85
+ if (shouldCollect) collected.push(chunk);
86
+ controller.enqueue(encoder.encode(chunk));
87
+ };
88
+
89
+ enqueue(args.beforeEntry);
90
+
91
+ if (typeof args.renderStream === "string") {
92
+ enqueue(args.renderStream);
93
+ const after = args.afterEntry();
94
+ enqueue(after);
95
+ args.onComplete?.(collected.join(""));
96
+ controller.close();
97
+ return;
98
+ }
99
+
100
+ const renderStream = args.renderStream;
101
+ const writable = new WritableStream<string>({
102
+ write(chunk) {
103
+ enqueue(chunk);
104
+ },
105
+ close() {
106
+ const after = args.afterEntry();
107
+ enqueue(after);
108
+ args.onComplete?.(collected.join(""));
109
+ controller.close();
110
+ },
111
+ abort(error) {
112
+ controller.error(error);
113
+ },
114
+ });
115
+
116
+ void ssrStorage
117
+ .run(args.store, async () => {
118
+ await renderStream.pipeTo(writable);
119
+ })
120
+ .catch((error) => {
121
+ controller.error(error);
122
+ });
123
+ },
124
+ });
125
+ }
126
+
127
+ export function createRenderRequestHandler(options: RenderRequestOptions) {
128
+ return async (c: Context) => {
129
+ if (options.env.isDev) await options.loadManifestAndTemplate();
130
+
131
+ const { template, manifest, sortedRoutes, staticRoutes, loaderRoutes, guardRoutes } = options.getManifestSnapshot();
132
+
133
+ if (!template || !manifest) {
134
+ return c.text("anaemia engine error: build assets are missing", 500);
135
+ }
136
+
137
+ const reqPath = c.req.path;
138
+ const {
139
+ activeChunk,
140
+ targetPattern,
141
+ statusCode: matchedStatus,
142
+ params,
143
+ } = matchRoute(manifest, reqPath, sortedRoutes);
144
+ let statusCode: StatusCode = matchedStatus;
145
+ const loaderArgs = { params, request: c.req.raw };
146
+
147
+ const store = ssrStorage.getStore() || new Map<string, unknown>();
148
+ let renderStream: SolidStream | string;
149
+
150
+ const isStaticRoute = !options.env.isDev && staticRoutes.has(targetPattern);
151
+
152
+ // serve from cache before doing any work
153
+ if (isStaticRoute) {
154
+ const cached = staticCache.get(reqPath);
155
+ if (cached) {
156
+ c.header("Content-Type", "text/html; charset=UTF-8");
157
+ c.header("X-Anaemia-Cache", "HIT");
158
+ return c.html(cached, statusCode as ContentfulStatusCode);
159
+ }
160
+ }
161
+
162
+ if (targetPattern && guardRoutes.has(targetPattern)) {
163
+ try {
164
+ const guardResult = await runGuards(options.serverGuardRegistry, targetPattern, {
165
+ params,
166
+ request: c.req.raw,
167
+ url: reqPath,
168
+ });
169
+ if (guardResult) {
170
+ if ("redirect" in guardResult) {
171
+ return c.redirect(guardResult.redirect, (guardResult.status ?? 302) as RedirectStatusCode);
172
+ }
173
+ if ("status" in guardResult) statusCode = guardResult.status as StatusCode;
174
+ }
175
+ } catch (err) {
176
+ console.error("[anaemia] guard threw unexpectedly:", err);
177
+ return c.text("Internal Server Error", 500);
178
+ }
179
+ }
180
+
181
+ try {
182
+ renderStream = await ssrStorage.run(store, async () => {
183
+ if (targetPattern && loaderRoutes.has(targetPattern)) {
184
+ const executableLoader = options.serverLoaderRegistry.get(targetPattern);
185
+ if (executableLoader) {
186
+ const initialLoaderData = await executableLoader(loaderArgs);
187
+ store.set(LOADER_DATA_KEY, initialLoaderData);
188
+ }
189
+ }
190
+
191
+ await options.preloadActiveClientRoute(reqPath);
192
+ return renderApp(options.App, reqPath);
193
+ });
194
+ } catch (err) {
195
+ statusCode = 500;
196
+ console.error("[anaemia framework] runtime execution crash handled:", err);
197
+ renderStream = await render500({
198
+ App: options.App,
199
+ error: err,
200
+ env: options.env,
201
+ manifest,
202
+ serverLoaderRegistry: options.serverLoaderRegistry,
203
+ store,
204
+ });
205
+ }
206
+
207
+ const routeAssetTags = getRouteAssetTags(manifest, activeChunk);
208
+ const headInjections = `${createDevNoCacheHeadTags(options.env.isDev)}${routeAssetTags.styles}${createHydrationRuntimeScript()}`;
209
+ const shell = createHtmlStreamShell({ template, headInjections, bodyInjections: "" });
210
+
211
+ setDevResponseCacheHeaders(c, options.env);
212
+ c.status(statusCode);
213
+ c.header("Content-Type", "text/html; charset=UTF-8");
214
+
215
+ return c.body(
216
+ createHtmlResponseStream({
217
+ beforeEntry: shell.beforeEntry,
218
+ renderStream,
219
+ afterEntry: () => {
220
+ const bodyInjections = `${createHydrationDataScript(store)}${routeAssetTags.scripts}`;
221
+ return shell.afterEntry.includes("</body>")
222
+ ? shell.afterEntry.replace("</body>", `${bodyInjections}</body>`)
223
+ : `${shell.afterEntry}${bodyInjections}`;
224
+ },
225
+ store,
226
+ onComplete: isStaticRoute ? (html) => staticCache.set(reqPath, html) : undefined,
227
+ }),
228
+ );
229
+ };
230
+ }
@@ -0,0 +1,45 @@
1
+ import type { RouteManifest, RouteMatch } from "./types.js";
2
+
3
+ type SortedRoute = RouteManifest["routes"][number];
4
+
5
+ function scoreRoutePattern(pattern: string): number {
6
+ const segments = pattern.split("/").filter(Boolean);
7
+ return segments.reduce((acc, segment) => {
8
+ if (segment.startsWith(":")) return acc - 1;
9
+ if (segment === "*" || segment.startsWith("*")) return acc - 2;
10
+ return acc;
11
+ }, segments.length * 10);
12
+ }
13
+
14
+ export function sortRoutes(routes: RouteManifest["routes"]): SortedRoute[] {
15
+ return [...routes].sort((a, b) => scoreRoutePattern(b.urlPattern) - scoreRoutePattern(a.urlPattern));
16
+ }
17
+
18
+ export function matchRoute(
19
+ manifest: RouteManifest,
20
+ reqPath: string,
21
+ sortedRoutes = sortRoutes(manifest.routes),
22
+ ): RouteMatch {
23
+ for (const route of sortedRoutes) {
24
+ const regexStr = route.urlPattern
25
+ .replace(/:([a-zA-Z0-9_-]+)/g, "(?<$1>[^/]+)")
26
+ .replace(/\*([a-zA-Z0-9_-]*)/g, "(?<catchall>.*)");
27
+
28
+ const match = new RegExp(`^${regexStr}$`).exec(reqPath);
29
+ if (match) {
30
+ return {
31
+ activeChunk: route.chunkName,
32
+ targetPattern: route.urlPattern,
33
+ statusCode: 200,
34
+ params: match.groups ? { ...match.groups } : {},
35
+ };
36
+ }
37
+ }
38
+
39
+ return {
40
+ activeChunk: "route-404",
41
+ targetPattern: manifest.errors?.["404"] || "",
42
+ statusCode: 404,
43
+ params: {},
44
+ };
45
+ }
@@ -0,0 +1,34 @@
1
+ import type { Hono } from "hono";
2
+ import { serverFunctionsRegistry } from "../context.js";
3
+ import { RPC_PATH } from "../shared/constants.js";
4
+
5
+ export function registerRpcRoute(app: Hono) {
6
+ app.post(RPC_PATH, async (c) => {
7
+ const functionId = c.req.query("id");
8
+ if (!functionId || !serverFunctionsRegistry.has(functionId)) {
9
+ return c.json({ error: "RPC function not found" }, 404);
10
+ }
11
+
12
+ const contentLength = Number(c.req.header("content-length") ?? 0);
13
+ if (contentLength > 512_000) {
14
+ return c.json({ error: "Payload too large" }, 413);
15
+ }
16
+
17
+ let argumentsArray: unknown[];
18
+ try {
19
+ const body = await c.req.json();
20
+ if (!Array.isArray(body)) throw new Error("Expected array");
21
+ argumentsArray = body;
22
+ } catch {
23
+ return c.json({ error: "Invalid request body" }, 400);
24
+ }
25
+
26
+ try {
27
+ const result = await serverFunctionsRegistry.get(functionId)!(...argumentsArray);
28
+ return c.json(result);
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : "Internal server error";
31
+ return c.json({ error: message }, 500);
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,36 @@
1
+ import type { StatusCode } from "hono/utils/http-status";
2
+
3
+ export interface ChunkAssets {
4
+ js?: string[];
5
+ css?: string[];
6
+ }
7
+
8
+ export type RouteManifest = {
9
+ routes: Array<{
10
+ urlPattern: string;
11
+ chunkName: string;
12
+ params: string[];
13
+ isStatic: boolean;
14
+ hasLoader: boolean;
15
+ hasGuard: boolean;
16
+ serverFunctionIds: string[];
17
+ }>;
18
+ chunks: Record<string, ChunkAssets>;
19
+ errors?: Record<string, string>;
20
+ };
21
+
22
+ export type RouteMatch = {
23
+ activeChunk: string;
24
+ targetPattern: string;
25
+ statusCode: StatusCode;
26
+ params: Record<string, string>;
27
+ };
28
+
29
+ export type RuntimeEnv = {
30
+ port: number;
31
+ isDev: boolean;
32
+ devServerUrl: string;
33
+ templatePath: string;
34
+ manifestPath: string;
35
+ clientDistPath: string;
36
+ };
@@ -0,0 +1,7 @@
1
+ export const ANAEMIA_DATA_SCRIPT_ID = "__ANAEMIA_DATA__";
2
+ export const ENTRY_SELECTOR = "[anaemia-entry]";
3
+ export const ENTRY_ATTRIBUTE = "anaemia-entry";
4
+ export const HONO_CONTEXT_KEY = "honoContext";
5
+ export const LOADER_DATA_KEY = "__LOADER_DATA__";
6
+ export const RPC_PATH = "/_rpc";
7
+ export const SERVER_FUNCTION_DATA_KEY = "__SERVER_FUNCTION_DATA__";