vite-plugin-server-actions 0.1.0 → 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.
@@ -0,0 +1,307 @@
1
+ import { z } from "zod";
2
+ import { extendZodWithOpenApi, OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
3
+
4
+ // Extend Zod with OpenAPI support
5
+ extendZodWithOpenApi(z);
6
+
7
+ /**
8
+ * Base validation adapter interface
9
+ */
10
+ export class ValidationAdapter {
11
+ /**
12
+ * Validate data against a schema
13
+ * @param {any} schema - The validation schema
14
+ * @param {any} data - The data to validate
15
+ * @returns {Promise<{success: boolean, data?: any, errors?: any[]}>}
16
+ */
17
+ async validate(schema, data) {
18
+ throw new Error("ValidationAdapter.validate must be implemented");
19
+ }
20
+
21
+ /**
22
+ * Convert schema to OpenAPI schema format
23
+ * @param {any} schema - The validation schema
24
+ * @returns {object} OpenAPI schema object
25
+ */
26
+ toOpenAPISchema(schema) {
27
+ throw new Error("ValidationAdapter.toOpenAPISchema must be implemented");
28
+ }
29
+
30
+ /**
31
+ * Get parameter definitions for OpenAPI
32
+ * @param {any} schema - The validation schema
33
+ * @returns {Array} Array of OpenAPI parameter objects
34
+ */
35
+ getParameters(schema) {
36
+ throw new Error("ValidationAdapter.getParameters must be implemented");
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Zod validation adapter
42
+ */
43
+ export class ZodAdapter extends ValidationAdapter {
44
+ async validate(schema, data) {
45
+ try {
46
+ const validatedData = await schema.parseAsync(data);
47
+ return {
48
+ success: true,
49
+ data: validatedData,
50
+ };
51
+ } catch (error) {
52
+ if (error instanceof z.ZodError) {
53
+ return {
54
+ success: false,
55
+ errors: error.errors.map((err) => ({
56
+ path: err.path.join("."),
57
+ message: err.message,
58
+ code: err.code,
59
+ value: err.input,
60
+ })),
61
+ };
62
+ }
63
+ return {
64
+ success: false,
65
+ errors: [
66
+ {
67
+ path: "root",
68
+ message: error.message,
69
+ code: "unknown",
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ }
75
+
76
+ toOpenAPISchema(schema) {
77
+ try {
78
+ // Use @asteasolutions/zod-to-openapi for conversion
79
+ const registry = new OpenAPIRegistry();
80
+ const schemaName = "_TempSchema";
81
+
82
+ // The library requires schemas to be registered with openapi metadata
83
+ // For simple conversion, we'll create a temporary registry
84
+ const extendedSchema = schema.openapi ? schema : schema;
85
+ registry.register(schemaName, extendedSchema);
86
+
87
+ // Generate the OpenAPI components
88
+ const generator = new OpenApiGeneratorV3(registry.definitions);
89
+ const components = generator.generateComponents();
90
+
91
+ // Extract the schema from components
92
+ const openAPISchema = components.components?.schemas?.[schemaName];
93
+
94
+ if (!openAPISchema) {
95
+ // Fallback for schemas that couldn't be converted
96
+ return { type: "object", description: "Schema conversion not supported" };
97
+ }
98
+
99
+ return openAPISchema;
100
+ } catch (error) {
101
+ console.warn(`Failed to convert Zod schema to OpenAPI: ${error.message}`);
102
+ return { type: "object", description: "Schema conversion failed" };
103
+ }
104
+ }
105
+
106
+ getParameters(schema) {
107
+ if (!schema || typeof schema._def === "undefined") {
108
+ return [];
109
+ }
110
+
111
+ // For function parameters, we expect an array schema or object schema
112
+ if (schema._def.typeName === "ZodArray") {
113
+ // Array of parameters - convert each item to a parameter
114
+ const itemSchema = schema._def.type;
115
+ return this._schemaToParameters(itemSchema, "body");
116
+ } else if (schema._def.typeName === "ZodObject") {
117
+ // Object parameters - convert each property to a parameter
118
+ return this._objectToParameters(schema);
119
+ }
120
+
121
+ return [
122
+ {
123
+ name: "data",
124
+ in: "body",
125
+ required: true,
126
+ schema: this.toOpenAPISchema(schema),
127
+ },
128
+ ];
129
+ }
130
+
131
+ _objectToParameters(zodObject) {
132
+ const shape = zodObject._def.shape();
133
+ const parameters = [];
134
+
135
+ for (const [key, value] of Object.entries(shape)) {
136
+ parameters.push({
137
+ name: key,
138
+ in: "body",
139
+ required: !value.isOptional(),
140
+ schema: this.toOpenAPISchema(value),
141
+ description: value.description || `Parameter: ${key}`,
142
+ });
143
+ }
144
+
145
+ return parameters;
146
+ }
147
+
148
+ _schemaToParameters(schema, location = "body") {
149
+ return [
150
+ {
151
+ name: "data",
152
+ in: location,
153
+ required: true,
154
+ schema: this.toOpenAPISchema(schema),
155
+ },
156
+ ];
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Schema discovery utilities
162
+ */
163
+ export class SchemaDiscovery {
164
+ constructor(adapter = new ZodAdapter()) {
165
+ this.adapter = adapter;
166
+ this.schemas = new Map();
167
+ }
168
+
169
+ /**
170
+ * Register a schema for a function
171
+ * @param {string} moduleName - Module name
172
+ * @param {string} functionName - Function name
173
+ * @param {any} schema - Validation schema
174
+ */
175
+ registerSchema(moduleName, functionName, schema) {
176
+ const key = `${moduleName}.${functionName}`;
177
+ this.schemas.set(key, schema);
178
+ }
179
+
180
+ /**
181
+ * Get schema for a function
182
+ * @param {string} moduleName - Module name
183
+ * @param {string} functionName - Function name
184
+ * @returns {any|null} Schema or null if not found
185
+ */
186
+ getSchema(moduleName, functionName) {
187
+ const key = `${moduleName}.${functionName}`;
188
+ return this.schemas.get(key) || null;
189
+ }
190
+
191
+ /**
192
+ * Get all schemas
193
+ * @returns {Map} All registered schemas
194
+ */
195
+ getAllSchemas() {
196
+ return new Map(this.schemas);
197
+ }
198
+
199
+ /**
200
+ * Discover schemas from module exports
201
+ * @param {object} module - Module with exported functions
202
+ * @param {string} moduleName - Module name
203
+ */
204
+ discoverFromModule(module, moduleName) {
205
+ for (const [functionName, fn] of Object.entries(module)) {
206
+ if (typeof fn === "function") {
207
+ // Check if function has attached schema
208
+ if (fn.schema) {
209
+ this.registerSchema(moduleName, functionName, fn.schema);
210
+ }
211
+
212
+ // Check for JSDoc schema annotations (future enhancement)
213
+ // This would require parsing JSDoc comments from the function
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Clear all schemas
220
+ */
221
+ clear() {
222
+ this.schemas.clear();
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Validation middleware factory
228
+ */
229
+ export function createValidationMiddleware(options = {}) {
230
+ const adapter = options.adapter || new ZodAdapter();
231
+ const schemaDiscovery = options.schemaDiscovery || new SchemaDiscovery(adapter);
232
+
233
+ return async function validationMiddleware(req, res, next) {
234
+ let moduleName, functionName, schema;
235
+
236
+ // Check for context from route setup
237
+ if (req.validationContext) {
238
+ moduleName = req.validationContext.moduleName;
239
+ functionName = req.validationContext.functionName;
240
+ schema = req.validationContext.schema;
241
+ } else {
242
+ // Fallback to URL parsing and schema discovery
243
+ const urlParts = req.url.split("/");
244
+ functionName = urlParts[urlParts.length - 1];
245
+ moduleName = urlParts[urlParts.length - 2];
246
+ schema = schemaDiscovery.getSchema(moduleName, functionName);
247
+ }
248
+
249
+ if (!schema) {
250
+ // No schema defined, skip validation
251
+ return next();
252
+ }
253
+
254
+ try {
255
+ // Request body should be an array of arguments for server functions
256
+ if (!Array.isArray(req.body) || req.body.length === 0) {
257
+ return res.status(400).json({
258
+ error: "Validation failed",
259
+ details: "Request body must be a non-empty array of function arguments",
260
+ });
261
+ }
262
+
263
+ // Validate based on schema type
264
+ let validationData;
265
+ if (schema._def?.typeName === "ZodTuple") {
266
+ // Schema expects multiple arguments (tuple)
267
+ validationData = req.body;
268
+ } else {
269
+ // Schema expects single argument (first element of array)
270
+ validationData = req.body[0];
271
+ }
272
+
273
+ const result = await adapter.validate(schema, validationData);
274
+
275
+ if (!result.success) {
276
+ return res.status(400).json({
277
+ error: "Validation failed",
278
+ details: result.errors,
279
+ validationErrors: result.errors,
280
+ });
281
+ }
282
+
283
+ // Replace request body with validated data
284
+ if (schema._def?.typeName === "ZodTuple") {
285
+ req.body = result.data;
286
+ } else {
287
+ req.body = [result.data];
288
+ }
289
+
290
+ next();
291
+ } catch (error) {
292
+ console.error("Validation middleware error:", error);
293
+ res.status(500).json({
294
+ error: "Internal validation error",
295
+ details: error.message,
296
+ });
297
+ }
298
+ };
299
+ }
300
+
301
+ // Export default adapters and utilities
302
+ export const adapters = {
303
+ zod: ZodAdapter,
304
+ };
305
+
306
+ export const defaultAdapter = new ZodAdapter();
307
+ export const defaultSchemaDiscovery = new SchemaDiscovery(defaultAdapter);
package/.editorconfig DELETED
@@ -1,20 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- end_of_line = lf
5
- insert_final_newline = true
6
- indent_style = tab
7
- indent_size = 2
8
- charset = utf-8
9
- trim_trailing_whitespace = true
10
-
11
- [test/**/expected.css]
12
- insert_final_newline = false
13
-
14
- [package.json]
15
- indent_style = space
16
-
17
- [*.md]
18
- trim_trailing_whitespace = true
19
- indent_size = 2
20
- indent_style = space
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "printWidth": 120,
3
- "quoteProps": "consistent",
4
- "semi": true,
5
- "singleQuote": false,
6
- "bracketSameLine": false
7
- }
@@ -1,17 +0,0 @@
1
- {
2
- "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
3
- "overrides": [
4
- {
5
- "files": "*.svelte",
6
- "options": {
7
- "parser": "svelte",
8
- "svelteAllowShorthand": false
9
- }
10
- }
11
- ],
12
- "printWidth": 120,
13
- "quoteProps": "consistent",
14
- "semi": true,
15
- "singleQuote": false,
16
- "bracketSameLine": false
17
- }
@@ -1,58 +0,0 @@
1
- # Vite Server Actions - TODO App Example
2
-
3
- This is an example of a simple TODO application that
4
- uses [Vite Server Actions](https://github.com/HelgeSverre/vite-plugin-server-actions)
5
- and [Svelte](https://svelte.dev/) to demonstrate a real-world use case, where server actions are used to save, list, and
6
- delete TODOs, with data stored in a JSON file.
7
-
8
- ## 🚀 Quick Start
9
-
10
- ### Clone the repository and install dependencies
11
-
12
- ```shell
13
- git clone
14
- cd examples/todo-app
15
- npm install
16
- ```
17
-
18
- ## Run in development mode
19
-
20
- ```shell
21
- npm install
22
- npm run dev
23
- ```
24
-
25
- Open [http://localhost:5173](http://localhost:5173) to view it in your browser. (Note: The port may vary, check the
26
- console output for the correct port)
27
-
28
- ## Build and run in production mode
29
-
30
- Server Actions works in production mode by bundling the server into a single file that can be run with Node.js.
31
-
32
- Here's how you can build and run the example in production mode:
33
-
34
- ```shell
35
- # Install dependencies and build the project
36
- npm install
37
- npm run build
38
-
39
- # Run the generated express.js server
40
- node dist/server.js
41
- ```
42
-
43
- Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
44
-
45
- ## 📁 Project Structure
46
-
47
- The important files and directories in this example are:
48
-
49
- - `src/` - Contains the client-side Svelte application
50
- - `src/actions/` - Contains the server action files (functions that are run on the server, note the `.server.js` suffix)
51
- - `src/actions/todo.server.js` - Contains server actions for managing TODOs
52
- - `src/actions/auth.server.js` - Contains a dummy server action for demonstration purposes
53
- - `src/App.svelte` - The main Svelte component that imports and calls the server actions to manage TODOs.
54
- - `vite.config.js` - Vite configuration file that includes the Server Actions plugin `serverActions()`
55
- - `dist/` - The output directory for the production build.
56
- - `dist/server.js` - The express server that serves the client-side application and the server actions (automatically
57
- created by the plugin)
58
- - `todos.json` - The JSON file where the TODOs are stored (serves the purpose of a simple database for this example)
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>TODO Example Svelte</title>
7
- </head>
8
- <body>
9
- <div id="app"></div>
10
- <script type="module" src="/src/main.js"></script>
11
- </body>
12
- </html>
@@ -1,32 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "moduleResolution": "bundler",
4
- "target": "ESNext",
5
- "module": "ESNext",
6
- /**
7
- * svelte-preprocess cannot figure out whether you have
8
- * a value or a type, so tell TypeScript to enforce using
9
- * `import type` instead of `import` for Types.
10
- */
11
- "verbatimModuleSyntax": true,
12
- "isolatedModules": true,
13
- "resolveJsonModule": true,
14
- /**
15
- * To have warnings / errors of the Svelte compiler at the
16
- * correct position, enable source maps by default.
17
- */
18
- "sourceMap": true,
19
- "esModuleInterop": true,
20
- "skipLibCheck": true,
21
- /**
22
- * Typecheck JS in `.svelte` and `.js` files by default.
23
- * Disable this if you'd like to use dynamic types.
24
- */
25
- "checkJs": true
26
- },
27
- /**
28
- * Use global.d.ts instead of compilerOptions.types
29
- * to avoid limiting type declarations.
30
- */
31
- "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
32
- }
@@ -1,22 +0,0 @@
1
- {
2
- "name": "todo-app",
3
- "version": "0.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "build": "vite build",
8
- "dev": "vite",
9
- "format": "npx prettier --write .",
10
- "preview": "vite preview",
11
- "sort": "npx sort-package-json"
12
- },
13
- "devDependencies": {
14
- "@sveltejs/vite-plugin-svelte": "^3.1.1",
15
- "prettier": "^3.3.3",
16
- "prettier-plugin-svelte": "^3.2.6",
17
- "prettier-plugin-tailwindcss": "^0.6.5",
18
- "svelte": "^4.2.18",
19
- "vite": "^5.4.1",
20
- "vite-plugin-inspect": "^0.8.7"
21
- }
22
- }
@@ -1,155 +0,0 @@
1
- <script>
2
- import { onMount } from "svelte";
3
- import { addTodo, deleteTodo, getTodos, updateTodo } from "./actions/todo.server.js";
4
- import { login } from "./actions/auth.server.js";
5
-
6
- let todos = [];
7
- let newTodoText = "";
8
-
9
- onMount(() => {
10
- login("admin", "admin");
11
- loadTodos();
12
- });
13
-
14
- async function loadTodos() {
15
- console.log("Loading todos...");
16
- todos = await getTodos();
17
- }
18
-
19
- async function handleAddTodo() {
20
- if (!newTodoText.trim()) return;
21
-
22
- await addTodo({ text: newTodoText });
23
- await loadTodos();
24
- newTodoText = "";
25
- }
26
-
27
- async function handleToggleTodo(id) {
28
- const todo = todos.find((t) => t.id === id);
29
- if (todo) {
30
- const updatedTodo = await updateTodo(id, { completed: !todo.completed });
31
- todos = todos.map((t) => (t.id === id ? updatedTodo : t));
32
- }
33
- }
34
-
35
- async function handleDeleteTodo(id) {
36
- await deleteTodo(id);
37
- await loadTodos();
38
- }
39
- </script>
40
-
41
- <main>
42
- <h1>Todo List</h1>
43
-
44
- <form class="todo-form" on:submit|preventDefault={handleAddTodo}>
45
- <input class="todo-input" bind:value={newTodoText} placeholder="Add a new todo" />
46
- <button class="todo-button" type="submit">Add</button>
47
- </form>
48
-
49
- {#if todos.length > 0}
50
- <ul class="todo-list">
51
- {#each todos as todo}
52
- <li class="todo-item">
53
- <input type="checkbox" checked={todo.completed} on:change={() => handleToggleTodo(todo.id)} />
54
- <span class:completed={todo.completed}>{todo.text}</span>
55
- <button class="delete-button" on:click={() => handleDeleteTodo(todo.id)}>Delete</button>
56
- </li>
57
- {/each}
58
- </ul>
59
- {/if}
60
- </main>
61
-
62
- <style>
63
- /* Basic global styling */
64
- main {
65
- font-family: sans-serif;
66
- max-width: 600px;
67
- margin: 2rem auto;
68
- padding: 1rem;
69
- background-color: #f8f8f8;
70
- border-radius: 8px;
71
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
72
- }
73
-
74
- h1 {
75
- text-align: center;
76
- color: #333;
77
- margin-bottom: 1rem;
78
- }
79
-
80
- .todo-form {
81
- display: flex;
82
- gap: 0.5rem;
83
- margin-bottom: 1rem;
84
- }
85
-
86
- .todo-input {
87
- flex: 1;
88
- padding: 0.5rem;
89
- border: 1px solid #ccc;
90
- border-radius: 4px;
91
- font-size: 1rem;
92
- }
93
-
94
- .todo-button {
95
- padding: 0.5rem 1rem;
96
- font-size: 1rem;
97
- color: white;
98
- background-color: #333;
99
- border: none;
100
- border-radius: 4px;
101
- cursor: pointer;
102
- }
103
-
104
- .todo-button:hover {
105
- background-color: #555;
106
- }
107
-
108
- .todo-list {
109
- list-style: none;
110
- padding: 0;
111
- margin: 0;
112
- border-top: 1px solid #e0e0e0;
113
- }
114
-
115
- .todo-item {
116
- display: flex;
117
- align-items: center;
118
- justify-content: space-between;
119
- padding: 0.5rem;
120
- border-bottom: 1px solid #e0e0e0;
121
- }
122
-
123
- .todo-item:last-child {
124
- border-bottom: none;
125
- }
126
-
127
- .todo-item input[type="checkbox"] {
128
- margin-right: 1rem;
129
- }
130
-
131
- .todo-item span {
132
- flex: 1;
133
- font-size: 1rem;
134
- color: #333;
135
- }
136
-
137
- .todo-item span.completed {
138
- text-decoration: line-through;
139
- color: #888;
140
- }
141
-
142
- .delete-button {
143
- padding: 0.25rem 0.5rem;
144
- font-size: 0.875rem;
145
- color: #fff;
146
- background-color: #cc0000;
147
- border: none;
148
- border-radius: 4px;
149
- cursor: pointer;
150
- }
151
-
152
- .delete-button:hover {
153
- background-color: #ff3333;
154
- }
155
- </style>
@@ -1,14 +0,0 @@
1
- export function login(user, pass) {
2
- if (user === "admin" && pass === "admin") {
3
- return {
4
- user: "admin",
5
- role: "admin",
6
- };
7
- } else {
8
- throw new Error("Invalid user or password");
9
- }
10
- }
11
-
12
- export function logout() {
13
- return true;
14
- }
@@ -1,57 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
-
4
- const TODO_FILE = path.join(process.cwd(), "todos.json");
5
-
6
- async function readTodos() {
7
- try {
8
- const data = await fs.readFile(TODO_FILE, "utf8");
9
- return JSON.parse(data);
10
- } catch (error) {
11
- console.log(error);
12
- if (error.code === "ENOENT") {
13
- // File doesn't exist, return an empty array
14
- return [];
15
- }
16
- console.error("Error reading todos:", error);
17
- throw error;
18
- }
19
- }
20
-
21
- async function writeTodos(todos) {
22
- try {
23
- await fs.writeFile(TODO_FILE, JSON.stringify(todos, null, 2), "utf8");
24
- } catch (error) {
25
- console.error("Error writing todos:", error);
26
- throw error;
27
- }
28
- }
29
-
30
- export async function getTodos() {
31
- return await readTodos();
32
- }
33
-
34
- export async function addTodo(todo) {
35
- const todos = await readTodos();
36
- const newTodo = { id: Date.now(), text: todo.text, completed: false };
37
- todos.push(newTodo);
38
- await writeTodos(todos);
39
- return newTodo;
40
- }
41
-
42
- export async function updateTodo(id, updates) {
43
- const todos = await readTodos();
44
- const index = todos.findIndex((todo) => todo.id === id);
45
- if (index !== -1) {
46
- todos[index] = { ...todos[index], ...updates };
47
- await writeTodos(todos);
48
- return todos[index];
49
- }
50
- throw new Error("Todo not found");
51
- }
52
-
53
- export async function deleteTodo(id) {
54
- const todos = await readTodos();
55
- const newTodos = todos.filter((todo) => todo.id != id);
56
- await writeTodos(newTodos);
57
- }