@effex/vite-plugin 1.0.0

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 ADDED
@@ -0,0 +1,137 @@
1
+ # @effex/vite-plugin
2
+
3
+ Vite plugin for Effex SSR applications. Provides server-code stripping for client builds and an SSR dev server with HMR.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add -D @effex/vite-plugin
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ // vite.config.ts
15
+ import { defineConfig } from "vite";
16
+ import { effexPlatform } from "@effex/vite-plugin";
17
+
18
+ export default defineConfig({
19
+ plugins: [
20
+ effexPlatform({ entry: "src/vite-entry.ts" }),
21
+ ],
22
+ });
23
+ ```
24
+
25
+ > Only needed when using `@effex/platform` for SSR. Pure SPA apps don't need this plugin.
26
+
27
+ ## What It Does
28
+
29
+ The plugin provides two capabilities:
30
+
31
+ ### 1. Server-Code Stripping (Client Builds)
32
+
33
+ When Vite builds the client bundle, the plugin removes server-only code from route definitions so that server dependencies (database clients, file system access, etc.) don't get bundled into the browser.
34
+
35
+ **Loaders** — the first argument to `Route.get()` is replaced with `null`:
36
+
37
+ ```ts
38
+ // Source
39
+ Route.get(
40
+ ({ params }) => db.getUser(params.id), // server-only loader
41
+ (user) => UserPage({ user }),
42
+ )
43
+
44
+ // Client bundle
45
+ Route.get(
46
+ null, // stripped
47
+ (user) => UserPage({ user }),
48
+ )
49
+ ```
50
+
51
+ **Mutation handlers** — the handler function in `Route.post/put/del()` is replaced with a throw stub. The action key is preserved since the client needs it to compute action URLs:
52
+
53
+ ```ts
54
+ // Source
55
+ Route.post("create", (body) =>
56
+ Effect.gen(function* () {
57
+ const svc = yield* PostService;
58
+ return yield* svc.createPost(body.content);
59
+ }),
60
+ )
61
+
62
+ // Client bundle
63
+ Route.post("create", () => { throw new Error("server only"); })
64
+ ```
65
+
66
+ This stripping only applies to client builds — SSR builds and dev-mode SSR modules keep the full server code.
67
+
68
+ ### 2. SSR Dev Server (Dev Mode)
69
+
70
+ When `entry` is provided, the plugin runs an SSR dev server during `vite dev`:
71
+
72
+ - Intercepts incoming requests (skips Vite internal paths and static assets)
73
+ - Loads your entry module via `server.ssrLoadModule()` for HMR support
74
+ - Calls your entry's `render(request)` function with a standard Web Request
75
+ - Injects Vite's HMR client into HTML responses
76
+ - Displays readable error pages with stack traces on failure
77
+
78
+ ## Entry Module
79
+
80
+ The entry file must export a `render` function:
81
+
82
+ ```ts
83
+ // src/vite-entry.ts
84
+ import { HttpApp, HttpRouter } from "@effect/platform";
85
+ import { Layer } from "effect";
86
+ import { Platform } from "@effex/platform";
87
+
88
+ import { App } from "./App.js";
89
+ import { router } from "./routes.js";
90
+
91
+ const effexRoutes = Platform.toHttpRoutes(router, {
92
+ app: App,
93
+ document: {
94
+ title: "My App",
95
+ scripts: ["/src/client.ts"],
96
+ head: '<link rel="stylesheet" href="/src/styles.css">',
97
+ },
98
+ });
99
+
100
+ const app = HttpRouter.empty.pipe(HttpRouter.concat(effexRoutes));
101
+
102
+ const { handler } = HttpApp.toWebHandlerLayer(app, MyServiceLayer);
103
+
104
+ export async function render(request: Request): Promise<Response> {
105
+ return handler(request);
106
+ }
107
+ ```
108
+
109
+ The `render` function receives a standard Web `Request` and must return a `Response`. Use `HttpApp.toWebHandlerLayer` from `@effect/platform` to bridge Effect's HTTP handlers to the Web API.
110
+
111
+ ## Options
112
+
113
+ ```ts
114
+ effexPlatform({
115
+ // Path to SSR entry module. Enables the dev server when provided.
116
+ entry: "src/vite-entry.ts",
117
+
118
+ // File patterns to apply stripping to (default: /\.(tsx?|jsx?)$/)
119
+ include: /\.(tsx?|jsx?)$/,
120
+
121
+ // File patterns to exclude from stripping
122
+ exclude: /\.test\./,
123
+ })
124
+ ```
125
+
126
+ | Option | Type | Default | Description |
127
+ |---|---|---|---|
128
+ | `entry` | `string` | — | SSR entry module path. Enables dev server when set. |
129
+ | `include` | `RegExp` | `/\.(tsx?\|jsx?)$/` | Files to apply server-code stripping to |
130
+ | `exclude` | `RegExp` | — | Files to exclude from stripping |
131
+
132
+ ## API Reference
133
+
134
+ | Export | Description |
135
+ |---|---|
136
+ | `effexPlatform(options?)` | Create the Vite plugin |
137
+ | `stripServerCode(code)` | Strip server code from a string (exported for testing) |
package/dist/index.cjs ADDED
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ effexPlatform: () => effexPlatform
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/plugin.ts
38
+ var path = __toESM(require("path"), 1);
39
+ var effexPlatform = (options = {}) => {
40
+ const include = options.include ?? /\.(tsx?|jsx?)$/;
41
+ const exclude = options.exclude;
42
+ let isSsr = false;
43
+ let root;
44
+ let entryPath = null;
45
+ return {
46
+ name: "effex-platform",
47
+ configResolved(config) {
48
+ root = config.root;
49
+ isSsr = !!config.build?.ssr;
50
+ if (options.entry) {
51
+ entryPath = path.resolve(root, options.entry);
52
+ }
53
+ },
54
+ // -------------------------------------------------------------------------
55
+ // Server-code stripping (client builds only)
56
+ // -------------------------------------------------------------------------
57
+ transform(code, id, options2) {
58
+ if (isSsr || options2?.ssr) return null;
59
+ if (!include.test(id)) return null;
60
+ if (exclude && exclude.test(id)) return null;
61
+ if (!code.includes("Route.get") && !code.includes("Route.post") && !code.includes("Route.put") && !code.includes("Route.del")) {
62
+ return null;
63
+ }
64
+ const transformed = stripServerCode(code);
65
+ if (transformed === code) return null;
66
+ return { code: transformed, map: null };
67
+ },
68
+ // -------------------------------------------------------------------------
69
+ // SSR dev server (dev mode only, when entry is provided)
70
+ // -------------------------------------------------------------------------
71
+ configureServer(server) {
72
+ if (!entryPath) return;
73
+ const entry = entryPath;
74
+ return () => {
75
+ server.middlewares.use(async (req, res, next) => {
76
+ const url = req.originalUrl || req.url || "/";
77
+ const normalizedUrl = url === "/" || url === "/index.html" ? "/" : url;
78
+ if (url.startsWith("/@") || url.startsWith("/__vite") || url.startsWith("/node_modules/") || url.startsWith("/src/") || url.includes(".") && !url.endsWith("/") && url !== "/index.html") {
79
+ return next();
80
+ }
81
+ try {
82
+ const serverModule = await server.ssrLoadModule(entry);
83
+ if (typeof serverModule.render !== "function") {
84
+ throw new Error(
85
+ `Server entry "${options.entry}" must export a "render(request: Request) => Promise<Response>" function`
86
+ );
87
+ }
88
+ const protocol = "http";
89
+ const host = req.headers.host || "localhost";
90
+ const webUrl = new URL(normalizedUrl, `${protocol}://${host}`);
91
+ let body;
92
+ if (req.method !== "GET" && req.method !== "HEAD") {
93
+ body = await new Promise((resolve2) => {
94
+ let data = "";
95
+ req.on("data", (chunk) => data += chunk);
96
+ req.on("end", () => resolve2(data));
97
+ });
98
+ }
99
+ const webRequest = new Request(webUrl.href, {
100
+ method: req.method,
101
+ headers: Object.entries(req.headers).reduce(
102
+ (acc, [key, value]) => {
103
+ if (value)
104
+ acc[key] = Array.isArray(value) ? value.join(", ") : value;
105
+ return acc;
106
+ },
107
+ {}
108
+ ),
109
+ body
110
+ });
111
+ const response = await serverModule.render(webRequest);
112
+ res.statusCode = response.status;
113
+ response.headers.forEach((value, key) => {
114
+ res.setHeader(key, value);
115
+ });
116
+ const responseBody = await response.text();
117
+ const contentType = response.headers.get("content-type") || "";
118
+ if (contentType.includes("text/html")) {
119
+ const transformedHtml = await server.transformIndexHtml(
120
+ normalizedUrl,
121
+ responseBody
122
+ );
123
+ res.setHeader(
124
+ "content-length",
125
+ Buffer.byteLength(transformedHtml)
126
+ );
127
+ res.end(transformedHtml);
128
+ } else {
129
+ res.end(responseBody);
130
+ }
131
+ } catch (e) {
132
+ server.ssrFixStacktrace(e);
133
+ console.error("[effex-platform] Error:", e);
134
+ res.statusCode = 500;
135
+ res.setHeader("Content-Type", "text/html");
136
+ res.end(`
137
+ <!DOCTYPE html>
138
+ <html>
139
+ <head><title>SSR Error</title></head>
140
+ <body>
141
+ <h1>Server Error</h1>
142
+ <pre style="color: red; white-space: pre-wrap;">${escapeHtml(e.stack || e.message)}</pre>
143
+ </body>
144
+ </html>
145
+ `);
146
+ }
147
+ });
148
+ };
149
+ }
150
+ };
151
+ };
152
+ var stripServerCode = (code) => {
153
+ let result = code;
154
+ result = stripLoaders(result);
155
+ result = stripHandlers(result);
156
+ return result;
157
+ };
158
+ var stripLoaders = (code) => {
159
+ const pattern = /Route\.get\s*\(/g;
160
+ let result = code;
161
+ let match;
162
+ let offset = 0;
163
+ pattern.lastIndex = 0;
164
+ while ((match = pattern.exec(code)) !== null) {
165
+ const callStart = match.index + offset;
166
+ const argsStart = callStart + match[0].length;
167
+ const firstArgEnd = findArgEnd(result, argsStart);
168
+ if (firstArgEnd === -1) continue;
169
+ const before = result.slice(0, argsStart);
170
+ const after = result.slice(firstArgEnd);
171
+ const replacement = "null";
172
+ const oldLen = firstArgEnd - argsStart;
173
+ result = before + replacement + after;
174
+ offset += replacement.length - oldLen;
175
+ pattern.lastIndex = match.index + match[0].length;
176
+ }
177
+ return result;
178
+ };
179
+ var stripHandlers = (code) => {
180
+ const pattern = /Route\.(post|put|del)\s*\(/g;
181
+ let result = code;
182
+ let match;
183
+ let offset = 0;
184
+ pattern.lastIndex = 0;
185
+ while ((match = pattern.exec(code)) !== null) {
186
+ const callStart = match.index + offset;
187
+ const argsStart = callStart + match[0].length;
188
+ const firstArgEnd = findArgEnd(result, argsStart);
189
+ if (firstArgEnd === -1) continue;
190
+ let secondArgStart = firstArgEnd;
191
+ while (secondArgStart < result.length && /[\s,]/.test(result[secondArgStart])) {
192
+ secondArgStart++;
193
+ }
194
+ const secondArgEnd = findArgEnd(result, secondArgStart);
195
+ if (secondArgEnd === -1) continue;
196
+ const before = result.slice(0, secondArgStart);
197
+ const after = result.slice(secondArgEnd);
198
+ const replacement = '() => { throw new Error("server only"); }';
199
+ const oldLen = secondArgEnd - secondArgStart;
200
+ result = before + replacement + after;
201
+ offset += replacement.length - oldLen;
202
+ pattern.lastIndex = match.index + match[0].length;
203
+ }
204
+ return result;
205
+ };
206
+ var findArgEnd = (code, start) => {
207
+ let depth = 0;
208
+ let i = start;
209
+ while (i < code.length) {
210
+ const ch = code[i];
211
+ if (ch === '"' || ch === "'" || ch === "`") {
212
+ i = skipString(code, i);
213
+ continue;
214
+ }
215
+ if (ch === "/" && code[i + 1] === "/") {
216
+ i = code.indexOf("\n", i);
217
+ if (i === -1) return -1;
218
+ i++;
219
+ continue;
220
+ }
221
+ if (ch === "/" && code[i + 1] === "*") {
222
+ i = code.indexOf("*/", i);
223
+ if (i === -1) return -1;
224
+ i += 2;
225
+ continue;
226
+ }
227
+ if (ch === "(" || ch === "{" || ch === "[") {
228
+ depth++;
229
+ } else if (ch === ")" || ch === "}" || ch === "]") {
230
+ if (depth === 0) {
231
+ return i;
232
+ }
233
+ depth--;
234
+ } else if (ch === "," && depth === 0) {
235
+ return i;
236
+ }
237
+ i++;
238
+ }
239
+ return -1;
240
+ };
241
+ var skipString = (code, start) => {
242
+ const quote = code[start];
243
+ let i = start + 1;
244
+ while (i < code.length) {
245
+ const ch = code[i];
246
+ if (ch === "\\") {
247
+ i += 2;
248
+ continue;
249
+ }
250
+ if (quote === "`" && ch === "$" && code[i + 1] === "{") {
251
+ i += 2;
252
+ let templateDepth = 1;
253
+ while (i < code.length && templateDepth > 0) {
254
+ if (code[i] === "{") templateDepth++;
255
+ else if (code[i] === "}") templateDepth--;
256
+ else if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
257
+ i = skipString(code, i);
258
+ continue;
259
+ }
260
+ i++;
261
+ }
262
+ continue;
263
+ }
264
+ if (ch === quote) {
265
+ return i + 1;
266
+ }
267
+ i++;
268
+ }
269
+ return i;
270
+ };
271
+ function escapeHtml(str) {
272
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
273
+ }
274
+ // Annotate the CommonJS export names for ESM import in node:
275
+ 0 && (module.exports = {
276
+ effexPlatform
277
+ });
278
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/plugin.ts"],"sourcesContent":["/**\n * @effex/vite-plugin\n *\n * Vite plugin for Effex Platform SSR applications.\n *\n * `effexPlatform()` provides:\n * - Server-code stripping from client builds (loaders + handlers)\n * - SSR dev server with HMR (when `entry` is provided)\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from \"vite\";\n * import { effexPlatform } from \"@effex/vite-plugin\";\n *\n * export default defineConfig({\n * plugins: [\n * effexPlatform({ entry: \"src/server-entry.ts\" }),\n * ],\n * });\n * ```\n *\n * @packageDocumentation\n */\n\nexport { effexPlatform, type EffexPlatformOptions } from \"./plugin.js\";\n","import * as path from \"node:path\";\n\nimport type { Plugin, ViteDevServer } from \"vite\";\n\n/**\n * Options for the Effex Platform Vite plugin.\n */\nexport interface EffexPlatformOptions {\n /**\n * Path to the SSR entry module that exports a `render` function.\n * The render function should have the signature: (request: Request) => Promise<Response>\n *\n * When provided, the plugin runs an SSR dev server with HMR in dev mode.\n * When omitted, only the server-code stripping transform is applied.\n *\n * @example \"src/vite-entry.ts\"\n */\n readonly entry?: string;\n /**\n * File patterns to apply the server-code stripping transform to.\n * Defaults to all .ts/.tsx/.js/.jsx files.\n */\n readonly include?: RegExp;\n /**\n * File patterns to exclude from the transform.\n */\n readonly exclude?: RegExp;\n}\n\n/**\n * Vite plugin for @effex/platform SSR applications.\n *\n * Provides two capabilities:\n *\n * 1. **Server-code stripping** (build time) — Removes loader and handler function\n * bodies from client builds so server-only dependencies (database services, etc.)\n * don't get bundled into the client.\n * - `Route.get(loader, render)` → `Route.get(null, render)`\n * - `Route.post(\"key\", handler)` → `Route.post(\"key\", () => { throw ... })`\n *\n * 2. **SSR dev server** (dev mode, when `entry` is provided) — Intercepts requests,\n * renders pages via `vite.ssrLoadModule`, and injects Vite's HMR client.\n *\n * Only needed when using @effex/platform for SSR. Pure SPAs that run loaders\n * client-side should NOT use this plugin.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from \"vite\";\n * import { effexPlatform } from \"@effex/vite-plugin\";\n *\n * export default defineConfig({\n * plugins: [\n * effexPlatform({ entry: \"src/server-entry.ts\" }),\n * ],\n * });\n * ```\n */\nexport const effexPlatform = (options: EffexPlatformOptions = {}): Plugin => {\n const include = options.include ?? /\\.(tsx?|jsx?)$/;\n const exclude = options.exclude;\n let isSsr = false;\n let root: string;\n let entryPath: string | null = null;\n\n return {\n name: \"effex-platform\",\n\n configResolved(config) {\n root = config.root;\n isSsr = !!config.build?.ssr;\n if (options.entry) {\n entryPath = path.resolve(root, options.entry);\n }\n },\n\n // -------------------------------------------------------------------------\n // Server-code stripping (client builds only)\n // -------------------------------------------------------------------------\n\n transform(code, id, options) {\n // Never strip server code in SSR builds or SSR-loaded modules (dev)\n if (isSsr || options?.ssr) return null;\n\n // Filter by include/exclude patterns\n if (!include.test(id)) return null;\n if (exclude && exclude.test(id)) return null;\n\n // Quick bail — only transform files that reference Route\n if (\n !code.includes(\"Route.get\") &&\n !code.includes(\"Route.post\") &&\n !code.includes(\"Route.put\") &&\n !code.includes(\"Route.del\")\n ) {\n return null;\n }\n\n const transformed = stripServerCode(code);\n if (transformed === code) return null;\n\n return { code: transformed, map: null };\n },\n\n // -------------------------------------------------------------------------\n // SSR dev server (dev mode only, when entry is provided)\n // -------------------------------------------------------------------------\n\n configureServer(server: ViteDevServer) {\n if (!entryPath) return;\n\n const entry = entryPath;\n\n // Return a function to run after Vite's internal middleware\n return () => {\n server.middlewares.use(async (req, res, next) => {\n // Use originalUrl to get the URL before Vite's historyFallback rewrites it\n const url =\n (req as { originalUrl?: string }).originalUrl || req.url || \"/\";\n\n // Normalize index.html to root path\n const normalizedUrl =\n url === \"/\" || url === \"/index.html\" ? \"/\" : url;\n\n // Skip Vite internal requests and static assets\n if (\n url.startsWith(\"/@\") ||\n url.startsWith(\"/__vite\") ||\n url.startsWith(\"/node_modules/\") ||\n url.startsWith(\"/src/\") ||\n (url.includes(\".\") && !url.endsWith(\"/\") && url !== \"/index.html\")\n ) {\n return next();\n }\n\n try {\n // Load the server entry module with HMR\n const serverModule = await server.ssrLoadModule(entry);\n\n if (typeof serverModule.render !== \"function\") {\n throw new Error(\n `Server entry \"${options.entry}\" must export a \"render(request: Request) => Promise<Response>\" function`,\n );\n }\n\n // Create a Web Request from the Node request\n const protocol = \"http\";\n const host = req.headers.host || \"localhost\";\n const webUrl = new URL(normalizedUrl, `${protocol}://${host}`);\n\n // Handle request body for POST/PUT/etc\n let body: string | undefined;\n if (req.method !== \"GET\" && req.method !== \"HEAD\") {\n body = await new Promise<string>((resolve) => {\n let data = \"\";\n req.on(\"data\", (chunk: string) => (data += chunk));\n req.on(\"end\", () => resolve(data));\n });\n }\n\n const webRequest = new Request(webUrl.href, {\n method: req.method,\n headers: Object.entries(req.headers).reduce(\n (acc, [key, value]) => {\n if (value)\n acc[key] = Array.isArray(value) ? value.join(\", \") : value;\n return acc;\n },\n {} as Record<string, string>,\n ),\n body: body,\n });\n\n // Call the render function — returns a Web Response\n const response: Response = await serverModule.render(webRequest);\n\n // Forward status and headers\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n const responseBody = await response.text();\n const contentType = response.headers.get(\"content-type\") || \"\";\n\n // Inject Vite's HMR client into HTML responses\n if (contentType.includes(\"text/html\")) {\n const transformedHtml = await server.transformIndexHtml(\n normalizedUrl,\n responseBody,\n );\n // Recalculate content-length since transformIndexHtml may inject scripts\n res.setHeader(\n \"content-length\",\n Buffer.byteLength(transformedHtml),\n );\n res.end(transformedHtml);\n } else {\n res.end(responseBody);\n }\n } catch (e) {\n server.ssrFixStacktrace(e as Error);\n console.error(\"[effex-platform] Error:\", e);\n\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(`\n <!DOCTYPE html>\n <html>\n <head><title>SSR Error</title></head>\n <body>\n <h1>Server Error</h1>\n <pre style=\"color: red; white-space: pre-wrap;\">${escapeHtml((e as Error).stack || (e as Error).message)}</pre>\n </body>\n </html>\n `);\n }\n });\n };\n },\n };\n};\n\n// =============================================================================\n// Server-code stripping internals\n// =============================================================================\n\n/**\n * Strip server-only code from route definitions.\n *\n * Transforms:\n * - `Route.get(loaderFn, renderFn)` → `Route.get(null, renderFn)`\n * - `Route.post(\"key\", handlerFn)` → `Route.post(\"key\", () => { throw new Error(\"server only\"); })`\n * - Same for Route.put and Route.del\n */\nexport const stripServerCode = (code: string): string => {\n let result = code;\n result = stripLoaders(result);\n result = stripHandlers(result);\n return result;\n};\n\n/**\n * Replace the first argument (loader) in Route.get() calls with null.\n */\nconst stripLoaders = (code: string): string => {\n const pattern = /Route\\.get\\s*\\(/g;\n let result = code;\n let match: RegExpExecArray | null;\n let offset = 0;\n\n pattern.lastIndex = 0;\n\n while ((match = pattern.exec(code)) !== null) {\n const callStart = match.index + offset;\n const argsStart = callStart + match[0].length;\n\n const firstArgEnd = findArgEnd(result, argsStart);\n if (firstArgEnd === -1) continue;\n\n const before = result.slice(0, argsStart);\n const after = result.slice(firstArgEnd);\n const replacement = \"null\";\n const oldLen = firstArgEnd - argsStart;\n result = before + replacement + after;\n offset += replacement.length - oldLen;\n\n pattern.lastIndex = match.index + match[0].length;\n }\n\n return result;\n};\n\n/**\n * Replace the handler function (second argument) in Route.post/put/del() calls with a no-op.\n * Keeps the key (first argument) since Outlet reads it to compute action paths.\n */\nconst stripHandlers = (code: string): string => {\n const pattern = /Route\\.(post|put|del)\\s*\\(/g;\n let result = code;\n let match: RegExpExecArray | null;\n let offset = 0;\n\n pattern.lastIndex = 0;\n\n while ((match = pattern.exec(code)) !== null) {\n const callStart = match.index + offset;\n const argsStart = callStart + match[0].length;\n\n const firstArgEnd = findArgEnd(result, argsStart);\n if (firstArgEnd === -1) continue;\n\n let secondArgStart = firstArgEnd;\n while (\n secondArgStart < result.length &&\n /[\\s,]/.test(result[secondArgStart])\n ) {\n secondArgStart++;\n }\n\n const secondArgEnd = findArgEnd(result, secondArgStart);\n if (secondArgEnd === -1) continue;\n\n const before = result.slice(0, secondArgStart);\n const after = result.slice(secondArgEnd);\n const replacement = '() => { throw new Error(\"server only\"); }';\n const oldLen = secondArgEnd - secondArgStart;\n result = before + replacement + after;\n offset += replacement.length - oldLen;\n\n pattern.lastIndex = match.index + match[0].length;\n }\n\n return result;\n};\n\n/**\n * Find the end position of a single argument starting at `start`.\n * Handles nested parens, braces, brackets, template literals, and strings.\n * Returns the index right after the argument (at the comma or closing paren).\n */\nconst findArgEnd = (code: string, start: number): number => {\n let depth = 0;\n let i = start;\n\n while (i < code.length) {\n const ch = code[i];\n\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(code, i);\n continue;\n }\n\n if (ch === \"/\" && code[i + 1] === \"/\") {\n i = code.indexOf(\"\\n\", i);\n if (i === -1) return -1;\n i++;\n continue;\n }\n\n if (ch === \"/\" && code[i + 1] === \"*\") {\n i = code.indexOf(\"*/\", i);\n if (i === -1) return -1;\n i += 2;\n continue;\n }\n\n if (ch === \"(\" || ch === \"{\" || ch === \"[\") {\n depth++;\n } else if (ch === \")\" || ch === \"}\" || ch === \"]\") {\n if (depth === 0) {\n return i;\n }\n depth--;\n } else if (ch === \",\" && depth === 0) {\n return i;\n }\n\n i++;\n }\n\n return -1;\n};\n\n/**\n * Skip past a string literal (single-quoted, double-quoted, or template).\n * Returns the index after the closing quote.\n */\nconst skipString = (code: string, start: number): number => {\n const quote = code[start];\n let i = start + 1;\n\n while (i < code.length) {\n const ch = code[i];\n\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n\n if (quote === \"`\" && ch === \"$\" && code[i + 1] === \"{\") {\n i += 2;\n let templateDepth = 1;\n while (i < code.length && templateDepth > 0) {\n if (code[i] === \"{\") templateDepth++;\n else if (code[i] === \"}\") templateDepth--;\n else if (code[i] === '\"' || code[i] === \"'\" || code[i] === \"`\") {\n i = skipString(code, i);\n continue;\n }\n i++;\n }\n continue;\n }\n\n if (ch === quote) {\n return i + 1;\n }\n\n i++;\n }\n\n return i;\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,WAAsB;AA2Df,IAAM,gBAAgB,CAAC,UAAgC,CAAC,MAAc;AAC3E,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,QAAQ;AACxB,MAAI,QAAQ;AACZ,MAAI;AACJ,MAAI,YAA2B;AAE/B,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,eAAe,QAAQ;AACrB,aAAO,OAAO;AACd,cAAQ,CAAC,CAAC,OAAO,OAAO;AACxB,UAAI,QAAQ,OAAO;AACjB,oBAAiB,aAAQ,MAAM,QAAQ,KAAK;AAAA,MAC9C;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAMA,UAAU,MAAM,IAAIA,UAAS;AAE3B,UAAI,SAASA,UAAS,IAAK,QAAO;AAGlC,UAAI,CAAC,QAAQ,KAAK,EAAE,EAAG,QAAO;AAC9B,UAAI,WAAW,QAAQ,KAAK,EAAE,EAAG,QAAO;AAGxC,UACE,CAAC,KAAK,SAAS,WAAW,KAC1B,CAAC,KAAK,SAAS,YAAY,KAC3B,CAAC,KAAK,SAAS,WAAW,KAC1B,CAAC,KAAK,SAAS,WAAW,GAC1B;AACA,eAAO;AAAA,MACT;AAEA,YAAM,cAAc,gBAAgB,IAAI;AACxC,UAAI,gBAAgB,KAAM,QAAO;AAEjC,aAAO,EAAE,MAAM,aAAa,KAAK,KAAK;AAAA,IACxC;AAAA;AAAA;AAAA;AAAA,IAMA,gBAAgB,QAAuB;AACrC,UAAI,CAAC,UAAW;AAEhB,YAAM,QAAQ;AAGd,aAAO,MAAM;AACX,eAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAE/C,gBAAM,MACH,IAAiC,eAAe,IAAI,OAAO;AAG9D,gBAAM,gBACJ,QAAQ,OAAO,QAAQ,gBAAgB,MAAM;AAG/C,cACE,IAAI,WAAW,IAAI,KACnB,IAAI,WAAW,SAAS,KACxB,IAAI,WAAW,gBAAgB,KAC/B,IAAI,WAAW,OAAO,KACrB,IAAI,SAAS,GAAG,KAAK,CAAC,IAAI,SAAS,GAAG,KAAK,QAAQ,eACpD;AACA,mBAAO,KAAK;AAAA,UACd;AAEA,cAAI;AAEF,kBAAM,eAAe,MAAM,OAAO,cAAc,KAAK;AAErD,gBAAI,OAAO,aAAa,WAAW,YAAY;AAC7C,oBAAM,IAAI;AAAA,gBACR,iBAAiB,QAAQ,KAAK;AAAA,cAChC;AAAA,YACF;AAGA,kBAAM,WAAW;AACjB,kBAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,kBAAM,SAAS,IAAI,IAAI,eAAe,GAAG,QAAQ,MAAM,IAAI,EAAE;AAG7D,gBAAI;AACJ,gBAAI,IAAI,WAAW,SAAS,IAAI,WAAW,QAAQ;AACjD,qBAAO,MAAM,IAAI,QAAgB,CAACC,aAAY;AAC5C,oBAAI,OAAO;AACX,oBAAI,GAAG,QAAQ,CAAC,UAAmB,QAAQ,KAAM;AACjD,oBAAI,GAAG,OAAO,MAAMA,SAAQ,IAAI,CAAC;AAAA,cACnC,CAAC;AAAA,YACH;AAEA,kBAAM,aAAa,IAAI,QAAQ,OAAO,MAAM;AAAA,cAC1C,QAAQ,IAAI;AAAA,cACZ,SAAS,OAAO,QAAQ,IAAI,OAAO,EAAE;AAAA,gBACnC,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM;AACrB,sBAAI;AACF,wBAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;AACvD,yBAAO;AAAA,gBACT;AAAA,gBACA,CAAC;AAAA,cACH;AAAA,cACA;AAAA,YACF,CAAC;AAGD,kBAAM,WAAqB,MAAM,aAAa,OAAO,UAAU;AAG/D,gBAAI,aAAa,SAAS;AAC1B,qBAAS,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACvC,kBAAI,UAAU,KAAK,KAAK;AAAA,YAC1B,CAAC;AAED,kBAAM,eAAe,MAAM,SAAS,KAAK;AACzC,kBAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAG5D,gBAAI,YAAY,SAAS,WAAW,GAAG;AACrC,oBAAM,kBAAkB,MAAM,OAAO;AAAA,gBACnC;AAAA,gBACA;AAAA,cACF;AAEA,kBAAI;AAAA,gBACF;AAAA,gBACA,OAAO,WAAW,eAAe;AAAA,cACnC;AACA,kBAAI,IAAI,eAAe;AAAA,YACzB,OAAO;AACL,kBAAI,IAAI,YAAY;AAAA,YACtB;AAAA,UACF,SAAS,GAAG;AACV,mBAAO,iBAAiB,CAAU;AAClC,oBAAQ,MAAM,2BAA2B,CAAC;AAE1C,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,WAAW;AACzC,gBAAI,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oEAMgD,WAAY,EAAY,SAAU,EAAY,OAAO,CAAC;AAAA;AAAA;AAAA,aAG7G;AAAA,UACH;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAcO,IAAM,kBAAkB,CAAC,SAAyB;AACvD,MAAI,SAAS;AACb,WAAS,aAAa,MAAM;AAC5B,WAAS,cAAc,MAAM;AAC7B,SAAO;AACT;AAKA,IAAM,eAAe,CAAC,SAAyB;AAC7C,QAAM,UAAU;AAChB,MAAI,SAAS;AACb,MAAI;AACJ,MAAI,SAAS;AAEb,UAAQ,YAAY;AAEpB,UAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,UAAM,YAAY,MAAM,QAAQ;AAChC,UAAM,YAAY,YAAY,MAAM,CAAC,EAAE;AAEvC,UAAM,cAAc,WAAW,QAAQ,SAAS;AAChD,QAAI,gBAAgB,GAAI;AAExB,UAAM,SAAS,OAAO,MAAM,GAAG,SAAS;AACxC,UAAM,QAAQ,OAAO,MAAM,WAAW;AACtC,UAAM,cAAc;AACpB,UAAM,SAAS,cAAc;AAC7B,aAAS,SAAS,cAAc;AAChC,cAAU,YAAY,SAAS;AAE/B,YAAQ,YAAY,MAAM,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC7C;AAEA,SAAO;AACT;AAMA,IAAM,gBAAgB,CAAC,SAAyB;AAC9C,QAAM,UAAU;AAChB,MAAI,SAAS;AACb,MAAI;AACJ,MAAI,SAAS;AAEb,UAAQ,YAAY;AAEpB,UAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,UAAM,YAAY,MAAM,QAAQ;AAChC,UAAM,YAAY,YAAY,MAAM,CAAC,EAAE;AAEvC,UAAM,cAAc,WAAW,QAAQ,SAAS;AAChD,QAAI,gBAAgB,GAAI;AAExB,QAAI,iBAAiB;AACrB,WACE,iBAAiB,OAAO,UACxB,QAAQ,KAAK,OAAO,cAAc,CAAC,GACnC;AACA;AAAA,IACF;AAEA,UAAM,eAAe,WAAW,QAAQ,cAAc;AACtD,QAAI,iBAAiB,GAAI;AAEzB,UAAM,SAAS,OAAO,MAAM,GAAG,cAAc;AAC7C,UAAM,QAAQ,OAAO,MAAM,YAAY;AACvC,UAAM,cAAc;AACpB,UAAM,SAAS,eAAe;AAC9B,aAAS,SAAS,cAAc;AAChC,cAAU,YAAY,SAAS;AAE/B,YAAQ,YAAY,MAAM,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC7C;AAEA,SAAO;AACT;AAOA,IAAM,aAAa,CAAC,MAAc,UAA0B;AAC1D,MAAI,QAAQ;AACZ,MAAI,IAAI;AAER,SAAO,IAAI,KAAK,QAAQ;AACtB,UAAM,KAAK,KAAK,CAAC;AAEjB,QAAI,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAC1C,UAAI,WAAW,MAAM,CAAC;AACtB;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,KAAK,IAAI,CAAC,MAAM,KAAK;AACrC,UAAI,KAAK,QAAQ,MAAM,CAAC;AACxB,UAAI,MAAM,GAAI,QAAO;AACrB;AACA;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,KAAK,IAAI,CAAC,MAAM,KAAK;AACrC,UAAI,KAAK,QAAQ,MAAM,CAAC;AACxB,UAAI,MAAM,GAAI,QAAO;AACrB,WAAK;AACL;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAC1C;AAAA,IACF,WAAW,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AACjD,UAAI,UAAU,GAAG;AACf,eAAO;AAAA,MACT;AACA;AAAA,IACF,WAAW,OAAO,OAAO,UAAU,GAAG;AACpC,aAAO;AAAA,IACT;AAEA;AAAA,EACF;AAEA,SAAO;AACT;AAMA,IAAM,aAAa,CAAC,MAAc,UAA0B;AAC1D,QAAM,QAAQ,KAAK,KAAK;AACxB,MAAI,IAAI,QAAQ;AAEhB,SAAO,IAAI,KAAK,QAAQ;AACtB,UAAM,KAAK,KAAK,CAAC;AAEjB,QAAI,OAAO,MAAM;AACf,WAAK;AACL;AAAA,IACF;AAEA,QAAI,UAAU,OAAO,OAAO,OAAO,KAAK,IAAI,CAAC,MAAM,KAAK;AACtD,WAAK;AACL,UAAI,gBAAgB;AACpB,aAAO,IAAI,KAAK,UAAU,gBAAgB,GAAG;AAC3C,YAAI,KAAK,CAAC,MAAM,IAAK;AAAA,iBACZ,KAAK,CAAC,MAAM,IAAK;AAAA,iBACjB,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK;AAC9D,cAAI,WAAW,MAAM,CAAC;AACtB;AAAA,QACF;AACA;AAAA,MACF;AACA;AAAA,IACF;AAEA,QAAI,OAAO,OAAO;AAChB,aAAO,IAAI;AAAA,IACb;AAEA;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;","names":["options","resolve"]}
@@ -0,0 +1,59 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /**
4
+ * Options for the Effex Platform Vite plugin.
5
+ */
6
+ interface EffexPlatformOptions {
7
+ /**
8
+ * Path to the SSR entry module that exports a `render` function.
9
+ * The render function should have the signature: (request: Request) => Promise<Response>
10
+ *
11
+ * When provided, the plugin runs an SSR dev server with HMR in dev mode.
12
+ * When omitted, only the server-code stripping transform is applied.
13
+ *
14
+ * @example "src/vite-entry.ts"
15
+ */
16
+ readonly entry?: string;
17
+ /**
18
+ * File patterns to apply the server-code stripping transform to.
19
+ * Defaults to all .ts/.tsx/.js/.jsx files.
20
+ */
21
+ readonly include?: RegExp;
22
+ /**
23
+ * File patterns to exclude from the transform.
24
+ */
25
+ readonly exclude?: RegExp;
26
+ }
27
+ /**
28
+ * Vite plugin for @effex/platform SSR applications.
29
+ *
30
+ * Provides two capabilities:
31
+ *
32
+ * 1. **Server-code stripping** (build time) — Removes loader and handler function
33
+ * bodies from client builds so server-only dependencies (database services, etc.)
34
+ * don't get bundled into the client.
35
+ * - `Route.get(loader, render)` → `Route.get(null, render)`
36
+ * - `Route.post("key", handler)` → `Route.post("key", () => { throw ... })`
37
+ *
38
+ * 2. **SSR dev server** (dev mode, when `entry` is provided) — Intercepts requests,
39
+ * renders pages via `vite.ssrLoadModule`, and injects Vite's HMR client.
40
+ *
41
+ * Only needed when using @effex/platform for SSR. Pure SPAs that run loaders
42
+ * client-side should NOT use this plugin.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // vite.config.ts
47
+ * import { defineConfig } from "vite";
48
+ * import { effexPlatform } from "@effex/vite-plugin";
49
+ *
50
+ * export default defineConfig({
51
+ * plugins: [
52
+ * effexPlatform({ entry: "src/server-entry.ts" }),
53
+ * ],
54
+ * });
55
+ * ```
56
+ */
57
+ declare const effexPlatform: (options?: EffexPlatformOptions) => Plugin;
58
+
59
+ export { type EffexPlatformOptions, effexPlatform };
@@ -0,0 +1,59 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /**
4
+ * Options for the Effex Platform Vite plugin.
5
+ */
6
+ interface EffexPlatformOptions {
7
+ /**
8
+ * Path to the SSR entry module that exports a `render` function.
9
+ * The render function should have the signature: (request: Request) => Promise<Response>
10
+ *
11
+ * When provided, the plugin runs an SSR dev server with HMR in dev mode.
12
+ * When omitted, only the server-code stripping transform is applied.
13
+ *
14
+ * @example "src/vite-entry.ts"
15
+ */
16
+ readonly entry?: string;
17
+ /**
18
+ * File patterns to apply the server-code stripping transform to.
19
+ * Defaults to all .ts/.tsx/.js/.jsx files.
20
+ */
21
+ readonly include?: RegExp;
22
+ /**
23
+ * File patterns to exclude from the transform.
24
+ */
25
+ readonly exclude?: RegExp;
26
+ }
27
+ /**
28
+ * Vite plugin for @effex/platform SSR applications.
29
+ *
30
+ * Provides two capabilities:
31
+ *
32
+ * 1. **Server-code stripping** (build time) — Removes loader and handler function
33
+ * bodies from client builds so server-only dependencies (database services, etc.)
34
+ * don't get bundled into the client.
35
+ * - `Route.get(loader, render)` → `Route.get(null, render)`
36
+ * - `Route.post("key", handler)` → `Route.post("key", () => { throw ... })`
37
+ *
38
+ * 2. **SSR dev server** (dev mode, when `entry` is provided) — Intercepts requests,
39
+ * renders pages via `vite.ssrLoadModule`, and injects Vite's HMR client.
40
+ *
41
+ * Only needed when using @effex/platform for SSR. Pure SPAs that run loaders
42
+ * client-side should NOT use this plugin.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // vite.config.ts
47
+ * import { defineConfig } from "vite";
48
+ * import { effexPlatform } from "@effex/vite-plugin";
49
+ *
50
+ * export default defineConfig({
51
+ * plugins: [
52
+ * effexPlatform({ entry: "src/server-entry.ts" }),
53
+ * ],
54
+ * });
55
+ * ```
56
+ */
57
+ declare const effexPlatform: (options?: EffexPlatformOptions) => Plugin;
58
+
59
+ export { type EffexPlatformOptions, effexPlatform };
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ // src/plugin.ts
2
+ import * as path from "path";
3
+ var effexPlatform = (options = {}) => {
4
+ const include = options.include ?? /\.(tsx?|jsx?)$/;
5
+ const exclude = options.exclude;
6
+ let isSsr = false;
7
+ let root;
8
+ let entryPath = null;
9
+ return {
10
+ name: "effex-platform",
11
+ configResolved(config) {
12
+ root = config.root;
13
+ isSsr = !!config.build?.ssr;
14
+ if (options.entry) {
15
+ entryPath = path.resolve(root, options.entry);
16
+ }
17
+ },
18
+ // -------------------------------------------------------------------------
19
+ // Server-code stripping (client builds only)
20
+ // -------------------------------------------------------------------------
21
+ transform(code, id, options2) {
22
+ if (isSsr || options2?.ssr) return null;
23
+ if (!include.test(id)) return null;
24
+ if (exclude && exclude.test(id)) return null;
25
+ if (!code.includes("Route.get") && !code.includes("Route.post") && !code.includes("Route.put") && !code.includes("Route.del")) {
26
+ return null;
27
+ }
28
+ const transformed = stripServerCode(code);
29
+ if (transformed === code) return null;
30
+ return { code: transformed, map: null };
31
+ },
32
+ // -------------------------------------------------------------------------
33
+ // SSR dev server (dev mode only, when entry is provided)
34
+ // -------------------------------------------------------------------------
35
+ configureServer(server) {
36
+ if (!entryPath) return;
37
+ const entry = entryPath;
38
+ return () => {
39
+ server.middlewares.use(async (req, res, next) => {
40
+ const url = req.originalUrl || req.url || "/";
41
+ const normalizedUrl = url === "/" || url === "/index.html" ? "/" : url;
42
+ if (url.startsWith("/@") || url.startsWith("/__vite") || url.startsWith("/node_modules/") || url.startsWith("/src/") || url.includes(".") && !url.endsWith("/") && url !== "/index.html") {
43
+ return next();
44
+ }
45
+ try {
46
+ const serverModule = await server.ssrLoadModule(entry);
47
+ if (typeof serverModule.render !== "function") {
48
+ throw new Error(
49
+ `Server entry "${options.entry}" must export a "render(request: Request) => Promise<Response>" function`
50
+ );
51
+ }
52
+ const protocol = "http";
53
+ const host = req.headers.host || "localhost";
54
+ const webUrl = new URL(normalizedUrl, `${protocol}://${host}`);
55
+ let body;
56
+ if (req.method !== "GET" && req.method !== "HEAD") {
57
+ body = await new Promise((resolve2) => {
58
+ let data = "";
59
+ req.on("data", (chunk) => data += chunk);
60
+ req.on("end", () => resolve2(data));
61
+ });
62
+ }
63
+ const webRequest = new Request(webUrl.href, {
64
+ method: req.method,
65
+ headers: Object.entries(req.headers).reduce(
66
+ (acc, [key, value]) => {
67
+ if (value)
68
+ acc[key] = Array.isArray(value) ? value.join(", ") : value;
69
+ return acc;
70
+ },
71
+ {}
72
+ ),
73
+ body
74
+ });
75
+ const response = await serverModule.render(webRequest);
76
+ res.statusCode = response.status;
77
+ response.headers.forEach((value, key) => {
78
+ res.setHeader(key, value);
79
+ });
80
+ const responseBody = await response.text();
81
+ const contentType = response.headers.get("content-type") || "";
82
+ if (contentType.includes("text/html")) {
83
+ const transformedHtml = await server.transformIndexHtml(
84
+ normalizedUrl,
85
+ responseBody
86
+ );
87
+ res.setHeader(
88
+ "content-length",
89
+ Buffer.byteLength(transformedHtml)
90
+ );
91
+ res.end(transformedHtml);
92
+ } else {
93
+ res.end(responseBody);
94
+ }
95
+ } catch (e) {
96
+ server.ssrFixStacktrace(e);
97
+ console.error("[effex-platform] Error:", e);
98
+ res.statusCode = 500;
99
+ res.setHeader("Content-Type", "text/html");
100
+ res.end(`
101
+ <!DOCTYPE html>
102
+ <html>
103
+ <head><title>SSR Error</title></head>
104
+ <body>
105
+ <h1>Server Error</h1>
106
+ <pre style="color: red; white-space: pre-wrap;">${escapeHtml(e.stack || e.message)}</pre>
107
+ </body>
108
+ </html>
109
+ `);
110
+ }
111
+ });
112
+ };
113
+ }
114
+ };
115
+ };
116
+ var stripServerCode = (code) => {
117
+ let result = code;
118
+ result = stripLoaders(result);
119
+ result = stripHandlers(result);
120
+ return result;
121
+ };
122
+ var stripLoaders = (code) => {
123
+ const pattern = /Route\.get\s*\(/g;
124
+ let result = code;
125
+ let match;
126
+ let offset = 0;
127
+ pattern.lastIndex = 0;
128
+ while ((match = pattern.exec(code)) !== null) {
129
+ const callStart = match.index + offset;
130
+ const argsStart = callStart + match[0].length;
131
+ const firstArgEnd = findArgEnd(result, argsStart);
132
+ if (firstArgEnd === -1) continue;
133
+ const before = result.slice(0, argsStart);
134
+ const after = result.slice(firstArgEnd);
135
+ const replacement = "null";
136
+ const oldLen = firstArgEnd - argsStart;
137
+ result = before + replacement + after;
138
+ offset += replacement.length - oldLen;
139
+ pattern.lastIndex = match.index + match[0].length;
140
+ }
141
+ return result;
142
+ };
143
+ var stripHandlers = (code) => {
144
+ const pattern = /Route\.(post|put|del)\s*\(/g;
145
+ let result = code;
146
+ let match;
147
+ let offset = 0;
148
+ pattern.lastIndex = 0;
149
+ while ((match = pattern.exec(code)) !== null) {
150
+ const callStart = match.index + offset;
151
+ const argsStart = callStart + match[0].length;
152
+ const firstArgEnd = findArgEnd(result, argsStart);
153
+ if (firstArgEnd === -1) continue;
154
+ let secondArgStart = firstArgEnd;
155
+ while (secondArgStart < result.length && /[\s,]/.test(result[secondArgStart])) {
156
+ secondArgStart++;
157
+ }
158
+ const secondArgEnd = findArgEnd(result, secondArgStart);
159
+ if (secondArgEnd === -1) continue;
160
+ const before = result.slice(0, secondArgStart);
161
+ const after = result.slice(secondArgEnd);
162
+ const replacement = '() => { throw new Error("server only"); }';
163
+ const oldLen = secondArgEnd - secondArgStart;
164
+ result = before + replacement + after;
165
+ offset += replacement.length - oldLen;
166
+ pattern.lastIndex = match.index + match[0].length;
167
+ }
168
+ return result;
169
+ };
170
+ var findArgEnd = (code, start) => {
171
+ let depth = 0;
172
+ let i = start;
173
+ while (i < code.length) {
174
+ const ch = code[i];
175
+ if (ch === '"' || ch === "'" || ch === "`") {
176
+ i = skipString(code, i);
177
+ continue;
178
+ }
179
+ if (ch === "/" && code[i + 1] === "/") {
180
+ i = code.indexOf("\n", i);
181
+ if (i === -1) return -1;
182
+ i++;
183
+ continue;
184
+ }
185
+ if (ch === "/" && code[i + 1] === "*") {
186
+ i = code.indexOf("*/", i);
187
+ if (i === -1) return -1;
188
+ i += 2;
189
+ continue;
190
+ }
191
+ if (ch === "(" || ch === "{" || ch === "[") {
192
+ depth++;
193
+ } else if (ch === ")" || ch === "}" || ch === "]") {
194
+ if (depth === 0) {
195
+ return i;
196
+ }
197
+ depth--;
198
+ } else if (ch === "," && depth === 0) {
199
+ return i;
200
+ }
201
+ i++;
202
+ }
203
+ return -1;
204
+ };
205
+ var skipString = (code, start) => {
206
+ const quote = code[start];
207
+ let i = start + 1;
208
+ while (i < code.length) {
209
+ const ch = code[i];
210
+ if (ch === "\\") {
211
+ i += 2;
212
+ continue;
213
+ }
214
+ if (quote === "`" && ch === "$" && code[i + 1] === "{") {
215
+ i += 2;
216
+ let templateDepth = 1;
217
+ while (i < code.length && templateDepth > 0) {
218
+ if (code[i] === "{") templateDepth++;
219
+ else if (code[i] === "}") templateDepth--;
220
+ else if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
221
+ i = skipString(code, i);
222
+ continue;
223
+ }
224
+ i++;
225
+ }
226
+ continue;
227
+ }
228
+ if (ch === quote) {
229
+ return i + 1;
230
+ }
231
+ i++;
232
+ }
233
+ return i;
234
+ };
235
+ function escapeHtml(str) {
236
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
237
+ }
238
+ export {
239
+ effexPlatform
240
+ };
241
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import * as path from \"node:path\";\n\nimport type { Plugin, ViteDevServer } from \"vite\";\n\n/**\n * Options for the Effex Platform Vite plugin.\n */\nexport interface EffexPlatformOptions {\n /**\n * Path to the SSR entry module that exports a `render` function.\n * The render function should have the signature: (request: Request) => Promise<Response>\n *\n * When provided, the plugin runs an SSR dev server with HMR in dev mode.\n * When omitted, only the server-code stripping transform is applied.\n *\n * @example \"src/vite-entry.ts\"\n */\n readonly entry?: string;\n /**\n * File patterns to apply the server-code stripping transform to.\n * Defaults to all .ts/.tsx/.js/.jsx files.\n */\n readonly include?: RegExp;\n /**\n * File patterns to exclude from the transform.\n */\n readonly exclude?: RegExp;\n}\n\n/**\n * Vite plugin for @effex/platform SSR applications.\n *\n * Provides two capabilities:\n *\n * 1. **Server-code stripping** (build time) — Removes loader and handler function\n * bodies from client builds so server-only dependencies (database services, etc.)\n * don't get bundled into the client.\n * - `Route.get(loader, render)` → `Route.get(null, render)`\n * - `Route.post(\"key\", handler)` → `Route.post(\"key\", () => { throw ... })`\n *\n * 2. **SSR dev server** (dev mode, when `entry` is provided) — Intercepts requests,\n * renders pages via `vite.ssrLoadModule`, and injects Vite's HMR client.\n *\n * Only needed when using @effex/platform for SSR. Pure SPAs that run loaders\n * client-side should NOT use this plugin.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from \"vite\";\n * import { effexPlatform } from \"@effex/vite-plugin\";\n *\n * export default defineConfig({\n * plugins: [\n * effexPlatform({ entry: \"src/server-entry.ts\" }),\n * ],\n * });\n * ```\n */\nexport const effexPlatform = (options: EffexPlatformOptions = {}): Plugin => {\n const include = options.include ?? /\\.(tsx?|jsx?)$/;\n const exclude = options.exclude;\n let isSsr = false;\n let root: string;\n let entryPath: string | null = null;\n\n return {\n name: \"effex-platform\",\n\n configResolved(config) {\n root = config.root;\n isSsr = !!config.build?.ssr;\n if (options.entry) {\n entryPath = path.resolve(root, options.entry);\n }\n },\n\n // -------------------------------------------------------------------------\n // Server-code stripping (client builds only)\n // -------------------------------------------------------------------------\n\n transform(code, id, options) {\n // Never strip server code in SSR builds or SSR-loaded modules (dev)\n if (isSsr || options?.ssr) return null;\n\n // Filter by include/exclude patterns\n if (!include.test(id)) return null;\n if (exclude && exclude.test(id)) return null;\n\n // Quick bail — only transform files that reference Route\n if (\n !code.includes(\"Route.get\") &&\n !code.includes(\"Route.post\") &&\n !code.includes(\"Route.put\") &&\n !code.includes(\"Route.del\")\n ) {\n return null;\n }\n\n const transformed = stripServerCode(code);\n if (transformed === code) return null;\n\n return { code: transformed, map: null };\n },\n\n // -------------------------------------------------------------------------\n // SSR dev server (dev mode only, when entry is provided)\n // -------------------------------------------------------------------------\n\n configureServer(server: ViteDevServer) {\n if (!entryPath) return;\n\n const entry = entryPath;\n\n // Return a function to run after Vite's internal middleware\n return () => {\n server.middlewares.use(async (req, res, next) => {\n // Use originalUrl to get the URL before Vite's historyFallback rewrites it\n const url =\n (req as { originalUrl?: string }).originalUrl || req.url || \"/\";\n\n // Normalize index.html to root path\n const normalizedUrl =\n url === \"/\" || url === \"/index.html\" ? \"/\" : url;\n\n // Skip Vite internal requests and static assets\n if (\n url.startsWith(\"/@\") ||\n url.startsWith(\"/__vite\") ||\n url.startsWith(\"/node_modules/\") ||\n url.startsWith(\"/src/\") ||\n (url.includes(\".\") && !url.endsWith(\"/\") && url !== \"/index.html\")\n ) {\n return next();\n }\n\n try {\n // Load the server entry module with HMR\n const serverModule = await server.ssrLoadModule(entry);\n\n if (typeof serverModule.render !== \"function\") {\n throw new Error(\n `Server entry \"${options.entry}\" must export a \"render(request: Request) => Promise<Response>\" function`,\n );\n }\n\n // Create a Web Request from the Node request\n const protocol = \"http\";\n const host = req.headers.host || \"localhost\";\n const webUrl = new URL(normalizedUrl, `${protocol}://${host}`);\n\n // Handle request body for POST/PUT/etc\n let body: string | undefined;\n if (req.method !== \"GET\" && req.method !== \"HEAD\") {\n body = await new Promise<string>((resolve) => {\n let data = \"\";\n req.on(\"data\", (chunk: string) => (data += chunk));\n req.on(\"end\", () => resolve(data));\n });\n }\n\n const webRequest = new Request(webUrl.href, {\n method: req.method,\n headers: Object.entries(req.headers).reduce(\n (acc, [key, value]) => {\n if (value)\n acc[key] = Array.isArray(value) ? value.join(\", \") : value;\n return acc;\n },\n {} as Record<string, string>,\n ),\n body: body,\n });\n\n // Call the render function — returns a Web Response\n const response: Response = await serverModule.render(webRequest);\n\n // Forward status and headers\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n const responseBody = await response.text();\n const contentType = response.headers.get(\"content-type\") || \"\";\n\n // Inject Vite's HMR client into HTML responses\n if (contentType.includes(\"text/html\")) {\n const transformedHtml = await server.transformIndexHtml(\n normalizedUrl,\n responseBody,\n );\n // Recalculate content-length since transformIndexHtml may inject scripts\n res.setHeader(\n \"content-length\",\n Buffer.byteLength(transformedHtml),\n );\n res.end(transformedHtml);\n } else {\n res.end(responseBody);\n }\n } catch (e) {\n server.ssrFixStacktrace(e as Error);\n console.error(\"[effex-platform] Error:\", e);\n\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(`\n <!DOCTYPE html>\n <html>\n <head><title>SSR Error</title></head>\n <body>\n <h1>Server Error</h1>\n <pre style=\"color: red; white-space: pre-wrap;\">${escapeHtml((e as Error).stack || (e as Error).message)}</pre>\n </body>\n </html>\n `);\n }\n });\n };\n },\n };\n};\n\n// =============================================================================\n// Server-code stripping internals\n// =============================================================================\n\n/**\n * Strip server-only code from route definitions.\n *\n * Transforms:\n * - `Route.get(loaderFn, renderFn)` → `Route.get(null, renderFn)`\n * - `Route.post(\"key\", handlerFn)` → `Route.post(\"key\", () => { throw new Error(\"server only\"); })`\n * - Same for Route.put and Route.del\n */\nexport const stripServerCode = (code: string): string => {\n let result = code;\n result = stripLoaders(result);\n result = stripHandlers(result);\n return result;\n};\n\n/**\n * Replace the first argument (loader) in Route.get() calls with null.\n */\nconst stripLoaders = (code: string): string => {\n const pattern = /Route\\.get\\s*\\(/g;\n let result = code;\n let match: RegExpExecArray | null;\n let offset = 0;\n\n pattern.lastIndex = 0;\n\n while ((match = pattern.exec(code)) !== null) {\n const callStart = match.index + offset;\n const argsStart = callStart + match[0].length;\n\n const firstArgEnd = findArgEnd(result, argsStart);\n if (firstArgEnd === -1) continue;\n\n const before = result.slice(0, argsStart);\n const after = result.slice(firstArgEnd);\n const replacement = \"null\";\n const oldLen = firstArgEnd - argsStart;\n result = before + replacement + after;\n offset += replacement.length - oldLen;\n\n pattern.lastIndex = match.index + match[0].length;\n }\n\n return result;\n};\n\n/**\n * Replace the handler function (second argument) in Route.post/put/del() calls with a no-op.\n * Keeps the key (first argument) since Outlet reads it to compute action paths.\n */\nconst stripHandlers = (code: string): string => {\n const pattern = /Route\\.(post|put|del)\\s*\\(/g;\n let result = code;\n let match: RegExpExecArray | null;\n let offset = 0;\n\n pattern.lastIndex = 0;\n\n while ((match = pattern.exec(code)) !== null) {\n const callStart = match.index + offset;\n const argsStart = callStart + match[0].length;\n\n const firstArgEnd = findArgEnd(result, argsStart);\n if (firstArgEnd === -1) continue;\n\n let secondArgStart = firstArgEnd;\n while (\n secondArgStart < result.length &&\n /[\\s,]/.test(result[secondArgStart])\n ) {\n secondArgStart++;\n }\n\n const secondArgEnd = findArgEnd(result, secondArgStart);\n if (secondArgEnd === -1) continue;\n\n const before = result.slice(0, secondArgStart);\n const after = result.slice(secondArgEnd);\n const replacement = '() => { throw new Error(\"server only\"); }';\n const oldLen = secondArgEnd - secondArgStart;\n result = before + replacement + after;\n offset += replacement.length - oldLen;\n\n pattern.lastIndex = match.index + match[0].length;\n }\n\n return result;\n};\n\n/**\n * Find the end position of a single argument starting at `start`.\n * Handles nested parens, braces, brackets, template literals, and strings.\n * Returns the index right after the argument (at the comma or closing paren).\n */\nconst findArgEnd = (code: string, start: number): number => {\n let depth = 0;\n let i = start;\n\n while (i < code.length) {\n const ch = code[i];\n\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(code, i);\n continue;\n }\n\n if (ch === \"/\" && code[i + 1] === \"/\") {\n i = code.indexOf(\"\\n\", i);\n if (i === -1) return -1;\n i++;\n continue;\n }\n\n if (ch === \"/\" && code[i + 1] === \"*\") {\n i = code.indexOf(\"*/\", i);\n if (i === -1) return -1;\n i += 2;\n continue;\n }\n\n if (ch === \"(\" || ch === \"{\" || ch === \"[\") {\n depth++;\n } else if (ch === \")\" || ch === \"}\" || ch === \"]\") {\n if (depth === 0) {\n return i;\n }\n depth--;\n } else if (ch === \",\" && depth === 0) {\n return i;\n }\n\n i++;\n }\n\n return -1;\n};\n\n/**\n * Skip past a string literal (single-quoted, double-quoted, or template).\n * Returns the index after the closing quote.\n */\nconst skipString = (code: string, start: number): number => {\n const quote = code[start];\n let i = start + 1;\n\n while (i < code.length) {\n const ch = code[i];\n\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n\n if (quote === \"`\" && ch === \"$\" && code[i + 1] === \"{\") {\n i += 2;\n let templateDepth = 1;\n while (i < code.length && templateDepth > 0) {\n if (code[i] === \"{\") templateDepth++;\n else if (code[i] === \"}\") templateDepth--;\n else if (code[i] === '\"' || code[i] === \"'\" || code[i] === \"`\") {\n i = skipString(code, i);\n continue;\n }\n i++;\n }\n continue;\n }\n\n if (ch === quote) {\n return i + 1;\n }\n\n i++;\n }\n\n return i;\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n"],"mappings":"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","names":["options","resolve"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@effex/vite-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Vite plugin for Effex applications",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "author": "Jon Laing",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/jonlaing/effex.git",
14
+ "directory": "packages/vite-plugin"
15
+ },
16
+ "keywords": [
17
+ "effect",
18
+ "effect-ts",
19
+ "vite",
20
+ "vite-plugin",
21
+ "ssr"
22
+ ],
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.cjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md"
36
+ ],
37
+ "sideEffects": false,
38
+ "peerDependencies": {
39
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "vite": "^7.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup"
46
+ }
47
+ }