@anchorlib/vite-ssr 1.0.0-beta.24

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.
@@ -0,0 +1,30 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __commonJS = (cb, mod) => function() {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
+ key = keys[i];
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
28
+
29
+ //#endregion
30
+ export { __commonJS, __require, __toESM };
@@ -0,0 +1,68 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/index.d.ts
4
+ type ViteSSROptions = {
5
+ /** Path to the router module. Must `export default` a `Router` instance. */
6
+ router: string;
7
+ /** Path to the root layout module. Must `export default` a `RouteComponent`. */
8
+ layout: string;
9
+ /**
10
+ * Path to the renderer module (e.g., `'@anchorlib/react/ssr'`).
11
+ * Must export `createSSR(router, layout) => SSRRenderer`.
12
+ */
13
+ renderer: string;
14
+ /**
15
+ * IRPC configuration. If provided, POST requests to the transport
16
+ * endpoint are routed through the HTTPRouter.
17
+ */
18
+ irpc?: {
19
+ /** IRPC instance. String loads `export default`, object loads a named export. */
20
+ module: ModuleRef;
21
+ /** HTTP Transport instance. */
22
+ transport: ModuleRef;
23
+ /** WebSocket Transport instance (optional). */
24
+ wsTransport?: ModuleRef;
25
+ /** Handler modules to load (e.g., ['./src/pages/constructor.ts']). */
26
+ handlers?: string[];
27
+ };
28
+ /** Placeholder for rendered head. Defaults to `<!--ssr-head-->`. */
29
+ headTag?: string;
30
+ /** Placeholder for rendered body. Defaults to `<!--ssr-outlet-->`. */
31
+ bodyTag?: string;
32
+ };
33
+ /** A module reference — string for default export, object for named export. */
34
+ type ModuleRef = string | {
35
+ path: string;
36
+ name: string;
37
+ };
38
+ /**
39
+ * Vite plugin for Anchor SSR.
40
+ *
41
+ * Replaces manual `server.ts` and `entry-server.tsx` with a single plugin call.
42
+ * Handles SSR renderer construction, IRPC routing, request isolation,
43
+ * abort signal propagation, and template transformation.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // vite.config.ts
48
+ * import { airSSR } from '@anchorlib/vite-ssr';
49
+ *
50
+ * export default defineConfig({
51
+ * plugins: [
52
+ * airSSR({
53
+ * router: './src/lib/router.ts',
54
+ * layout: './src/pages/layout.tsx',
55
+ * renderer: '@anchorlib/react/ssr',
56
+ * irpc: {
57
+ * module: './src/lib/irpc.ts',
58
+ * transport: './src/lib/transport.ts',
59
+ * handlers: ['./src/pages/constructor.ts'],
60
+ * },
61
+ * }),
62
+ * ],
63
+ * });
64
+ * ```
65
+ */
66
+ declare function airSSR(options: ViteSSROptions): Plugin;
67
+ //#endregion
68
+ export { ViteSSROptions, airSSR };
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ import { sendWebResponse, toWebRequest } from "./utils.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { decodeCookies, getContext, setCookieContext } from "@anchorlib/core";
5
+
6
+ //#region src/index.ts
7
+ /**
8
+ * Vite plugin for Anchor SSR.
9
+ *
10
+ * Replaces manual `server.ts` and `entry-server.tsx` with a single plugin call.
11
+ * Handles SSR renderer construction, IRPC routing, request isolation,
12
+ * abort signal propagation, and template transformation.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // vite.config.ts
17
+ * import { airSSR } from '@anchorlib/vite-ssr';
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [
21
+ * airSSR({
22
+ * router: './src/lib/router.ts',
23
+ * layout: './src/pages/layout.tsx',
24
+ * renderer: '@anchorlib/react/ssr',
25
+ * irpc: {
26
+ * module: './src/lib/irpc.ts',
27
+ * transport: './src/lib/transport.ts',
28
+ * handlers: ['./src/pages/constructor.ts'],
29
+ * },
30
+ * }),
31
+ * ],
32
+ * });
33
+ * ```
34
+ */
35
+ function airSSR(options) {
36
+ const { router: routerPath, layout: layoutPath, renderer: rendererPath, irpc: irpcConfig, headTag = "<!--ssr-head-->", bodyTag = "<!--ssr-outlet-->" } = options;
37
+ let router;
38
+ let templatePath;
39
+ let rendererFactory;
40
+ return {
41
+ name: "air-ssr",
42
+ configureServer(server) {
43
+ templatePath = path.resolve(server.config.root, "index.html");
44
+ const ready = (async () => {
45
+ await server.ssrLoadModule("@irpclib/irpc/server").catch(() => {});
46
+ rendererFactory = (await server.ssrLoadModule(rendererPath)).createSSR;
47
+ if (irpcConfig) {
48
+ router = await initRouter(server, irpcConfig);
49
+ if (irpcConfig.wsTransport) await initWsRouter(server, irpcConfig);
50
+ const { IRPC_STORE } = await server.ssrLoadModule("@irpclib/irpc");
51
+ IRPC_STORE.subscribe(() => {
52
+ IRPC_STORE.print();
53
+ });
54
+ }
55
+ })();
56
+ return () => {
57
+ server.middlewares.use(async (req, res, next) => {
58
+ await ready;
59
+ const controller = new AbortController();
60
+ req.on("close", () => controller.abort());
61
+ try {
62
+ const url = req.originalUrl ?? req.url ?? "/";
63
+ if (router && req.method === "POST" && url.startsWith(router.transport.endpoint)) {
64
+ if (irpcConfig?.handlers) for (const handler of irpcConfig.handlers) await server.ssrLoadModule(handler);
65
+ const request = toWebRequest(req, controller);
66
+ const cookie$1 = req.headers.cookie ?? "";
67
+ await sendWebResponse(res, await router.resolve(request, [["cookie", cookie$1]]));
68
+ return;
69
+ }
70
+ if (url.includes(".")) return next();
71
+ let template = fs.readFileSync(templatePath, "utf-8");
72
+ template = await server.transformIndexHtml(url, template);
73
+ const { default: pageRouter } = await server.ssrLoadModule(routerPath);
74
+ const { default: RootLayout } = await server.ssrLoadModule(layoutPath);
75
+ const render = await rendererFactory(pageRouter, RootLayout);
76
+ const cookie = req.headers.cookie ?? "";
77
+ let ssrResult;
78
+ if (router) {
79
+ const cookieJar = decodeCookies(cookie);
80
+ ssrResult = await router.isolate(() => render(url, cookie, void 0, controller, true), controller, [["cookie", cookie]], () => {
81
+ setCookieContext(cookieJar);
82
+ });
83
+ } else ssrResult = await render(url, cookie, void 0, controller);
84
+ const { html, head, status, redirect, cookies } = ssrResult;
85
+ if (redirect) {
86
+ res.writeHead(302, { Location: redirect });
87
+ res.end();
88
+ return;
89
+ }
90
+ const headers = { "Content-Type": "text/html" };
91
+ if (cookies?.length) headers["Set-Cookie"] = cookies;
92
+ const page = template.replace(headTag, head).replace(bodyTag, html);
93
+ res.writeHead(status ?? 200, headers);
94
+ res.end(page);
95
+ } catch (error) {
96
+ next(error);
97
+ }
98
+ });
99
+ };
100
+ }
101
+ };
102
+ }
103
+ /**
104
+ * Loads an export from a module reference.
105
+ * String path loads the default export; { path, name } loads a named export.
106
+ */
107
+ async function loadExport(server, ref) {
108
+ if (typeof ref === "string") return (await server.ssrLoadModule(ref)).default;
109
+ return (await server.ssrLoadModule(ref.path))[ref.name];
110
+ }
111
+ /**
112
+ * Initializes the IRPC HTTP router by loading the module and handlers.
113
+ */
114
+ async function initRouter(server, config) {
115
+ try {
116
+ const irpc = await loadExport(server, config.module);
117
+ const transport = await loadExport(server, config.transport);
118
+ if (!irpc || !transport) {
119
+ server.config.logger.warn("[air-ssr] IRPC module and transport are required.");
120
+ return;
121
+ }
122
+ if (config.handlers) for (const handler of config.handlers) await server.ssrLoadModule(handler);
123
+ const { HTTPRouter } = await server.ssrLoadModule("@irpclib/http/router");
124
+ const router = new HTTPRouter(irpc, transport);
125
+ router.use(() => {
126
+ setCookieContext(decodeCookies(getContext("cookie", "")));
127
+ });
128
+ server.config.logger.info(`[air-ssr] IRPC HTTP router initialized at ${transport.endpoint}`);
129
+ return router;
130
+ } catch (error) {
131
+ server.config.logger.error("[air-ssr] Failed to initialize IRPC HTTP router:");
132
+ server.config.logger.error(String(error));
133
+ return;
134
+ }
135
+ }
136
+ /**
137
+ * Initializes the IRPC WebSocket router and attaches it to the Vite dev server.
138
+ *
139
+ * Creates a WebSocketServer on the same HTTP server (noServer mode),
140
+ * intercepts upgrade requests at the WS transport endpoint, and routes
141
+ * messages through WebSocketRouter.
142
+ */
143
+ async function initWsRouter(server, config) {
144
+ if (!config.wsTransport) return;
145
+ try {
146
+ const irpc = await loadExport(server, config.module);
147
+ const wsTransport = await loadExport(server, config.wsTransport);
148
+ if (!irpc || !wsTransport) {
149
+ server.config.logger.warn("[air-ssr] IRPC module and wsTransport are required for WebSocket.");
150
+ return;
151
+ }
152
+ const { WebSocketRouter } = await server.ssrLoadModule("@irpclib/ws/router");
153
+ const wsRouter = new WebSocketRouter(irpc, wsTransport);
154
+ wsRouter.use(() => {
155
+ setCookieContext(decodeCookies(getContext("cookie", "")));
156
+ });
157
+ const endpoint = wsTransport.endpoint;
158
+ const { WebSocketServer } = await import("./node_modules/.bun/ws@8.21.0/node_modules/ws/wrapper.js");
159
+ const wss = new WebSocketServer({ noServer: true });
160
+ server.httpServer?.on("upgrade", (req, socket, head) => {
161
+ if (req.url === endpoint) wss.handleUpgrade(req, socket, head, (ws) => {
162
+ wss.emit("connection", ws, req);
163
+ });
164
+ });
165
+ wss.on("connection", (ws, req) => {
166
+ const cookie = req.headers.cookie ?? "";
167
+ ws.on("message", async (data) => {
168
+ if (config.handlers) for (const handler of config.handlers) await server.ssrLoadModule(handler);
169
+ const message = data instanceof ArrayBuffer ? data : data.toString();
170
+ await wsRouter.resolve(message, ws, [["cookie", cookie]]);
171
+ });
172
+ ws.on("close", () => {
173
+ wsRouter.disconnect();
174
+ });
175
+ });
176
+ server.config.logger.info(`[air-ssr] IRPC WebSocket router initialized at ${endpoint}`);
177
+ return wsRouter;
178
+ } catch (error) {
179
+ server.config.logger.error("[air-ssr] Failed to initialize IRPC WebSocket router:");
180
+ server.config.logger.error(String(error));
181
+ return;
182
+ }
183
+ }
184
+
185
+ //#endregion
186
+ export { airSSR };
@@ -0,0 +1,27 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ //#region src/utils.d.ts
4
+
5
+ /**
6
+ * Converts a Node.js IncomingMessage into a Web Standard Request.
7
+ *
8
+ * Wires the AbortController so that `req.on('close')` (client disconnect)
9
+ * propagates as `request.signal.aborted`, enabling IRPC stream cleanup.
10
+ *
11
+ * @param req - The incoming Node.js HTTP request.
12
+ * @param controller - AbortController to wire to the request signal.
13
+ * @returns A Web Standard Request.
14
+ */
15
+ declare function toWebRequest(req: IncomingMessage, controller: AbortController): Request;
16
+ /**
17
+ * Pipes a Web Standard Response back into a Node.js ServerResponse.
18
+ *
19
+ * Handles both streaming and non-streaming responses. For streaming responses,
20
+ * pipes the ReadableStream with proper backpressure handling.
21
+ *
22
+ * @param res - The Node.js ServerResponse to write to.
23
+ * @param response - The Web Standard Response to send.
24
+ */
25
+ declare function sendWebResponse(res: ServerResponse, response: Response): Promise<void>;
26
+ //#endregion
27
+ export { sendWebResponse, toWebRequest };
package/dist/utils.js ADDED
@@ -0,0 +1,54 @@
1
+ import { Readable } from "node:stream";
2
+
3
+ //#region src/utils.ts
4
+ /**
5
+ * Converts a Node.js IncomingMessage into a Web Standard Request.
6
+ *
7
+ * Wires the AbortController so that `req.on('close')` (client disconnect)
8
+ * propagates as `request.signal.aborted`, enabling IRPC stream cleanup.
9
+ *
10
+ * @param req - The incoming Node.js HTTP request.
11
+ * @param controller - AbortController to wire to the request signal.
12
+ * @returns A Web Standard Request.
13
+ */
14
+ function toWebRequest(req, controller) {
15
+ const { method = "GET", headers, url = "/" } = req;
16
+ const origin = `http://${headers.host ?? "localhost"}`;
17
+ const fullUrl = new URL(url, origin).href;
18
+ const isBodyMethod = method !== "GET" && method !== "HEAD";
19
+ return new Request(fullUrl, {
20
+ method,
21
+ headers,
22
+ body: isBodyMethod ? Readable.toWeb(req) : void 0,
23
+ signal: controller.signal,
24
+ duplex: "half"
25
+ });
26
+ }
27
+ /**
28
+ * Pipes a Web Standard Response back into a Node.js ServerResponse.
29
+ *
30
+ * Handles both streaming and non-streaming responses. For streaming responses,
31
+ * pipes the ReadableStream with proper backpressure handling.
32
+ *
33
+ * @param res - The Node.js ServerResponse to write to.
34
+ * @param response - The Web Standard Response to send.
35
+ */
36
+ async function sendWebResponse(res, response) {
37
+ response.headers.forEach((value, key) => {
38
+ if (key.toLowerCase() !== "content-encoding") res.setHeader(key, value);
39
+ });
40
+ res.writeHead(response.status, response.statusText);
41
+ if (response.body) {
42
+ const readable = Readable.fromWeb(response.body);
43
+ readable.pipe(res);
44
+ res.on("close", () => {
45
+ if (!readable.destroyed) readable.destroy();
46
+ });
47
+ } else {
48
+ const text = await response.text();
49
+ res.end(text);
50
+ }
51
+ }
52
+
53
+ //#endregion
54
+ export { sendWebResponse, toWebRequest };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@anchorlib/vite-ssr",
4
+ "version": "1.0.0-beta.24",
5
+ "description": "Vite plugin that handles SSR rendering and IRPC routing for AIR Stack applications",
6
+ "keywords": ["vite", "ssr", "anchor", "irpc"],
7
+ "author": "Nanang Mahdaen El Agung <mahdaen@gmail.com>",
8
+ "homepage": "https://github.com/beerush-id/anchor",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/beerush-id/anchor.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/beerush-id/anchor/issues"
16
+ },
17
+ "types": "./dist/index.d.ts",
18
+ "module": "./dist/index.js",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": ["dist"],
26
+ "directories": {
27
+ "dist": "./dist"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "peerDependencies": {
33
+ "@anchorlib/core": "^1.0.0-beta.24",
34
+ "@irpclib/http": "^1.0.0-beta.24",
35
+ "@irpclib/irpc": "^1.0.0-beta.24",
36
+ "@irpclib/ws": "^1.0.0-beta.24",
37
+ "typescript": "^5.9.3",
38
+ "vite": ">=5"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@anchorlib/core": {
42
+ "optional": true
43
+ },
44
+ "@irpclib/irpc": {
45
+ "optional": true
46
+ },
47
+ "@irpclib/http": {
48
+ "optional": true
49
+ },
50
+ "@irpclib/ws": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@biomejs/biome": "2.3.3",
56
+ "@types/ws": "latest",
57
+ "publint": "0.3.15",
58
+ "rimraf": "6.0.1",
59
+ "tsdown": "0.15.9",
60
+ "vite": "8.0.10",
61
+ "ws": "latest"
62
+ },
63
+ "scripts": {
64
+ "dev": "rimraf dist && tsdown --watch ./src",
65
+ "clean": "rimraf dist",
66
+ "build": "rimraf dist && tsdown && publint",
67
+ "prepublish": "rimraf dist && tsdown && publint",
68
+ "format": "biome format --write"
69
+ }
70
+ }
package/readme.md ADDED
@@ -0,0 +1,57 @@
1
+ # @anchorlib/vite-ssr
2
+
3
+ Vite plugin that handles SSR rendering and IRPC routing for AIR Stack applications.
4
+
5
+ ## Features
6
+
7
+ - **SSR + IRPC** — Server-side rendering and RPC handling in one plugin
8
+ - **Zero Boilerplate** — No manual server entry or renderer wiring needed
9
+ - **Request Isolation** — Cookies, abort signals, and hooks scoped per request
10
+ - **Hot Module Replacement** — Edit handlers and components without restarting the server
11
+ - **Framework Agnostic** — Works with any renderer that exports `createSSR`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @anchorlib/vite-ssr
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ // vite.config.ts
23
+ import { airSSR } from '@anchorlib/vite-ssr';
24
+
25
+ export default defineConfig({
26
+ plugins: [
27
+ react(),
28
+ airSSR({
29
+ router: './src/lib/router.ts',
30
+ layout: './src/pages/layout.tsx',
31
+ renderer: '@anchorlib/react/ssr',
32
+ irpc: {
33
+ module: './src/lib/module.ts',
34
+ handlers: ['./src/pages/constructor.ts'],
35
+ },
36
+ }),
37
+ ],
38
+ });
39
+ ```
40
+
41
+ ### Options
42
+
43
+ | Option | Required | Description |
44
+ |--------|----------|-------------|
45
+ | `router` | Yes | Path to the router module. Must `export default` a `Router` instance. |
46
+ | `layout` | Yes | Path to the root layout module. Must `export default` a `RouteComponent`. |
47
+ | `renderer` | Yes | Path to the renderer module (e.g., `'@anchorlib/react/ssr'`). Must export `createSSR`. |
48
+ | `irpc.module` | No | IRPC instance. String for default export, `{ path, name }` for named export. |
49
+ | `irpc.transport` | No | HTTP Transport instance. Same format as `module`. |
50
+ | `irpc.wsTransport` | No | WebSocket Transport instance. Same format as `module`. |
51
+ | `irpc.handlers` | No | Handler modules to load (e.g., `['./src/pages/constructor.ts']`). |
52
+ | `headTag` | No | Placeholder for rendered head. Defaults to `<!--ssr-head-->`. |
53
+ | `bodyTag` | No | Placeholder for rendered body. Defaults to `<!--ssr-outlet-->`. |
54
+
55
+ ## License
56
+
57
+ MIT