@anaemia/core 0.3.7 → 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 (94) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/config.d.ts +8 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/plugins/lightningcss.js +4 -4
  8. package/dist/runtime/context.d.ts.map +1 -1
  9. package/dist/runtime/context.js +5 -4
  10. package/dist/runtime/entry-client.jsx +4 -5
  11. package/dist/runtime/entry-server.d.ts +1 -2
  12. package/dist/runtime/entry-server.d.ts.map +1 -1
  13. package/dist/runtime/entry-server.jsx +18 -321
  14. package/dist/runtime/resources.d.ts.map +1 -1
  15. package/dist/runtime/resources.js +2 -1
  16. package/dist/runtime/route-data.d.ts.map +1 -1
  17. package/dist/runtime/route-data.js +4 -3
  18. package/dist/runtime/route-request.d.ts.map +1 -1
  19. package/dist/runtime/route-request.js +3 -2
  20. package/dist/runtime/rpc-client.d.ts.map +1 -1
  21. package/dist/runtime/rpc-client.js +10 -9
  22. package/dist/runtime/server/app.d.ts +22 -0
  23. package/dist/runtime/server/app.d.ts.map +1 -0
  24. package/dist/runtime/server/app.js +21 -0
  25. package/dist/runtime/server/assets.d.ts +5 -0
  26. package/dist/runtime/server/assets.d.ts.map +1 -0
  27. package/dist/runtime/server/assets.js +48 -0
  28. package/dist/runtime/server/boot.d.ts +4 -0
  29. package/dist/runtime/server/boot.d.ts.map +1 -0
  30. package/dist/runtime/server/boot.js +6 -0
  31. package/dist/runtime/server/env.d.ts +3 -0
  32. package/dist/runtime/server/env.d.ts.map +1 -0
  33. package/dist/runtime/server/env.js +14 -0
  34. package/dist/runtime/server/guards.d.ts +29 -0
  35. package/dist/runtime/server/guards.d.ts.map +1 -0
  36. package/dist/runtime/server/guards.js +12 -0
  37. package/dist/runtime/server/html.d.ts +15 -0
  38. package/dist/runtime/server/html.d.ts.map +1 -0
  39. package/dist/runtime/server/html.js +62 -0
  40. package/dist/runtime/server/hydration.d.ts +3 -0
  41. package/dist/runtime/server/hydration.d.ts.map +1 -0
  42. package/dist/runtime/server/hydration.js +20 -0
  43. package/dist/runtime/server/manifest.d.ts +14 -0
  44. package/dist/runtime/server/manifest.d.ts.map +1 -0
  45. package/dist/runtime/server/manifest.js +59 -0
  46. package/dist/runtime/server/render-request.d.ts +21 -0
  47. package/dist/runtime/server/render-request.d.ts.map +1 -0
  48. package/dist/runtime/server/render-request.jsx +170 -0
  49. package/dist/runtime/server/route-match.d.ts +14 -0
  50. package/dist/runtime/server/route-match.d.ts.map +1 -0
  51. package/dist/runtime/server/route-match.js +35 -0
  52. package/dist/runtime/server/rpc.d.ts +3 -0
  53. package/dist/runtime/server/rpc.d.ts.map +1 -0
  54. package/dist/runtime/server/rpc.js +32 -0
  55. package/dist/runtime/server/types.d.ts +33 -0
  56. package/dist/runtime/server/types.d.ts.map +1 -0
  57. package/dist/runtime/server/types.js +1 -0
  58. package/dist/runtime/shared/constants.d.ts +8 -0
  59. package/dist/runtime/shared/constants.d.ts.map +1 -0
  60. package/dist/runtime/shared/constants.js +7 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +11 -7
  63. package/src/anaemia.d.ts +75 -0
  64. package/src/config.ts +9 -1
  65. package/src/index.ts +9 -1
  66. package/src/plugins/index.ts +1 -1
  67. package/src/plugins/lightningcss.ts +11 -11
  68. package/src/runtime/context.browser.ts +1 -1
  69. package/src/runtime/context.ts +6 -5
  70. package/src/runtime/entry-client.tsx +5 -8
  71. package/src/runtime/entry-server.tsx +19 -373
  72. package/src/runtime/resources.ts +3 -2
  73. package/src/runtime/route-data.ts +6 -5
  74. package/src/runtime/route-request.ts +4 -3
  75. package/src/runtime/rpc-client.ts +14 -13
  76. package/src/runtime/server/app.ts +44 -0
  77. package/src/runtime/server/assets.ts +69 -0
  78. package/src/runtime/server/boot.ts +9 -0
  79. package/src/runtime/server/env.ts +17 -0
  80. package/src/runtime/server/guards.ts +26 -0
  81. package/src/runtime/server/html.ts +84 -0
  82. package/src/runtime/server/hydration.ts +26 -0
  83. package/src/runtime/server/manifest.ts +69 -0
  84. package/src/runtime/server/render-request.tsx +230 -0
  85. package/src/runtime/server/route-match.ts +45 -0
  86. package/src/runtime/server/rpc.ts +34 -0
  87. package/src/runtime/server/types.ts +36 -0
  88. package/src/runtime/shared/constants.ts +7 -0
  89. package/src/runtime/webpack.d.ts +1 -1
  90. package/src/types.ts +18 -3
  91. package/test/integration/hmr.test.mjs +18 -5
  92. package/test/rpc-client.test.mjs +1 -1
  93. package/test/run-on-server.test.mjs +2 -6
  94. package/tsconfig.json +1 -1
@@ -1,14 +1,7 @@
1
- import { Hono } from "hono";
2
- import { serve } from "@hono/node-server";
3
- import type { Context } from "hono";
4
- import { serveStatic } from "@hono/node-server/serve-static";
5
- import { compress } from "hono/compress";
6
- import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
7
- import { Router } from "@solidjs/router";
8
- import { ssrStorage, serverFunctionsRegistry } from "./context.js";
9
- import fs from "node:fs";
10
- import path from "path";
11
- import type { StatusCode, RedirectStatusCode } from "hono/utils/http-status";
1
+ import { createServerApp } from "./server/app.js";
2
+ import { serveServer } from "./server/boot.js";
3
+ import { createRuntimeEnv } from "./server/env.js";
4
+ import { createManifestStore } from "./server/manifest.js";
12
5
 
13
6
  // @ts-expect-error - resolved by Rspack
14
7
  import App from "anaemia-user-app";
@@ -19,371 +12,24 @@ import { preloadActiveClientRoute, serverLoaderRegistry, serverGuardRegistry } f
19
12
  // @ts-expect-error - resolved by Rspack
20
13
  import { registerServerRoutes } from "__anaemia_server_routes__";
21
14
 
22
- const port = Number(process.env.PORT) || 3000;
23
- const isDev = process.env.NODE_ENV !== "production";
24
-
25
- const devPort = Number(process.env.RSPACK_DEV_PORT) || 4445;
26
- const devServerUrl = `http://localhost:${devPort}`;
27
-
28
- interface ChunkAssets {
29
- js?: string[];
30
- css?: string[];
31
- }
32
-
33
- interface RouteManifest {
34
- routes: Array<{ urlPattern: string; chunkName: string; params: string[] }>;
35
- chunks: Record<string, ChunkAssets>;
36
- errors?: Record<string, string>;
37
- }
38
-
39
- type SortedRoute = { urlPattern: string; chunkName: string; params: string[] };
40
- let sortedRoutes: SortedRoute[] | null = null;
41
-
42
- const ENTRY_TAG_REGEX = /(<([a-zA-Z0-9-]+)[^>]*anaemia-entry[^>]*>)(.*?)(<\/\2>)/is;
43
-
44
- const app = new Hono();
45
-
46
- app.use("*", compress());
47
-
48
- app.use("*", async (c, next) => {
49
- const store = new Map<string, unknown>();
50
- store.set("honoContext", c);
51
- return await ssrStorage.run(store, next);
52
- });
53
-
54
- if (isDev) {
55
- const devAssetProxy = async (c: Context) => {
56
- const targetUrl = `${devServerUrl}${c.req.path}`;
57
- try {
58
- const response = await fetch(targetUrl);
59
- if (!response.ok) return c.text("asset not found in Rspack memory", 404);
60
-
61
- const contentType = response.headers.get("content-type");
62
- if (contentType) c.header("content-type", contentType);
63
-
64
- c.header("Cache-Control", "no-cache, no-store, must-revalidate");
65
- c.header("Pragma", "no-cache");
66
- c.header("Expires", "0");
67
-
68
- return c.body(await response.arrayBuffer());
69
- } catch {
70
- return c.text("failed to connect to Rspack dev server asset bridge", 500);
71
- }
72
- };
73
-
74
- app.get("/assets/*", devAssetProxy);
75
- } else {
76
- app.use("/assets/*", async (c, next) => {
77
- await next();
78
- if (c.res.ok) c.res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
79
- });
80
-
81
- app.use(
82
- "/assets/*",
83
- serveStatic({
84
- root: path.resolve(process.cwd(), "./dist/client"),
85
- })
86
- );
87
- }
88
-
89
- app.post("/_rpc", async (c) => {
90
- const functionId = c.req.query("id");
91
- if (!functionId || !serverFunctionsRegistry.has(functionId)) {
92
- return c.json({ error: "RPC function not found" }, 404);
93
- }
94
-
95
- const contentLength = Number(c.req.header("content-length") ?? 0);
96
- if (contentLength > 512_000) {
97
- return c.json({ error: "Payload too large" }, 413);
98
- }
99
-
100
- let argumentsArray: unknown[];
101
- try {
102
- const body = await c.req.json();
103
- if (!Array.isArray(body)) throw new Error("Expected array");
104
- argumentsArray = body;
105
- } catch {
106
- return c.json({ error: "Invalid request body" }, 400);
107
- }
108
-
109
- try {
110
- const result = await serverFunctionsRegistry.get(functionId)!(...argumentsArray);
111
- return c.json(result);
112
- } catch (error) {
113
- const message = error instanceof Error ? error.message : "Internal server error";
114
- return c.json({ error: message }, 500);
115
- }
116
- });
117
-
118
- app.use(async (c, next) => {
119
- const p = c.req.path;
120
- if (isDev && p.includes(".hot-update.")) {
121
- const targetUrl = `${devServerUrl}${p}`;
122
- try {
123
- const response = await fetch(targetUrl);
124
- if (!response.ok) return c.text("hot update not found", 404);
125
-
126
- const contentType = response.headers.get("content-type");
127
- if (contentType) c.header("content-type", contentType);
128
-
129
- c.header("Cache-Control", "no-cache, no-store, must-revalidate");
130
-
131
- return c.body(await response.arrayBuffer());
132
- } catch {
133
- return c.text("failed to fetch hot update", 500);
134
- }
135
- }
136
- await next();
137
- });
138
-
139
- registerServerRoutes(app);
140
-
141
- let memoizedHtmlTemplate = "";
142
- let memoizedManifest: RouteManifest | null = null;
143
-
144
- const templatePath = path.resolve(process.cwd(), "./dist/client/index.html");
145
- const manifestPath = path.resolve(process.cwd(), "./dist/route-manifest.json");
146
-
147
- const loadManifestAndTemplate = async () => {
148
- if (isDev) {
149
- try {
150
- memoizedHtmlTemplate = await fetch(`${devServerUrl}/index.html`).then((r) => {
151
- if (!r.ok) throw new Error(`index.html fetch failed: ${r.status}`);
152
- return r.text();
153
- });
154
- } catch (err) {
155
- console.error("[anaemia engine sync error - HTML fetch failed]:", err);
156
- memoizedHtmlTemplate = "";
157
- }
158
-
159
- try {
160
- if (fs.existsSync(manifestPath)) {
161
- memoizedManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
162
- } else {
163
- memoizedManifest = { routes: [], chunks: {}, errors: {} };
164
- }
165
- } catch (err) {
166
- console.error("[anaemia engine sync error - manifest read failed]:", err);
167
- memoizedManifest = { routes: [], chunks: {}, errors: {} };
168
- }
169
- } else {
170
- try {
171
- if (fs.existsSync(templatePath)) memoizedHtmlTemplate = fs.readFileSync(templatePath, "utf-8");
172
- if (fs.existsSync(manifestPath)) memoizedManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
173
- } catch {
174
- console.warn("build assets not fully initialized during bootstrapping cycle.");
175
- }
176
- }
177
-
178
- sortedRoutes = null;
179
- };
180
-
181
- const normalizeAssetUrl = (url: unknown): string => {
182
- if (!url || typeof url !== "string") return "";
183
- if (url.startsWith("http://") || url.startsWith("https://")) return url;
184
- return url.startsWith("/") ? url : `/${url}`;
185
- };
186
-
187
- type RouteMatch = {
188
- activeChunk: string;
189
- targetPattern: string;
190
- statusCode: StatusCode;
191
- params: Record<string, string>;
192
- };
193
-
194
- type GuardFn = (ctx: { params: Record<string, string>; request: Request; url: string }) => void | undefined | { redirect: string; status?: 301 | 302 | 307 | 308 } | { status: number; body?: string } | Promise<void | undefined | { redirect: string; status?: number } | { status: number; body?: string }>;
195
-
196
- async function runGuards(pattern: string, ctx: { params: Record<string, string>; request: Request; url: string }) {
197
- const chain: (() => Promise<GuardFn[]>)[] = serverGuardRegistry.get(pattern) ?? [];
198
- for (const loadGuards of chain) {
199
- const guards: GuardFn[] = await loadGuards();
200
- for (const guard of guards) {
201
- const result = await guard(ctx);
202
- if (result && ("redirect" in result || "status" in result)) return result;
203
- }
204
- }
205
- return null;
206
- }
207
-
208
- function matchRoute(manifest: RouteManifest, reqPath: string): RouteMatch {
209
- if (!sortedRoutes) {
210
- sortedRoutes = [...manifest.routes].sort((a, b) => {
211
- const score = (pattern: string) => {
212
- const segments = pattern.split("/").filter(Boolean);
213
- return segments.reduce((acc, s) => {
214
- if (s.startsWith(":")) return acc - 1;
215
- if (s === "*" || s.startsWith("*")) return acc - 2;
216
- return acc;
217
- }, segments.length * 10);
218
- };
219
- return score(b.urlPattern) - score(a.urlPattern);
220
- });
221
- }
222
-
223
- for (const route of sortedRoutes) {
224
- const regexStr = route.urlPattern.replace(/:([a-zA-Z0-9_-]+)/g, "(?<$1>[^/]+)").replace(/\*([a-zA-Z0-9_-]*)/g, "(?<catchall>.*)");
225
-
226
- const match = new RegExp(`^${regexStr}$`).exec(reqPath);
227
- if (match) {
228
- return {
229
- activeChunk: route.chunkName,
230
- targetPattern: route.urlPattern,
231
- statusCode: 200,
232
- params: match.groups ? { ...match.groups } : {},
233
- };
234
- }
235
- }
236
-
237
- return {
238
- activeChunk: "route-404",
239
- targetPattern: manifest.errors?.["404"] || "",
240
- statusCode: 404,
241
- params: {},
242
- };
243
- }
244
-
245
- app.get("*", async (c) => {
246
- if (isDev) await loadManifestAndTemplate();
247
-
248
- let template = memoizedHtmlTemplate;
249
- let manifest = memoizedManifest;
250
-
251
- if (!template || !manifest) {
252
- return c.text("anaemia engine error: build assets are missing", 500);
253
- }
254
-
255
- const reqPath = c.req.path;
256
- const { activeChunk, targetPattern, statusCode: matchedStatus, params } = matchRoute(manifest, reqPath);
257
- let statusCode: StatusCode = matchedStatus;
258
- const loaderArgs = { params, request: c.req.raw };
259
-
260
- const store = ssrStorage.getStore() || new Map<string, unknown>();
261
- let htmlPayload;
262
-
263
- if (targetPattern) {
264
- try {
265
- const guardResult = await runGuards(targetPattern, { params, request: c.req.raw, url: reqPath });
266
- if (guardResult) {
267
- if ("redirect" in guardResult) return c.redirect(guardResult.redirect, (guardResult.status ?? 302) as RedirectStatusCode);
268
- if ("status" in guardResult) statusCode = guardResult.status as StatusCode;
269
- }
270
- } catch (err) {
271
- console.error("[anaemia] guard threw unexpectedly:", err);
272
- return c.text("Internal Server Error", 500);
273
- }
274
- }
275
-
276
- try {
277
- htmlPayload = await ssrStorage.run(store, async () => {
278
- if (targetPattern) {
279
- const executableLoader = serverLoaderRegistry.get(targetPattern);
280
- if (executableLoader) {
281
- const initialLoaderData = await executableLoader(loaderArgs);
282
- store.set("__LOADER_DATA__", initialLoaderData);
283
- }
284
- }
285
-
286
- await preloadActiveClientRoute(reqPath);
287
-
288
- return await renderToStringAsync(() => (
289
- <Router url={reqPath}>
290
- <App />
291
- </Router>
292
- ));
293
- });
294
- } catch (err) {
295
- statusCode = 500;
296
- console.error("[anaemia framework] runtime execution crash handled:", err);
297
-
298
- const error500Pattern = manifest.errors?.["500"];
299
- const error500Loader = error500Pattern ? serverLoaderRegistry.get(error500Pattern) : null;
300
-
301
- if (error500Loader) {
302
- const message = err instanceof Error ? err.message : String(err);
303
- const stack = err instanceof Error ? err.stack : undefined;
304
- const runtimeContextPayload = { message, stack: isDev ? stack : undefined };
305
- store.set("__LOADER_DATA__", runtimeContextPayload);
306
-
307
- try {
308
- htmlPayload = await ssrStorage.run(store, async () => {
309
- return await renderToStringAsync(() => (
310
- <Router url={error500Pattern}>
311
- <App />
312
- </Router>
313
- ));
314
- });
315
- } catch {
316
- htmlPayload = `<h1>500 Internal Server Error</h1>`;
317
- }
318
- } else {
319
- const stack = err instanceof Error ? err.stack : String(err);
320
- htmlPayload = `<h1>500 Internal Server Error</h1><pre>${isDev ? stack : ""}</pre>`;
321
- }
322
- }
323
-
324
- let assetScripts = "";
325
- let assetStyles = "";
326
-
327
- if (manifest.chunks) {
328
- const processChunkAssets = (chunk: ChunkAssets | undefined) => {
329
- if (!chunk) return;
330
- if (chunk.js) {
331
- const jsSpecs = Array.isArray(chunk.js) ? chunk.js : [chunk.js];
332
- jsSpecs.forEach((jsFile: string) => {
333
- assetScripts += `<script type="module" src="${normalizeAssetUrl(jsFile)}"></script>\n`;
334
- });
335
- }
336
- if (chunk.css) {
337
- const cssSpecs = Array.isArray(chunk.css) ? chunk.css : [chunk.css];
338
- cssSpecs.forEach((cssFile: string) => {
339
- assetStyles += `<link rel="stylesheet" href="${normalizeAssetUrl(cssFile)}">\n`;
340
- });
341
- }
342
- };
343
-
344
- processChunkAssets(manifest.chunks["client"]);
345
- if (manifest.chunks["commons"]) processChunkAssets(manifest.chunks["commons"]);
346
- if (manifest.chunks["vendors"]) processChunkAssets(manifest.chunks["vendors"]);
347
- if (activeChunk && activeChunk !== "client") processChunkAssets(manifest.chunks[activeChunk]);
348
- }
349
-
350
- const hydrationScript = generateHydrationScript();
351
- const rawStorePayload = Object.fromEntries(store);
352
-
353
- const finalHydrationStatePayload = {
354
- __LOADER_DATA__: rawStorePayload.__LOADER_DATA__ || {},
355
- __SERVER_FUNCTION_DATA__: rawStorePayload.__SERVER_FUNCTION_DATA__ || {},
356
- };
357
-
358
- const serializedData = JSON.stringify(finalHydrationStatePayload).replace(/&/g, "\\u0026").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\//g, "\\u002f");
359
-
360
- const dataScript = `<script id="__ANAEMIA_DATA__" type="application/json">${serializedData}</script>\n`;
361
-
362
- const devNoCacheTag = isDev ? `<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` : "";
363
-
364
- const combinedHeadInjections = `${devNoCacheTag}${assetStyles}${dataScript}${hydrationScript}`;
365
- const sanitizedPayload = htmlPayload.trim();
366
-
367
- let completeHtmlOutput = ENTRY_TAG_REGEX.test(template) ? template.replace(ENTRY_TAG_REGEX, (_, open, _tag, _inner, close) => `${open}${sanitizedPayload}${close}`) : template.replace("</body>", () => `<div anaemia-entry>${sanitizedPayload}</div></body>`);
368
-
369
- completeHtmlOutput = completeHtmlOutput.replace("<head>", `<head>${combinedHeadInjections}`);
370
- completeHtmlOutput = completeHtmlOutput.replace("</body>", `${assetScripts}</body>`);
371
-
372
- if (isDev) {
373
- c.header("Cache-Control", "no-cache, no-store, must-revalidate");
374
- c.header("Pragma", "no-cache");
375
- c.header("Expires", "0");
376
- }
377
-
378
- c.status(statusCode);
379
- return c.html(completeHtmlOutput);
15
+ const env = createRuntimeEnv();
16
+ const manifestStore = createManifestStore(env);
17
+
18
+ const app = createServerApp({
19
+ App,
20
+ env,
21
+ preloadActiveClientRoute,
22
+ serverLoaderRegistry,
23
+ serverGuardRegistry,
24
+ registerServerRoutes,
25
+ getManifestSnapshot: manifestStore.getSnapshot,
26
+ loadManifestAndTemplate: manifestStore.load,
380
27
  });
381
28
 
382
- loadManifestAndTemplate()
29
+ manifestStore
30
+ .load()
383
31
  .then(() => {
384
- serve({ fetch: app.fetch, port }, (info) => {
385
- console.log(`[anaemia framework] server live at http://localhost:${info.port}`);
386
- });
32
+ serveServer(app, env);
387
33
  })
388
34
  .catch((err) => {
389
35
  console.error("[anaemia] failed to initialize:", err);
@@ -1,5 +1,6 @@
1
1
  import { createResource, type ResourceOptions, type ResourceReturn } from "solid-js";
2
2
  import { isServer } from "solid-js/web";
3
+ import { SERVER_FUNCTION_DATA_KEY } from "./shared/constants.js";
3
4
 
4
5
  interface ServerStorage {
5
6
  getStore?: () => Map<string, unknown> | undefined;
@@ -18,14 +19,14 @@ export function createServerResource<Source, Return>(
18
19
  readHydrationCache?: (s: Source) => Return | undefined;
19
20
  id?: string;
20
21
  },
21
- options?: ResourceOptions<Return, Source>
22
+ options?: ResourceOptions<Return, Source>,
22
23
  ): ResourceReturn<Return, unknown> {
23
24
  if (isServer) {
24
25
  let ssrInitialValue: Return | undefined = undefined;
25
26
  const store = (globalThis as unknown as AnaemiaGlobal).__ANAEMIA_SERVER_STORAGE__?.getStore?.();
26
27
 
27
28
  if (store && serverFn.id) {
28
- const fnData = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionData | undefined;
29
+ const fnData = store.get(SERVER_FUNCTION_DATA_KEY) as ServerFunctionData | undefined;
29
30
  const fnCache = fnData?.[serverFn.id];
30
31
  if (fnCache) {
31
32
  const sourceValue = source();
@@ -4,6 +4,7 @@ import { useParams, useLocation, type Params } from "@solidjs/router";
4
4
  import type { Location } from "@solidjs/router";
5
5
  import { ssrStorage } from "./context.js";
6
6
  import { createRouteRequest } from "./route-request.js";
7
+ import { ANAEMIA_DATA_SCRIPT_ID, LOADER_DATA_KEY } from "./shared/constants.js";
7
8
 
8
9
  type LoaderArgs<TParams extends Params> = {
9
10
  params: TParams;
@@ -25,19 +26,19 @@ const RouteDataContext = createContext<RouteDataContextValue>();
25
26
  let hasReadClientHydrationData = false;
26
27
 
27
28
  interface AnaemiaHydrationData {
28
- __LOADER_DATA__?: unknown;
29
+ [LOADER_DATA_KEY]?: unknown;
29
30
  }
30
31
 
31
32
  function readSSRData(): unknown {
32
33
  if (isServer) {
33
- return ssrStorage.getStore()?.get("__LOADER_DATA__");
34
+ return ssrStorage.getStore()?.get(LOADER_DATA_KEY);
34
35
  }
35
36
  if (hasReadClientHydrationData) return undefined;
36
37
  hasReadClientHydrationData = true;
37
- const el = document.getElementById("__ANAEMIA_DATA__");
38
+ const el = document.getElementById(ANAEMIA_DATA_SCRIPT_ID);
38
39
  if (!el?.textContent) return undefined;
39
40
  try {
40
- return (JSON.parse(el.textContent) as AnaemiaHydrationData).__LOADER_DATA__;
41
+ return (JSON.parse(el.textContent) as AnaemiaHydrationData)[LOADER_DATA_KEY];
41
42
  } catch {
42
43
  return undefined;
43
44
  }
@@ -61,7 +62,7 @@ export function RouteDataController<TParams extends Params = Params>(props: Rout
61
62
  {
62
63
  initialValue: ssrData,
63
64
  ssrLoadFrom: "initial",
64
- }
65
+ },
65
66
  );
66
67
 
67
68
  return createComponent(RouteDataContext.Provider, {
@@ -1,13 +1,14 @@
1
1
  import { isServer } from "solid-js/web";
2
2
  import type { Context } from "hono";
3
3
  import { ssrStorage } from "./context.js";
4
+ import { HONO_CONTEXT_KEY } from "./shared/constants.js";
4
5
 
5
6
  export function createRouteRequest(pathname: string): Request {
6
7
  if (isServer) {
7
- const honoContext = ssrStorage.getStore()?.get("honoContext") as Context | undefined;
8
- const request = honoContext?.req?.raw;
8
+ const honoContext = ssrStorage.getStore()?.get(HONO_CONTEXT_KEY) as Context | undefined;
9
+ const request = honoContext?.req.raw;
9
10
  if (request) return request;
10
11
  return new Request(new URL(pathname, "http://localhost").toString());
11
12
  }
12
13
  return new Request(new URL(pathname, window.location.origin).toString());
13
- }
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { isServer } from "solid-js/web";
2
+ import { ANAEMIA_DATA_SCRIPT_ID, RPC_PATH, SERVER_FUNCTION_DATA_KEY } from "./shared/constants.js";
2
3
 
3
4
  interface CacheMatch {
4
5
  matchingKey: string;
@@ -10,7 +11,7 @@ interface ServerFunctionCache {
10
11
  }
11
12
 
12
13
  interface AnaemiaClientCache {
13
- __SERVER_FUNCTION_DATA__?: ServerFunctionCache;
14
+ [SERVER_FUNCTION_DATA_KEY]?: ServerFunctionCache;
14
15
  }
15
16
 
16
17
  interface AnaemiaServerStorage {
@@ -25,7 +26,7 @@ let _clientCache: AnaemiaClientCache | null = null;
25
26
 
26
27
  function ensureCacheInitialized() {
27
28
  if (isServer || _clientCache) return;
28
- const script = document.getElementById("__ANAEMIA_DATA__");
29
+ const script = document.getElementById(ANAEMIA_DATA_SCRIPT_ID);
29
30
  try {
30
31
  _clientCache = JSON.parse(script?.textContent || "{}") as AnaemiaClientCache;
31
32
  } catch {
@@ -34,8 +35,8 @@ function ensureCacheInitialized() {
34
35
  }
35
36
 
36
37
  function findLooseCacheMatch(
37
- serverFunctionData: Record<string, unknown>,
38
- targetArg: string
38
+ serverFunctionData: Record<string, unknown> | null | undefined,
39
+ targetArg: string,
39
40
  ): CacheMatch | undefined {
40
41
  if (!serverFunctionData) return undefined;
41
42
  const strictKey = JSON.stringify([targetArg]);
@@ -56,17 +57,17 @@ export function $$executeClientRpc(hashId: string) {
56
57
  if (isServer) {
57
58
  const store = getServerStore();
58
59
  if (store) {
59
- const functionCache = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionCache | undefined;
60
+ const functionCache = store.get(SERVER_FUNCTION_DATA_KEY) as ServerFunctionCache | undefined;
60
61
  if (functionCache?.[hashId]) {
61
62
  const match = findLooseCacheMatch(functionCache[hashId], args[0] as string);
62
63
  if (match) return match.data;
63
64
  }
64
65
  }
65
- return undefined;
66
+ return;
66
67
  }
67
68
 
68
69
  ensureCacheInitialized();
69
- const serverFunctionData = _clientCache?.__SERVER_FUNCTION_DATA__?.[hashId];
70
+ const serverFunctionData = _clientCache?.[SERVER_FUNCTION_DATA_KEY]?.[hashId];
70
71
  const match = findLooseCacheMatch(serverFunctionData ?? {}, args[0] as string);
71
72
  if (match) {
72
73
  const { matchingKey, data } = match;
@@ -74,13 +75,13 @@ export function $$executeClientRpc(hashId: string) {
74
75
  return data;
75
76
  }
76
77
 
77
- const response = await fetch(`/_rpc?id=${hashId}`, {
78
+ const response = await fetch(`${RPC_PATH}?id=${hashId}`, {
78
79
  method: "POST",
79
80
  headers: { "Content-Type": "application/json" },
80
81
  body: JSON.stringify(args),
81
82
  });
82
83
  if (!response.ok) throw new Error(`[anaemia] RPC execution failed: ${response.status}`);
83
- return await response.json() as unknown;
84
+ return (await response.json()) as unknown;
84
85
  };
85
86
 
86
87
  asyncRpcCall.id = hashId;
@@ -88,20 +89,20 @@ export function $$executeClientRpc(hashId: string) {
88
89
  if (isServer) {
89
90
  const store = getServerStore();
90
91
  if (store) {
91
- const functionCache = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionCache | undefined;
92
+ const functionCache = store.get(SERVER_FUNCTION_DATA_KEY) as ServerFunctionCache | undefined;
92
93
  if (functionCache?.[hashId]) {
93
94
  const match = findLooseCacheMatch(functionCache[hashId], args[0] as string);
94
95
  if (match) return match.data;
95
96
  }
96
97
  }
97
- return undefined;
98
+ return;
98
99
  }
99
100
 
100
101
  ensureCacheInitialized();
101
- const serverFunctionData = _clientCache?.__SERVER_FUNCTION_DATA__?.[hashId];
102
+ const serverFunctionData = _clientCache?.[SERVER_FUNCTION_DATA_KEY]?.[hashId];
102
103
  const match = findLooseCacheMatch(serverFunctionData ?? {}, args[0] as string);
103
104
  return match ? match.data : undefined;
104
105
  };
105
106
 
106
107
  return asyncRpcCall;
107
- }
108
+ }
@@ -0,0 +1,44 @@
1
+ import { Hono } from "hono";
2
+ import { compress } from "hono/compress";
3
+ import type { Component } from "solid-js";
4
+ import { ssrStorage } from "../context.js";
5
+ import { HONO_CONTEXT_KEY } from "../shared/constants.js";
6
+ import { registerAssetRoutes } from "./assets.js";
7
+ import { registerRpcRoute } from "./rpc.js";
8
+ import { createRenderRequestHandler } from "./render-request.jsx";
9
+ import type { RuntimeEnv } from "./types.js";
10
+ import type { GuardFn } from "./guards.js";
11
+ import type { ManifestSnapshot } from "./manifest.js";
12
+
13
+ type ServerLoader = (args: { params: Record<string, string>; request: Request }) => unknown | Promise<unknown>;
14
+
15
+ type CreateServerAppOptions = {
16
+ App: Component;
17
+ env: RuntimeEnv;
18
+ preloadActiveClientRoute: (path: string) => unknown | Promise<unknown>;
19
+ serverLoaderRegistry: Map<string, ServerLoader>;
20
+ serverGuardRegistry: Map<string, (() => Promise<GuardFn[]>)[]>;
21
+ registerServerRoutes: (app: Hono) => void;
22
+ getManifestSnapshot: () => ManifestSnapshot;
23
+ loadManifestAndTemplate: () => Promise<void>;
24
+ };
25
+
26
+ export function createServerApp(options: CreateServerAppOptions) {
27
+ const app = new Hono();
28
+
29
+ app.use("*", compress());
30
+
31
+ app.use("*", async (c, next) => {
32
+ const store = new Map<string, unknown>();
33
+ store.set(HONO_CONTEXT_KEY, c);
34
+ return await ssrStorage.run(store, next);
35
+ });
36
+
37
+ registerAssetRoutes(app, options.env);
38
+ registerRpcRoute(app);
39
+ options.registerServerRoutes(app);
40
+
41
+ app.get("*", createRenderRequestHandler(options));
42
+
43
+ return app;
44
+ }