@nmvuong92/fluxe 0.8.0 → 0.9.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 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,3 @@
1
+ import type { RequestHandler } from "express";
2
+ import { createHandler } from "../server_factory.ts";
3
+ export declare function fluxe(...args: Parameters<typeof createHandler>): RequestHandler;
@@ -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,3 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import { createHandler } from "../server_factory.ts";
3
+ export declare function fluxe(...args: Parameters<typeof createHandler>): MiddlewareHandler;
@@ -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,3 @@
1
+ import type { NestMiddleware } from "@nestjs/common";
2
+ import { createHandler } from "../server_factory.ts";
3
+ export declare function fluxeMiddleware(...args: Parameters<typeof createHandler>): NestMiddleware["use"];
@@ -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 src/server.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.
@@ -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 declare function makeServer(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: {
14
+ export interface MakeServerOpts {
15
15
  i18n?: I18n;
16
16
  storage?: Storage;
17
17
  config?: FluxeConfig;
18
18
  backend?: unknown;
19
- }): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
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 {};
@@ -74,7 +74,9 @@ function renderBodyToString(node) {
74
74
  });
75
75
  });
76
76
  }
77
- export function makeServer(manifest, cells, layouts = {}, opts = {}) {
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 http.createServer(async (req, res) => {
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.8.0",
3
+ "version": "0.9.1",
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
- "./sqlite": {
34
- "types": "./lib/backends/sqlite.d.ts",
35
- "default": "./lib/backends/sqlite.js"
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 src/server.tsx",
52
- "dev:remote": "npm run build:client && FLUXE_BACKEND=remote tsx src/server.tsx",
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
- "typescript": "^5",
74
- "tsx": "^4",
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
- "@types/node": "^20",
79
- "@types/react": "^18",
80
- "@types/react-dom": "^18"
116
+ "reflect-metadata": "^0.2.2",
117
+ "rxjs": "^7.8.2",
118
+ "tsx": "^4",
119
+ "typescript": "^5"
81
120
  }
82
121
  }