@nmvuong92/fluxe 0.8.0 → 0.9.2
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 +2 -0
- package/lib/adapters/express.d.ts +3 -0
- package/lib/adapters/express.js +7 -0
- package/lib/adapters/hono.d.ts +3 -0
- package/lib/adapters/hono.js +48 -0
- package/lib/adapters/nest.d.ts +3 -0
- package/lib/adapters/nest.js +9 -0
- package/lib/core/cli.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/server_factory.d.ts +5 -2
- package/lib/server_factory.js +9 -3
- package/package.json +51 -12
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ npm run test:all # typecheck + 144 unit + integration (selftest2) —
|
|
|
47
47
|
app/ ← DEV sở hữu (sửa thoải mái) — Contract Plane
|
|
48
48
|
cells/ trang/feature (route + loader + view + action/head/layout/guard)
|
|
49
49
|
layouts/ layout dùng chung (nested)
|
|
50
|
+
server.ts server entry — Express/Hono/Nest mount fluxe (mặc định Express)
|
|
50
51
|
backend.ts TẦNG DATA của bạn: interface domain + chọn driver (memory/sqlite/postgres)
|
|
51
52
|
profiles.ts profile resolve render mode (static/island) per môi trường
|
|
52
53
|
contract.ts schema → codegen types TS
|
|
@@ -64,6 +65,7 @@ inject qua `makeServer(…, { backend })`. Engine không bao giờ import ngư
|
|
|
64
65
|
|
|
65
66
|
## Tính năng (tất cả TDD + chạy thật)
|
|
66
67
|
|
|
68
|
+
- **Server** — chạy zero-config (`makeServer`, node:http) HOẶC nhúng vào **Express/Hono/Nest** qua adapter (`@nmvuong92/fluxe/express|hono|nest`)
|
|
67
69
|
- **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender · API mode `?json=1`
|
|
68
70
|
- **Routing** — động `[param]` → `ctx.input` · **nested layouts** · SEO (head/canonical/OG/JSON-LD per cell, `/sitemap.xml`, `/robots.txt`)
|
|
69
71
|
- **Bảo mật (đầy đủ)** — input validation (Zod) · auth password **scrypt** · **RBAC** · **CSRF** double-submit · **rate-limit** token-bucket · error handling không-leak + structured
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createHandler } from "../server_factory.js";
|
|
2
|
+
/* Mount fluxe như middleware (đặt SAU các route riêng của bạn — fluxe là catch-all):
|
|
3
|
+
* app.use(fluxe(manifest, cells, layouts, { backend })); */
|
|
4
|
+
export function fluxe(...args) {
|
|
5
|
+
const handler = createHandler(...args);
|
|
6
|
+
return (req, res, next) => { handler(req, res).catch(next); };
|
|
7
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2026 nmvuong92
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/* Adapter Hono (chạy trên Node qua @hono/node-server). `hono` + `@hono/node-server` là peerDependency.
|
|
4
|
+
* Hono yêu cầu handler trả về Web `Response`; còn createHandler ghi vào node res. Ta dùng một
|
|
5
|
+
* "capture shim": chạy handler vào res đệm (buffer) rồi dựng Response chuẩn → Hono ghi sạch,
|
|
6
|
+
* không đụng nội bộ node-server. (Đánh đổi: buffer toàn response thay vì stream — chấp nhận được
|
|
7
|
+
* cho lớp adapter; muốn stream thì dùng makeServer/Express.) */
|
|
8
|
+
import { Writable } from "node:stream";
|
|
9
|
+
import { createHandler } from "../server_factory.js";
|
|
10
|
+
class CaptureRes extends Writable {
|
|
11
|
+
statusCode = 200;
|
|
12
|
+
headersSent = false;
|
|
13
|
+
headers = {};
|
|
14
|
+
chunks = [];
|
|
15
|
+
writeHead(status, headers) {
|
|
16
|
+
this.statusCode = status;
|
|
17
|
+
if (headers)
|
|
18
|
+
for (const k of Object.keys(headers))
|
|
19
|
+
this.headers[k.toLowerCase()] = headers[k];
|
|
20
|
+
this.headersSent = true;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
setHeader(k, v) { this.headers[k.toLowerCase()] = v; return this; }
|
|
24
|
+
getHeader(k) { return this.headers[k.toLowerCase()]; }
|
|
25
|
+
_write(chunk, _enc, cb) { this.chunks.push(Buffer.from(chunk)); cb(); }
|
|
26
|
+
}
|
|
27
|
+
/* Mount fluxe như catch-all (đặt SAU route Hono riêng của bạn):
|
|
28
|
+
* app.use("*", fluxe(manifest, cells, layouts, { backend }));
|
|
29
|
+
* serve({ fetch: app.fetch, port: 5180 }); // từ @hono/node-server */
|
|
30
|
+
export function fluxe(...args) {
|
|
31
|
+
const handler = createHandler(...args);
|
|
32
|
+
return async (c) => {
|
|
33
|
+
const { incoming } = c.env;
|
|
34
|
+
const res = new CaptureRes();
|
|
35
|
+
const finished = new Promise((resolve) => res.on("finish", resolve));
|
|
36
|
+
await handler(incoming, res);
|
|
37
|
+
await finished;
|
|
38
|
+
const headers = new Headers();
|
|
39
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
40
|
+
if (Array.isArray(v))
|
|
41
|
+
v.forEach((x) => headers.append(k, String(x)));
|
|
42
|
+
else
|
|
43
|
+
headers.set(k, String(v));
|
|
44
|
+
}
|
|
45
|
+
const body = Buffer.concat(res.chunks);
|
|
46
|
+
return new Response(body.length ? body : null, { status: res.statusCode, headers });
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHandler } from "../server_factory.js";
|
|
2
|
+
/* Functional middleware — mount global (khuyên dùng, catch-all đặt sau route Nest):
|
|
3
|
+
* const app = await NestFactory.create(AppModule);
|
|
4
|
+
* app.use(fluxeMiddleware(manifest, cells, layouts, { backend }));
|
|
5
|
+
* (hoặc consumer.apply(...).forRoutes("{*splat}") trong module — Nest 11 dùng wildcard mới). */
|
|
6
|
+
export function fluxeMiddleware(...args) {
|
|
7
|
+
const handler = createHandler(...args);
|
|
8
|
+
return (req, res, next) => { handler(req, res).catch(next); };
|
|
9
|
+
}
|
package/lib/core/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ export const COMMANDS = {
|
|
|
47
47
|
},
|
|
48
48
|
dev: {
|
|
49
49
|
desc: "Sync + resolve + build client + chạy server",
|
|
50
|
-
shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && ${ESBUILD} && tsx
|
|
50
|
+
shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && ${ESBUILD} && tsx app/server.ts`,
|
|
51
51
|
},
|
|
52
52
|
test: {
|
|
53
53
|
desc: "Sync + typecheck + unit + integration",
|
package/lib/index.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export * from "./storage/types.ts";
|
|
|
18
18
|
export { createMemoryStorage } from "./storage/memory.ts";
|
|
19
19
|
export { createLocalStorage } from "./storage/local.ts";
|
|
20
20
|
export { createS3Storage } from "./storage/s3.ts";
|
|
21
|
-
export { makeServer } from "./server_factory.ts";
|
|
21
|
+
export { makeServer, createHandler, type NodeHandler, type MakeServerOpts } from "./server_factory.ts";
|
package/lib/index.js
CHANGED
|
@@ -22,5 +22,5 @@ export * from "./storage/types.js"; // Storage, PutResult, GetResult, safeKey, m
|
|
|
22
22
|
export { createMemoryStorage } from "./storage/memory.js";
|
|
23
23
|
export { createLocalStorage } from "./storage/local.js";
|
|
24
24
|
export { createS3Storage } from "./storage/s3.js"; // adapter tham chiếu (cần @aws-sdk/client-s3)
|
|
25
|
-
export { makeServer } from "./server_factory.js";
|
|
25
|
+
export { makeServer, createHandler } from "./server_factory.js";
|
|
26
26
|
// Backend = USER-OWNED (app/backend.ts) — engine KHÔNG ship driver/domain data nào.
|
package/lib/server_factory.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ type LayoutMap = Record<string, LayoutEntry>;
|
|
|
11
11
|
import { type I18n } from "./core/i18n.ts";
|
|
12
12
|
import { type Storage } from "./storage/types.ts";
|
|
13
13
|
import { type FluxeConfig } from "./core/config.ts";
|
|
14
|
-
export
|
|
14
|
+
export interface MakeServerOpts {
|
|
15
15
|
i18n?: I18n;
|
|
16
16
|
storage?: Storage;
|
|
17
17
|
config?: FluxeConfig;
|
|
18
18
|
backend?: unknown;
|
|
19
|
-
}
|
|
19
|
+
}
|
|
20
|
+
export type NodeHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<unknown>;
|
|
21
|
+
export declare function createHandler(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: MakeServerOpts): NodeHandler;
|
|
22
|
+
export declare function makeServer(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: MakeServerOpts): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
20
23
|
export {};
|
package/lib/server_factory.js
CHANGED
|
@@ -74,7 +74,9 @@ function renderBodyToString(node) {
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
/* createHandler — lõi request framework-agnostic: trả về handler Node (req,res).
|
|
78
|
+
* Dùng trực tiếp cho adapter Express/Hono/Nest; makeServer chỉ bọc bằng http.createServer. */
|
|
79
|
+
export function createHandler(manifest, cells, layouts = {}, opts = {}) {
|
|
78
80
|
const i18n = opts.i18n;
|
|
79
81
|
const storage = opts.storage;
|
|
80
82
|
const config = opts.config ?? loadConfig(); // default ← ENV (FLUXE_*) ← override
|
|
@@ -109,7 +111,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
109
111
|
const recorder = createRecorder(); // request log — chạy mỗi request → eager (luôn dùng)
|
|
110
112
|
const renderCache = createRenderCache({ maxKeys: config.renderCache.maxKeys }); // FLUXE_RENDERCACHE_MAX_KEYS
|
|
111
113
|
let clientJs; // ý A: đọc dist/client.js 1 lần (zero-copy: tái dùng buffer)
|
|
112
|
-
return
|
|
114
|
+
return async (req, res) => {
|
|
113
115
|
const url = new URL(req.url, "http://localhost");
|
|
114
116
|
const start = Date.now();
|
|
115
117
|
res.on("finish", () => recorder.record({ method: req.method ?? "?", path: url.pathname, status: res.statusCode, ms: Date.now() - start, ts: start }));
|
|
@@ -351,5 +353,9 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
351
353
|
// Action (rpc) luôn nhận lỗi dạng JSON.
|
|
352
354
|
sendError(res, wantsJson || url.pathname.startsWith("/__action/"), err);
|
|
353
355
|
}
|
|
354
|
-
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/* makeServer — đường zero-config: bọc createHandler bằng http.createServer (giữ API cũ). */
|
|
359
|
+
export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
360
|
+
return http.createServer(createHandler(manifest, cells, layouts, opts));
|
|
355
361
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nmvuong92/fluxe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "fluxe — khung fullstack tối giản, một runtime TS (RCA: Resolved Cell Architecture).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "nmvuong92",
|
|
@@ -30,9 +30,17 @@
|
|
|
30
30
|
"types": "./lib/core/jobs.d.ts",
|
|
31
31
|
"default": "./lib/core/jobs.js"
|
|
32
32
|
},
|
|
33
|
-
"./
|
|
34
|
-
"types": "./lib/
|
|
35
|
-
"default": "./lib/
|
|
33
|
+
"./express": {
|
|
34
|
+
"types": "./lib/adapters/express.d.ts",
|
|
35
|
+
"default": "./lib/adapters/express.js"
|
|
36
|
+
},
|
|
37
|
+
"./hono": {
|
|
38
|
+
"types": "./lib/adapters/hono.d.ts",
|
|
39
|
+
"default": "./lib/adapters/hono.js"
|
|
40
|
+
},
|
|
41
|
+
"./nest": {
|
|
42
|
+
"types": "./lib/adapters/nest.d.ts",
|
|
43
|
+
"default": "./lib/adapters/nest.js"
|
|
36
44
|
}
|
|
37
45
|
},
|
|
38
46
|
"files": [
|
|
@@ -48,8 +56,8 @@
|
|
|
48
56
|
"typecheck": "tsc --noEmit",
|
|
49
57
|
"sync": "tsx scripts/sync.ts",
|
|
50
58
|
"build:client": "npm run sync && esbuild src/client.tsx --bundle --format=esm --outfile=dist/client.js --jsx=automatic --loader:.tsx=tsx",
|
|
51
|
-
"dev": "npm run build:client && tsx
|
|
52
|
-
"dev:
|
|
59
|
+
"dev": "npm run build:client && tsx app/server.ts",
|
|
60
|
+
"dev:node": "npm run build:client && tsx src/server.tsx",
|
|
53
61
|
"test": "tsx src/selftest2.ts",
|
|
54
62
|
"test:unit": "node --experimental-sqlite --import tsx --test '{src,app}/**/*.test.ts'",
|
|
55
63
|
"test:cells": "node --experimental-sqlite --import tsx --test 'app/cells/**/*.test.ts'",
|
|
@@ -67,16 +75,47 @@
|
|
|
67
75
|
},
|
|
68
76
|
"peerDependencies": {
|
|
69
77
|
"react": "^18 || ^19",
|
|
70
|
-
"react-dom": "^18 || ^19"
|
|
78
|
+
"react-dom": "^18 || ^19",
|
|
79
|
+
"express": "^4 || ^5",
|
|
80
|
+
"hono": "^4",
|
|
81
|
+
"@hono/node-server": "^1 || ^2",
|
|
82
|
+
"@nestjs/common": "^10 || ^11",
|
|
83
|
+
"@nestjs/core": "^10 || ^11"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"express": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"hono": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@hono/node-server": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@nestjs/common": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@nestjs/core": {
|
|
99
|
+
"optional": true
|
|
100
|
+
}
|
|
71
101
|
},
|
|
72
102
|
"devDependencies": {
|
|
73
|
-
"
|
|
74
|
-
"
|
|
103
|
+
"@hono/node-server": "^2.0.6",
|
|
104
|
+
"@nestjs/common": "^11.1.27",
|
|
105
|
+
"@nestjs/core": "^11.1.27",
|
|
106
|
+
"@nestjs/platform-express": "^11.1.27",
|
|
107
|
+
"@types/express": "^5.0.6",
|
|
108
|
+
"@types/node": "^20",
|
|
109
|
+
"@types/react": "^18",
|
|
110
|
+
"@types/react-dom": "^18",
|
|
75
111
|
"esbuild": "^0.23",
|
|
112
|
+
"express": "^5.2.1",
|
|
113
|
+
"hono": "^4.12.27",
|
|
76
114
|
"react": "^18",
|
|
77
115
|
"react-dom": "^18",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
116
|
+
"reflect-metadata": "^0.2.2",
|
|
117
|
+
"rxjs": "^7.8.2",
|
|
118
|
+
"tsx": "^4",
|
|
119
|
+
"typescript": "^5"
|
|
81
120
|
}
|
|
82
121
|
}
|