@fiyuu/runtime 0.2.0 → 0.4.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/README.md +62 -0
- package/package.json +23 -4
- package/src/bundler.ts +0 -151
- package/src/cli.ts +0 -32
- package/src/client-runtime.ts +0 -528
- package/src/index.ts +0 -4
- package/src/inspector.ts +0 -329
- package/src/server-devtools.ts +0 -133
- package/src/server-loader.ts +0 -213
- package/src/server-middleware.ts +0 -71
- package/src/server-renderer.ts +0 -260
- package/src/server-router.ts +0 -77
- package/src/server-types.ts +0 -198
- package/src/server-utils.ts +0 -137
- package/src/server-websocket.ts +0 -71
- package/src/server.ts +0 -1089
- package/src/service.ts +0 -97
package/src/server-types.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types and interfaces for the Fiyuu runtime server.
|
|
3
|
-
* All other server modules import from here — no implementation code.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
-
import type { WebSocket } from "ws";
|
|
8
|
-
import type { createProjectGraph, FeatureRecord, FiyuuConfig, MetaDefinition, RenderMode } from "@fiyuu/core";
|
|
9
|
-
import type { FiyuuDB } from "@fiyuu/db";
|
|
10
|
-
import type { FiyuuRealtime } from "@fiyuu/realtime";
|
|
11
|
-
import type { ClientAsset } from "./bundler.js";
|
|
12
|
-
import type { InsightsReport } from "./inspector.js";
|
|
13
|
-
|
|
14
|
-
export type { IncomingMessage, ServerResponse };
|
|
15
|
-
export type { FeatureRecord, FiyuuConfig, MetaDefinition, RenderMode };
|
|
16
|
-
|
|
17
|
-
// ── Module shapes ─────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export interface ModuleShape {
|
|
20
|
-
default?: unknown;
|
|
21
|
-
execute?: (context: any) => Promise<unknown> | unknown;
|
|
22
|
-
page?: { intent: string };
|
|
23
|
-
cache?: { ttl?: number; vary?: string[] };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface LayoutModule {
|
|
27
|
-
default?: unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface GeaRenderable {
|
|
31
|
-
props?: Record<string, unknown>;
|
|
32
|
-
template?: (props?: Record<string, unknown>) => string;
|
|
33
|
-
toString: () => string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ApiRouteModule {
|
|
37
|
-
GET?: (context: RequestContext) => Promise<unknown> | unknown;
|
|
38
|
-
POST?: (context: RequestContext) => Promise<unknown> | unknown;
|
|
39
|
-
PUT?: (context: RequestContext) => Promise<unknown> | unknown;
|
|
40
|
-
PATCH?: (context: RequestContext) => Promise<unknown> | unknown;
|
|
41
|
-
DELETE?: (context: RequestContext) => Promise<unknown> | unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SocketModule {
|
|
45
|
-
registerSocketServer?: () => {
|
|
46
|
-
namespace?: string;
|
|
47
|
-
events?: string[];
|
|
48
|
-
onConnect?: (socket: WebSocket) => void;
|
|
49
|
-
onMessage?: (socket: WebSocket, message: string) => void;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Middleware ────────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
export interface MiddlewareModule {
|
|
56
|
-
middleware?: MiddlewareHandler | MiddlewareHandler[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface MiddlewareContext {
|
|
60
|
-
request: IncomingMessage;
|
|
61
|
-
url: URL;
|
|
62
|
-
responseHeaders: Record<string, string>;
|
|
63
|
-
requestId: string;
|
|
64
|
-
warnings: string[];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface MiddlewareResult {
|
|
68
|
-
headers?: Record<string, string>;
|
|
69
|
-
response?: {
|
|
70
|
-
status?: number;
|
|
71
|
-
json?: unknown;
|
|
72
|
-
body?: string;
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type MiddlewareNext = () => Promise<void>;
|
|
77
|
-
export type MiddlewareHandler = (
|
|
78
|
-
context: MiddlewareContext,
|
|
79
|
-
next: MiddlewareNext,
|
|
80
|
-
) => Promise<MiddlewareResult | void> | MiddlewareResult | void;
|
|
81
|
-
|
|
82
|
-
// ── Request handling ──────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
export interface RequestContext {
|
|
85
|
-
request: IncomingMessage;
|
|
86
|
-
route: string;
|
|
87
|
-
feature: FeatureRecord | null;
|
|
88
|
-
input?: Record<string, unknown>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ── Server options & result ───────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
export interface StartServerOptions {
|
|
94
|
-
mode: "dev" | "start";
|
|
95
|
-
rootDirectory: string;
|
|
96
|
-
appDirectory: string;
|
|
97
|
-
config?: FiyuuConfig;
|
|
98
|
-
port?: number;
|
|
99
|
-
maxPort?: number;
|
|
100
|
-
clientOutputDirectory: string;
|
|
101
|
-
staticClientRoot: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface StartedServer {
|
|
105
|
-
port: number;
|
|
106
|
-
url: string;
|
|
107
|
-
websocketUrl?: string;
|
|
108
|
-
close: () => Promise<void>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── Cache entries ─────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
export interface QueryCacheEntry {
|
|
114
|
-
data: unknown;
|
|
115
|
-
expiresAt: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface SsgCacheEntry {
|
|
119
|
-
html: string;
|
|
120
|
-
etag: string;
|
|
121
|
-
expiresAt: number | null;
|
|
122
|
-
revalidateSeconds: number | null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Runtime state ─────────────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
export interface RuntimeState {
|
|
128
|
-
graph: Awaited<ReturnType<typeof createProjectGraph>>;
|
|
129
|
-
features: FeatureRecord[];
|
|
130
|
-
routeIndex: RouteIndex;
|
|
131
|
-
assets: ClientAsset[];
|
|
132
|
-
assetsByRoute: Map<string, ClientAsset>;
|
|
133
|
-
insights: InsightsReport;
|
|
134
|
-
ssgCache: Map<string, SsgCacheEntry>;
|
|
135
|
-
queryCache: Map<string, QueryCacheEntry>;
|
|
136
|
-
queryInflight: Map<string, Promise<unknown>>;
|
|
137
|
-
queryCacheLastPruneAt: number;
|
|
138
|
-
layoutStackCache: Map<string, Array<{ component: unknown; meta: MetaDefinition }>>;
|
|
139
|
-
featureMetaCache: Map<string, MetaDefinition>;
|
|
140
|
-
mergedMetaCache: Map<string, MetaDefinition>;
|
|
141
|
-
serverEvents: Array<{ at: string; level: "info" | "warn" | "error"; event: string; details?: string }>;
|
|
142
|
-
version: number;
|
|
143
|
-
warnings: string[];
|
|
144
|
-
db: FiyuuDB;
|
|
145
|
-
realtime: FiyuuRealtime;
|
|
146
|
-
serviceNames: string[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Tiny internal router ──────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
export interface TinyRouteContext {
|
|
152
|
-
request: IncomingMessage;
|
|
153
|
-
response: ServerResponse;
|
|
154
|
-
url: URL;
|
|
155
|
-
state: RuntimeState;
|
|
156
|
-
options: StartServerOptions;
|
|
157
|
-
liveClients: Set<ServerResponse>;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface TinyRoute {
|
|
161
|
-
method: "GET" | "POST";
|
|
162
|
-
path: string;
|
|
163
|
-
type: "exact" | "prefix";
|
|
164
|
-
devOnly?: boolean;
|
|
165
|
-
handler: (context: TinyRouteContext) => Promise<void> | void;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Status page ───────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
export interface StatusPageInput {
|
|
171
|
-
statusCode: number;
|
|
172
|
-
title: string;
|
|
173
|
-
summary: string;
|
|
174
|
-
detail?: string;
|
|
175
|
-
route?: string;
|
|
176
|
-
method?: string;
|
|
177
|
-
requestId?: string;
|
|
178
|
-
hints?: string[];
|
|
179
|
-
diagnostics?: string[];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── Route matching ────────────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
export interface RouteMatch {
|
|
185
|
-
feature: FeatureRecord;
|
|
186
|
-
params: Record<string, string>;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface DynamicRouteMatcher {
|
|
190
|
-
feature: FeatureRecord;
|
|
191
|
-
regex: RegExp;
|
|
192
|
-
paramNames: string[];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export interface RouteIndex {
|
|
196
|
-
exact: Map<string, FeatureRecord>;
|
|
197
|
-
dynamic: DynamicRouteMatcher[];
|
|
198
|
-
}
|
package/src/server-utils.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure utility functions for the Fiyuu runtime server.
|
|
3
|
-
* No dependencies on other server modules — safe to import anywhere.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
import type { RuntimeState, StatusPageInput } from "./server-types.js";
|
|
9
|
-
|
|
10
|
-
// ── HTML escaping ─────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
export function escapeHtml(value: unknown): string {
|
|
13
|
-
const text = value == null ? "" : String(value);
|
|
14
|
-
return text
|
|
15
|
-
.replaceAll("&", "&")
|
|
16
|
-
.replaceAll("<", "<")
|
|
17
|
-
.replaceAll(">", ">")
|
|
18
|
-
.replaceAll('"', """)
|
|
19
|
-
.replaceAll("'", "'");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ── Serialisation ─────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
export function serialize(value: unknown): string {
|
|
25
|
-
return JSON.stringify(value ?? null).replaceAll("<", "\\u003c");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Request ID ────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
export function createRequestId(): string {
|
|
31
|
-
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Content negotiation ───────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export function prefersHtmlResponse(request: IncomingMessage): boolean {
|
|
37
|
-
if ((request.url ?? "").startsWith("/api")) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
const accept = request.headers.accept ?? "";
|
|
41
|
-
if (accept.includes("application/json") && !accept.includes("text/html")) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Response helpers ──────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
export function sendJson(response: ServerResponse, statusCode: number, value: unknown): void {
|
|
50
|
-
response.statusCode = statusCode;
|
|
51
|
-
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
52
|
-
response.end(`${JSON.stringify(value, null, 2)}\n`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function sendText(response: ServerResponse, statusCode: number, message: string): void {
|
|
56
|
-
response.statusCode = statusCode;
|
|
57
|
-
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
58
|
-
response.end(message);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function sendXml(response: ServerResponse, statusCode: number, xml: string): void {
|
|
62
|
-
response.statusCode = statusCode;
|
|
63
|
-
response.setHeader("content-type", "application/xml; charset=utf-8");
|
|
64
|
-
response.end(xml);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createWeakEtag(value: string): string {
|
|
68
|
-
const digest = createHash("sha1").update(value).digest("base64url");
|
|
69
|
-
return `W/\"${digest}\"`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const MAX_BODY_BYTES = 1_048_576; // 1 MB
|
|
73
|
-
|
|
74
|
-
export async function parseRequestBody(request: IncomingMessage): Promise<Record<string, unknown>> {
|
|
75
|
-
const chunks: Buffer[] = [];
|
|
76
|
-
let totalBytes = 0;
|
|
77
|
-
|
|
78
|
-
for await (const chunk of request) {
|
|
79
|
-
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
80
|
-
totalBytes += buffer.byteLength;
|
|
81
|
-
if (totalBytes > MAX_BODY_BYTES) {
|
|
82
|
-
throw Object.assign(new Error("Request body too large (limit: 1 MB)."), { statusCode: 413 });
|
|
83
|
-
}
|
|
84
|
-
chunks.push(buffer);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const rawBody = Buffer.concat(chunks).toString("utf8").trim();
|
|
88
|
-
if (!rawBody) {
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const contentType = request.headers["content-type"] ?? "";
|
|
93
|
-
|
|
94
|
-
if (contentType.includes("application/json")) {
|
|
95
|
-
const parsed = JSON.parse(rawBody);
|
|
96
|
-
// Reject prototype pollution attempts.
|
|
97
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
98
|
-
const hasOwn = Object.prototype.hasOwnProperty;
|
|
99
|
-
if (
|
|
100
|
-
hasOwn.call(parsed, "__proto__") ||
|
|
101
|
-
hasOwn.call(parsed, "constructor") ||
|
|
102
|
-
hasOwn.call(parsed, "prototype")
|
|
103
|
-
) {
|
|
104
|
-
throw Object.assign(new Error("Request body contains forbidden keys."), { statusCode: 400 });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return parsed as Record<string, unknown>;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
111
|
-
const entries = [...new URLSearchParams(rawBody).entries()].filter(
|
|
112
|
-
([key]) => key !== "__proto__" && key !== "constructor" && key !== "prototype",
|
|
113
|
-
);
|
|
114
|
-
return Object.fromEntries(entries);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { raw: rawBody };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ── Server event log ──────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
export function pushServerEvent(
|
|
123
|
-
state: RuntimeState,
|
|
124
|
-
level: "info" | "warn" | "error",
|
|
125
|
-
event: string,
|
|
126
|
-
details?: string,
|
|
127
|
-
): void {
|
|
128
|
-
state.serverEvents.unshift({
|
|
129
|
-
at: new Date().toISOString(),
|
|
130
|
-
level,
|
|
131
|
-
event,
|
|
132
|
-
details,
|
|
133
|
-
});
|
|
134
|
-
if (state.serverEvents.length > 120) {
|
|
135
|
-
state.serverEvents.length = 120;
|
|
136
|
-
}
|
|
137
|
-
}
|
package/src/server-websocket.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket server attachment for the Fiyuu runtime.
|
|
3
|
-
* Wires up a ws.WebSocketServer on the HTTP server's upgrade event.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
import { createServer } from "node:http";
|
|
8
|
-
import type { Socket } from "node:net";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { WebSocketServer } from "ws";
|
|
11
|
-
import type { SocketModule, StartServerOptions } from "./server-types.js";
|
|
12
|
-
import { importModule } from "./server-loader.js";
|
|
13
|
-
|
|
14
|
-
export async function attachWebsocketServer(
|
|
15
|
-
server: ReturnType<typeof createServer>,
|
|
16
|
-
options: StartServerOptions,
|
|
17
|
-
websocketPath: string,
|
|
18
|
-
): Promise<string | undefined> {
|
|
19
|
-
if (!options.config?.websocket?.enabled) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const socketModulePath = path.join(options.rootDirectory, "server", "socket.ts");
|
|
24
|
-
const socketModule = existsSync(socketModulePath)
|
|
25
|
-
? ((await importModule(socketModulePath, options.mode)) as SocketModule)
|
|
26
|
-
: null;
|
|
27
|
-
const registration = socketModule?.registerSocketServer?.() ?? {};
|
|
28
|
-
|
|
29
|
-
const wss = new WebSocketServer({
|
|
30
|
-
noServer: true,
|
|
31
|
-
maxPayload: options.config?.websocket?.maxPayloadBytes ?? 64 * 1024,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
wss.on("connection", (socket) => {
|
|
35
|
-
socket.send(
|
|
36
|
-
JSON.stringify({
|
|
37
|
-
type: "fiyuu:ready",
|
|
38
|
-
namespace: registration.namespace ?? "app",
|
|
39
|
-
events: registration.events ?? [],
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
registration.onConnect?.(socket);
|
|
43
|
-
socket.on("message", (message) => {
|
|
44
|
-
registration.onMessage?.(socket, message.toString());
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const handledSockets = new WeakSet<Socket>();
|
|
49
|
-
|
|
50
|
-
server.on("upgrade", (request, socket: Socket, head) => {
|
|
51
|
-
if (handledSockets.has(socket)) return;
|
|
52
|
-
handledSockets.add(socket);
|
|
53
|
-
|
|
54
|
-
if (!request.url) {
|
|
55
|
-
socket.destroy();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const url = new URL(request.url, "http://localhost");
|
|
59
|
-
if (url.pathname !== websocketPath) return;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
wss.handleUpgrade(request, socket, head, (client) => {
|
|
63
|
-
wss.emit("connection", client, request);
|
|
64
|
-
});
|
|
65
|
-
} catch {
|
|
66
|
-
// Another WebSocket handler already processed this socket
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return `ws://localhost:${options.port ?? 4050}${websocketPath}`;
|
|
71
|
-
}
|