@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 +137 -0
- package/dist/index.cjs +278 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n"],"mappings":"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","names":["options","resolve"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\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
|
+
}
|