@anaemia/core 0.4.0 → 0.5.1
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/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +4 -3
- package/dist/runtime/entry-client.jsx +2 -1
- package/dist/runtime/entry-server.d.ts +1 -2
- package/dist/runtime/entry-server.d.ts.map +1 -1
- package/dist/runtime/entry-server.jsx +18 -331
- package/dist/runtime/resources.d.ts.map +1 -1
- package/dist/runtime/resources.js +2 -1
- package/dist/runtime/route-data.d.ts.map +1 -1
- package/dist/runtime/route-data.js +4 -3
- package/dist/runtime/route-request.d.ts.map +1 -1
- package/dist/runtime/route-request.js +2 -1
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +7 -6
- package/dist/runtime/server/app.d.ts +22 -0
- package/dist/runtime/server/app.d.ts.map +1 -0
- package/dist/runtime/server/app.js +21 -0
- package/dist/runtime/server/assets.d.ts +5 -0
- package/dist/runtime/server/assets.d.ts.map +1 -0
- package/dist/runtime/server/assets.js +48 -0
- package/dist/runtime/server/boot.d.ts +4 -0
- package/dist/runtime/server/boot.d.ts.map +1 -0
- package/dist/runtime/server/boot.js +6 -0
- package/dist/runtime/server/env.d.ts +3 -0
- package/dist/runtime/server/env.d.ts.map +1 -0
- package/dist/runtime/server/env.js +14 -0
- package/dist/runtime/server/guards.d.ts +29 -0
- package/dist/runtime/server/guards.d.ts.map +1 -0
- package/dist/runtime/server/guards.js +12 -0
- package/dist/runtime/server/html.d.ts +15 -0
- package/dist/runtime/server/html.d.ts.map +1 -0
- package/dist/runtime/server/html.js +62 -0
- package/dist/runtime/server/hydration.d.ts +3 -0
- package/dist/runtime/server/hydration.d.ts.map +1 -0
- package/dist/runtime/server/hydration.js +20 -0
- package/dist/runtime/server/manifest.d.ts +14 -0
- package/dist/runtime/server/manifest.d.ts.map +1 -0
- package/dist/runtime/server/manifest.js +59 -0
- package/dist/runtime/server/render-request.d.ts +21 -0
- package/dist/runtime/server/render-request.d.ts.map +1 -0
- package/dist/runtime/server/render-request.jsx +170 -0
- package/dist/runtime/server/route-match.d.ts +14 -0
- package/dist/runtime/server/route-match.d.ts.map +1 -0
- package/dist/runtime/server/route-match.js +35 -0
- package/dist/runtime/server/rpc.d.ts +3 -0
- package/dist/runtime/server/rpc.d.ts.map +1 -0
- package/dist/runtime/server/rpc.js +32 -0
- package/dist/runtime/server/types.d.ts +33 -0
- package/dist/runtime/server/types.d.ts.map +1 -0
- package/dist/runtime/server/types.js +1 -0
- package/dist/runtime/shared/constants.d.ts +8 -0
- package/dist/runtime/shared/constants.d.ts.map +1 -0
- package/dist/runtime/shared/constants.js +7 -0
- package/package.json +5 -2
- package/src/runtime/context.ts +4 -6
- package/src/runtime/entry-client.tsx +2 -1
- package/src/runtime/entry-server.tsx +19 -397
- package/src/runtime/resources.ts +2 -1
- package/src/runtime/route-data.ts +5 -4
- package/src/runtime/route-request.ts +2 -1
- package/src/runtime/rpc-client.ts +8 -7
- package/src/runtime/server/app.ts +44 -0
- package/src/runtime/server/assets.ts +69 -0
- package/src/runtime/server/boot.ts +9 -0
- package/src/runtime/server/env.ts +17 -0
- package/src/runtime/server/guards.ts +26 -0
- package/src/runtime/server/html.ts +84 -0
- package/src/runtime/server/hydration.ts +26 -0
- package/src/runtime/server/manifest.ts +69 -0
- package/src/runtime/server/render-request.tsx +230 -0
- package/src/runtime/server/route-match.ts +45 -0
- package/src/runtime/server/rpc.ts +34 -0
- package/src/runtime/server/types.ts +36 -0
- package/src/runtime/shared/constants.ts +7 -0
- package/tsconfig.json +1 -1
- /package/{src/anaemia.d.ts → anaemia.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anaemia/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"types": "./dist/plugins/index.d.ts",
|
|
29
29
|
"import": "./dist/plugins/index.js"
|
|
30
30
|
},
|
|
31
|
-
"./client":
|
|
31
|
+
"./client": {
|
|
32
|
+
"types": "./anaemia.d.ts",
|
|
33
|
+
"default": "./anaemia.d.ts"
|
|
34
|
+
},
|
|
32
35
|
"./package.json": "./package.json"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
package/src/runtime/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { SERVER_FUNCTION_DATA_KEY } from "./shared/constants.js";
|
|
2
3
|
|
|
3
4
|
type AnyFn = (...args: unknown[]) => unknown;
|
|
4
5
|
|
|
@@ -14,13 +15,10 @@ export function runOnServer<T extends AnyFn>(backendFn: T, id?: string): T & { i
|
|
|
14
15
|
const result = await backendFn(...args);
|
|
15
16
|
const store = ssrStorage.getStore();
|
|
16
17
|
if (store && hashId) {
|
|
17
|
-
if (!store.has(
|
|
18
|
-
store.set(
|
|
18
|
+
if (!store.has(SERVER_FUNCTION_DATA_KEY)) {
|
|
19
|
+
store.set(SERVER_FUNCTION_DATA_KEY, {});
|
|
19
20
|
}
|
|
20
|
-
const functionCache = store.get(
|
|
21
|
-
string,
|
|
22
|
-
Record<string, unknown> | undefined
|
|
23
|
-
>;
|
|
21
|
+
const functionCache = store.get(SERVER_FUNCTION_DATA_KEY) as Record<string, Record<string, unknown> | undefined>;
|
|
24
22
|
if (!functionCache[hashId]) {
|
|
25
23
|
functionCache[hashId] = {};
|
|
26
24
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { hydrate, render } from "solid-js/web";
|
|
2
2
|
import { Router } from "@solidjs/router";
|
|
3
|
+
import { ENTRY_SELECTOR } from "./shared/constants.js";
|
|
3
4
|
|
|
4
5
|
// @ts-expect-error - resolved by Rspack
|
|
5
6
|
import App, { preloadActiveClientRoute } from "anaemia-user-app";
|
|
6
7
|
|
|
7
|
-
const mountTarget = document.querySelector(
|
|
8
|
+
const mountTarget = document.querySelector(ENTRY_SELECTOR) as HTMLElement | null;
|
|
8
9
|
|
|
9
10
|
if (!mountTarget) {
|
|
10
11
|
throw new Error("[anaemia] missing mount target");
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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 "node: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,395 +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
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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: {
|
|
195
|
-
params: Record<string, string>;
|
|
196
|
-
request: Request;
|
|
197
|
-
url: string;
|
|
198
|
-
}) =>
|
|
199
|
-
| void
|
|
200
|
-
| undefined
|
|
201
|
-
| { redirect: string; status?: 301 | 302 | 307 | 308 }
|
|
202
|
-
| { status: number; body?: string }
|
|
203
|
-
| Promise<void | undefined | { redirect: string; status?: number } | { status: number; body?: string }>;
|
|
204
|
-
|
|
205
|
-
async function runGuards(pattern: string, ctx: { params: Record<string, string>; request: Request; url: string }) {
|
|
206
|
-
const chain: (() => Promise<GuardFn[]>)[] = serverGuardRegistry.get(pattern) ?? [];
|
|
207
|
-
for (const loadGuards of chain) {
|
|
208
|
-
const guards: GuardFn[] = await loadGuards();
|
|
209
|
-
for (const guard of guards) {
|
|
210
|
-
const result = await guard(ctx);
|
|
211
|
-
if (result && ("redirect" in result || "status" in result)) return result;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function matchRoute(manifest: RouteManifest, reqPath: string): RouteMatch {
|
|
218
|
-
if (!sortedRoutes) {
|
|
219
|
-
sortedRoutes = [...manifest.routes].sort((a, b) => {
|
|
220
|
-
const score = (pattern: string) => {
|
|
221
|
-
const segments = pattern.split("/").filter(Boolean);
|
|
222
|
-
return segments.reduce((acc, s) => {
|
|
223
|
-
if (s.startsWith(":")) return acc - 1;
|
|
224
|
-
if (s === "*" || s.startsWith("*")) return acc - 2;
|
|
225
|
-
return acc;
|
|
226
|
-
}, segments.length * 10);
|
|
227
|
-
};
|
|
228
|
-
return score(b.urlPattern) - score(a.urlPattern);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
for (const route of sortedRoutes) {
|
|
233
|
-
const regexStr = route.urlPattern
|
|
234
|
-
.replace(/:([a-zA-Z0-9_-]+)/g, "(?<$1>[^/]+)")
|
|
235
|
-
.replace(/\*([a-zA-Z0-9_-]*)/g, "(?<catchall>.*)");
|
|
236
|
-
|
|
237
|
-
const match = new RegExp(`^${regexStr}$`).exec(reqPath);
|
|
238
|
-
if (match) {
|
|
239
|
-
return {
|
|
240
|
-
activeChunk: route.chunkName,
|
|
241
|
-
targetPattern: route.urlPattern,
|
|
242
|
-
statusCode: 200,
|
|
243
|
-
params: match.groups ? { ...match.groups } : {},
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
activeChunk: "route-404",
|
|
250
|
-
targetPattern: manifest.errors?.["404"] || "",
|
|
251
|
-
statusCode: 404,
|
|
252
|
-
params: {},
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
app.get("*", async (c) => {
|
|
257
|
-
if (isDev) await loadManifestAndTemplate();
|
|
258
|
-
|
|
259
|
-
const template = memoizedHtmlTemplate;
|
|
260
|
-
const manifest = memoizedManifest;
|
|
261
|
-
|
|
262
|
-
if (!template || !manifest) {
|
|
263
|
-
return c.text("anaemia engine error: build assets are missing", 500);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const reqPath = c.req.path;
|
|
267
|
-
const { activeChunk, targetPattern, statusCode: matchedStatus, params } = matchRoute(manifest, reqPath);
|
|
268
|
-
let statusCode: StatusCode = matchedStatus;
|
|
269
|
-
const loaderArgs = { params, request: c.req.raw };
|
|
270
|
-
|
|
271
|
-
const store = ssrStorage.getStore() || new Map<string, unknown>();
|
|
272
|
-
let htmlPayload;
|
|
273
|
-
|
|
274
|
-
if (targetPattern) {
|
|
275
|
-
try {
|
|
276
|
-
const guardResult = await runGuards(targetPattern, {
|
|
277
|
-
params,
|
|
278
|
-
request: c.req.raw,
|
|
279
|
-
url: reqPath,
|
|
280
|
-
});
|
|
281
|
-
if (guardResult) {
|
|
282
|
-
if ("redirect" in guardResult)
|
|
283
|
-
return c.redirect(guardResult.redirect, (guardResult.status ?? 302) as RedirectStatusCode);
|
|
284
|
-
if ("status" in guardResult) statusCode = guardResult.status as StatusCode;
|
|
285
|
-
}
|
|
286
|
-
} catch (err) {
|
|
287
|
-
console.error("[anaemia] guard threw unexpectedly:", err);
|
|
288
|
-
return c.text("Internal Server Error", 500);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
htmlPayload = await ssrStorage.run(store, async () => {
|
|
294
|
-
if (targetPattern) {
|
|
295
|
-
const executableLoader = serverLoaderRegistry.get(targetPattern);
|
|
296
|
-
if (executableLoader) {
|
|
297
|
-
const initialLoaderData = await executableLoader(loaderArgs);
|
|
298
|
-
store.set("__LOADER_DATA__", initialLoaderData);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await preloadActiveClientRoute(reqPath);
|
|
303
|
-
|
|
304
|
-
return await renderToStringAsync(() => (
|
|
305
|
-
<Router url={reqPath}>
|
|
306
|
-
<App />
|
|
307
|
-
</Router>
|
|
308
|
-
));
|
|
309
|
-
});
|
|
310
|
-
} catch (err) {
|
|
311
|
-
statusCode = 500;
|
|
312
|
-
console.error("[anaemia framework] runtime execution crash handled:", err);
|
|
313
|
-
|
|
314
|
-
const error500Pattern = manifest.errors?.["500"];
|
|
315
|
-
const error500Loader = error500Pattern ? serverLoaderRegistry.get(error500Pattern) : null;
|
|
316
|
-
|
|
317
|
-
if (error500Loader) {
|
|
318
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
319
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
320
|
-
const runtimeContextPayload = { message, stack: isDev ? stack : undefined };
|
|
321
|
-
store.set("__LOADER_DATA__", runtimeContextPayload);
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
htmlPayload = await ssrStorage.run(store, async () => {
|
|
325
|
-
return await renderToStringAsync(() => (
|
|
326
|
-
<Router url={error500Pattern}>
|
|
327
|
-
<App />
|
|
328
|
-
</Router>
|
|
329
|
-
));
|
|
330
|
-
});
|
|
331
|
-
} catch {
|
|
332
|
-
htmlPayload = `<h1>500 Internal Server Error</h1>`;
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
const stack = err instanceof Error ? err.stack : String(err);
|
|
336
|
-
htmlPayload = `<h1>500 Internal Server Error</h1><pre>${isDev ? stack : ""}</pre>`;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
let assetScripts = "";
|
|
341
|
-
let assetStyles = "";
|
|
342
|
-
|
|
343
|
-
const processChunkAssets = (chunk: ChunkAssets | undefined) => {
|
|
344
|
-
if (!chunk) return;
|
|
345
|
-
if (chunk.js) {
|
|
346
|
-
const jsSpecs = Array.isArray(chunk.js) ? chunk.js : [chunk.js];
|
|
347
|
-
|
|
348
|
-
for (const jsFile of jsSpecs) {
|
|
349
|
-
assetScripts += `<script type="module" src="${normalizeAssetUrl(jsFile)}"></script>\n`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (chunk.css) {
|
|
353
|
-
const cssSpecs = Array.isArray(chunk.css) ? chunk.css : [chunk.css];
|
|
354
|
-
|
|
355
|
-
for (const cssFile of cssSpecs) {
|
|
356
|
-
assetStyles += `<link rel="stylesheet" href="${normalizeAssetUrl(cssFile)}">\n`;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
processChunkAssets(manifest.chunks["client"]);
|
|
362
|
-
processChunkAssets(manifest.chunks["commons"]);
|
|
363
|
-
processChunkAssets(manifest.chunks["vendors"]);
|
|
364
|
-
if (activeChunk && activeChunk !== "client") processChunkAssets(manifest.chunks[activeChunk]);
|
|
365
|
-
|
|
366
|
-
const hydrationScript = generateHydrationScript();
|
|
367
|
-
const rawStorePayload = Object.fromEntries(store);
|
|
368
|
-
|
|
369
|
-
const finalHydrationStatePayload = {
|
|
370
|
-
__LOADER_DATA__: rawStorePayload.__LOADER_DATA__ || {},
|
|
371
|
-
__SERVER_FUNCTION_DATA__: rawStorePayload.__SERVER_FUNCTION_DATA__ || {},
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const serializedData = JSON.stringify(finalHydrationStatePayload)
|
|
375
|
-
.replace(/&/g, "\\u0026")
|
|
376
|
-
.replace(/</g, "\\u003c")
|
|
377
|
-
.replace(/>/g, "\\u003e")
|
|
378
|
-
.replace(/\//g, "\\u002f");
|
|
379
|
-
|
|
380
|
-
const dataScript = `<script id="__ANAEMIA_DATA__" type="application/json">${serializedData}</script>\n`;
|
|
381
|
-
|
|
382
|
-
const devNoCacheTag = isDev
|
|
383
|
-
? `<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`
|
|
384
|
-
: "";
|
|
385
|
-
|
|
386
|
-
const combinedHeadInjections = `${devNoCacheTag}${assetStyles}${dataScript}${hydrationScript}`;
|
|
387
|
-
const sanitizedPayload = htmlPayload.trim();
|
|
388
|
-
|
|
389
|
-
let completeHtmlOutput = ENTRY_TAG_REGEX.test(template)
|
|
390
|
-
? template.replace(ENTRY_TAG_REGEX, (_, open, _tag, _inner, close) => `${open}${sanitizedPayload}${close}`)
|
|
391
|
-
: template.replace("</body>", () => `<div anaemia-entry>${sanitizedPayload}</div></body>`);
|
|
392
|
-
|
|
393
|
-
completeHtmlOutput = completeHtmlOutput.replace("<head>", `<head>${combinedHeadInjections}`);
|
|
394
|
-
completeHtmlOutput = completeHtmlOutput.replace("</body>", `${assetScripts}</body>`);
|
|
395
|
-
|
|
396
|
-
if (isDev) {
|
|
397
|
-
c.header("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
398
|
-
c.header("Pragma", "no-cache");
|
|
399
|
-
c.header("Expires", "0");
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
c.status(statusCode);
|
|
403
|
-
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,
|
|
404
27
|
});
|
|
405
28
|
|
|
406
|
-
|
|
29
|
+
manifestStore
|
|
30
|
+
.load()
|
|
407
31
|
.then(() => {
|
|
408
|
-
|
|
409
|
-
console.log(`[anaemia framework] server live at http://localhost:${info.port}`);
|
|
410
|
-
});
|
|
32
|
+
serveServer(app, env);
|
|
411
33
|
})
|
|
412
34
|
.catch((err) => {
|
|
413
35
|
console.error("[anaemia] failed to initialize:", err);
|
package/src/runtime/resources.ts
CHANGED
|
@@ -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;
|
|
@@ -25,7 +26,7 @@ export function createServerResource<Source, Return>(
|
|
|
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(
|
|
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
|
-
|
|
29
|
+
[LOADER_DATA_KEY]?: unknown;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function readSSRData(): unknown {
|
|
32
33
|
if (isServer) {
|
|
33
|
-
return ssrStorage.getStore()?.get(
|
|
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(
|
|
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)
|
|
41
|
+
return (JSON.parse(el.textContent) as AnaemiaHydrationData)[LOADER_DATA_KEY];
|
|
41
42
|
} catch {
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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(
|
|
8
|
+
const honoContext = ssrStorage.getStore()?.get(HONO_CONTEXT_KEY) as Context | undefined;
|
|
8
9
|
const request = honoContext?.req.raw;
|
|
9
10
|
if (request) return request;
|
|
10
11
|
return new Request(new URL(pathname, "http://localhost").toString());
|
|
@@ -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
|
-
|
|
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(
|
|
29
|
+
const script = document.getElementById(ANAEMIA_DATA_SCRIPT_ID);
|
|
29
30
|
try {
|
|
30
31
|
_clientCache = JSON.parse(script?.textContent || "{}") as AnaemiaClientCache;
|
|
31
32
|
} catch {
|
|
@@ -56,7 +57,7 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
56
57
|
if (isServer) {
|
|
57
58
|
const store = getServerStore();
|
|
58
59
|
if (store) {
|
|
59
|
-
const functionCache = store.get(
|
|
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;
|
|
@@ -66,7 +67,7 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
ensureCacheInitialized();
|
|
69
|
-
const serverFunctionData = _clientCache?.
|
|
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,7 +75,7 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
74
75
|
return data;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const response = await fetch(
|
|
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),
|
|
@@ -88,7 +89,7 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
88
89
|
if (isServer) {
|
|
89
90
|
const store = getServerStore();
|
|
90
91
|
if (store) {
|
|
91
|
-
const functionCache = store.get(
|
|
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;
|
|
@@ -98,7 +99,7 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
ensureCacheInitialized();
|
|
101
|
-
const serverFunctionData = _clientCache?.
|
|
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
|
};
|
|
@@ -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
|
+
}
|