vite-plugin-server-actions 0.1.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/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "vite-plugin-server-actions",
3
+ "description": "Automatically proxy imported backend functions to a server endpoint in Vite",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "keywords": [
7
+ "vite-plugin",
8
+ "server",
9
+ "actions"
10
+ ],
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/HelgeSverre/vite-plugin-server-actions",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/HelgeSverre/vite-plugin-server-actions"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/HelgeSverre/vite-plugin-server-actions/issues"
19
+ },
20
+ "readme": "README.md",
21
+ "author": {
22
+ "name": "Helge Sverre",
23
+ "email": "helge.sverre@gmail.com",
24
+ "url": "https://helgesver.re"
25
+ },
26
+ "main": "src/index.js",
27
+ "scripts": {
28
+ "format": "npx prettier --write src/ README.md",
29
+ "sort": "npx sort-package-json"
30
+ },
31
+ "devDependencies": {
32
+ "express": "^4.21.0",
33
+ "prettier": "^3.3.3",
34
+ "vitest": "^0.34.0"
35
+ },
36
+ "peerDependencies": {
37
+ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0"
38
+ }
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,192 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import express from "express";
4
+ // TODO: find a way to not use rollup directly
5
+ import { rollup } from "rollup";
6
+
7
+ export default function serverActions() {
8
+ const serverFunctions = new Map();
9
+ let app;
10
+
11
+ return {
12
+ name: "vite-plugin-server-actions",
13
+
14
+ configureServer(server) {
15
+ app = express();
16
+ app.use(express.json());
17
+ server.middlewares.use(app);
18
+ },
19
+
20
+ async resolveId(source, importer) {
21
+ if (source.endsWith(".server.js") && importer) {
22
+ const resolvedPath = path.resolve(path.dirname(importer), source);
23
+ return resolvedPath;
24
+ }
25
+ },
26
+
27
+ async load(id) {
28
+ if (id.endsWith(".server.js")) {
29
+ const code = await fs.readFile(id, "utf-8");
30
+ const moduleName = path.basename(id, ".server.js");
31
+ const exportRegex = /export\s+(async\s+)?function\s+(\w+)/g;
32
+ const functions = [];
33
+ let match;
34
+ while ((match = exportRegex.exec(code)) !== null) {
35
+ functions.push(match[2]);
36
+ }
37
+
38
+ serverFunctions.set(moduleName, { functions, id });
39
+
40
+ if (process.env.NODE_ENV !== "production") {
41
+ functions.forEach((functionName) => {
42
+ const endpoint = `/api/${moduleName}/${functionName}`;
43
+ app.post(endpoint, async (req, res) => {
44
+ try {
45
+ const module = await import(id);
46
+ const result = await module[functionName](...req.body);
47
+ res.json(result || "* No response *");
48
+ } catch (error) {
49
+ console.error(`Error in ${functionName}: ${error.message}`);
50
+ res.status(500).json({ error: error.message });
51
+ }
52
+ });
53
+ });
54
+ }
55
+ return generateClientProxy(moduleName, functions);
56
+ }
57
+ },
58
+
59
+ async generateBundle(options, bundle) {
60
+ // Create a virtual entry point for all server functions
61
+ const virtualEntryId = "virtual:server-actions-entry";
62
+ let virtualModuleContent = "";
63
+ for (const [moduleName, { id }] of serverFunctions) {
64
+ virtualModuleContent += `import * as ${moduleName} from '${id}';\n`;
65
+ }
66
+ virtualModuleContent += `export { ${Array.from(serverFunctions.keys()).join(", ")} };`;
67
+
68
+ // Use Rollup to bundle the virtual module
69
+ const build = await rollup({
70
+ input: virtualEntryId,
71
+ plugins: [
72
+ {
73
+ name: "virtual",
74
+ resolveId(id) {
75
+ if (id === virtualEntryId) {
76
+ return id;
77
+ }
78
+ },
79
+ load(id) {
80
+ if (id === virtualEntryId) {
81
+ return virtualModuleContent;
82
+ }
83
+ },
84
+ },
85
+ {
86
+ name: "external-modules",
87
+ resolveId(source) {
88
+ if (!source.endsWith(".server.js") && !source.startsWith(".") && !path.isAbsolute(source)) {
89
+ return { id: source, external: true };
90
+ }
91
+ },
92
+ },
93
+ ],
94
+ });
95
+
96
+ const { output } = await build.generate({ format: "es" });
97
+
98
+ if (output.length === 0) {
99
+ throw new Error("Failed to bundle server functions");
100
+ }
101
+
102
+ // Get the bundled code
103
+ const bundledCode = output[0].code;
104
+
105
+ // Emit the bundled server functions
106
+ this.emitFile({
107
+ type: "asset",
108
+ fileName: "actions.js",
109
+ source: bundledCode,
110
+ });
111
+
112
+ // Generate server.js
113
+ let serverCode = `
114
+ import express from 'express';
115
+ import * as serverActions from './actions.js';
116
+
117
+ const app = express();
118
+
119
+ // Middleware
120
+ // --------------------------------------------------
121
+ app.use(express.json());
122
+ app.use(express.static('dist'));
123
+
124
+ // Server functions
125
+ // --------------------------------------------------
126
+ ${Array.from(serverFunctions.entries())
127
+ .flatMap(([moduleName, { functions }]) =>
128
+ functions
129
+ .map(
130
+ (functionName) => `
131
+ app.post('/api/${moduleName}/${functionName}', async (req, res) => {
132
+ try {
133
+ const result = await serverActions.${moduleName}.${functionName}(...req.body);
134
+ res.json(result || "* No response *");
135
+ } catch (error) {
136
+ console.error(\`Error in ${functionName}: \${error.message}\`);
137
+ res.status(500).json({ error: error.message });
138
+ }
139
+ });
140
+ `,
141
+ )
142
+ .join("\n")
143
+ .trim(),
144
+ )
145
+ .join("\n")
146
+ .trim()}
147
+
148
+ // Start server
149
+ // --------------------------------------------------
150
+ const port = process.env.PORT || 3000;
151
+ app.listen(port, () => console.log(\`🚀 Server listening: http://localhost:\${port}\`));
152
+
153
+ // List all server functions
154
+ // --------------------------------------------------
155
+ `;
156
+
157
+ // TODO: Add a way to list all server functions in the console
158
+
159
+ this.emitFile({
160
+ type: "asset",
161
+ fileName: "server.js",
162
+ source: serverCode,
163
+ });
164
+ },
165
+ };
166
+ }
167
+
168
+ function generateClientProxy(moduleName, functions) {
169
+ let clientProxy = `\n// vite-server-actions: ${moduleName}\n`;
170
+ functions.forEach((functionName) => {
171
+ clientProxy += `
172
+ export async function ${functionName}(...args) {
173
+ console.log("[Vite Server Actions] 🚀 - Executing ${functionName}");
174
+ const response = await fetch('/api/${moduleName}/${functionName}', {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify(args)
178
+ });
179
+
180
+ if (!response.ok) {
181
+ console.log("[Vite Server Actions] ❗ - Error in ${functionName}");
182
+ throw new Error('Server request failed');
183
+ }
184
+
185
+ console.log("[Vite Server Actions] ✅ - ${functionName} executed successfully");
186
+
187
+ return response.json();
188
+ }
189
+ `;
190
+ });
191
+ return clientProxy;
192
+ }