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/.editorconfig ADDED
@@ -0,0 +1,20 @@
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 ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "printWidth": 120,
3
+ "quoteProps": "consistent",
4
+ "semi": true,
5
+ "singleQuote": false,
6
+ "bracketSameLine": false
7
+ }
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # 🚀 Vite Server Actions
2
+
3
+ [![npm version](https://img.shields.io/npm/v/vite-plugin-server-actions.svg?style=flat)](https://www.npmjs.com/package/vite-plugin-server-actions)
4
+ [![Downloads](https://img.shields.io/npm/dm/vite-plugin-server-actions.svg?style=flat)](https://www.npmjs.com/package/vite-plugin-server-actions)
5
+ [![Build Status](https://img.shields.io/github/workflow/status/HelgeSverre/vite-plugin-server-actions/CI)](https://github.com/HelgeSverre/vite-plugin-server-actions/actions)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ > 🚧 **Experimental:** This is currently a proof of concept. Use at your own risk.
9
+
10
+
11
+ **Vite Server Actions** is a Vite plugin that makes it easy to create functions (actions) that runs on the server, while
12
+ allowing you to call them from the client-side as if they were local functions.
13
+
14
+ ## ✨ Features
15
+
16
+ - 🔄 Automatic API endpoint creation for server functions (e.g. `POST /api/todos/addTodo`)
17
+ - 🔗 Seamless client-side proxies for easy usage (e.g. `import {addTodo} from './server/todos.server.js'`)
18
+ - 🛠 Support for both development and production environments ( `vite build` )
19
+ - 🚀 Zero-config setup for instant productivity
20
+
21
+ ## 🚀 Quick Start
22
+
23
+ 1. Install the plugin:
24
+
25
+ ```bash
26
+ # Install using npm
27
+ npm install vite-plugin-server-actions
28
+
29
+ # Install using yarn
30
+ yarn add vite-plugin-server-actions
31
+ ```
32
+
33
+ 2. Add to your `vite.config.js`:
34
+
35
+ ```javascript
36
+ import {defineConfig} from "vite";
37
+ import serverActions from "helgesverre/vite-plugin-server-actions";
38
+
39
+ export default defineConfig({
40
+ plugins: [serverActions()],
41
+ });
42
+ ```
43
+
44
+ 3. Create a `[whatever].server.js` file anywhere in your project:
45
+
46
+ ```javascript
47
+ // ex: src/actions/todo.server.js
48
+ import fs from "fs";
49
+ import path from "path";
50
+
51
+ const TODO_FILE_PATH = path.join(process.cwd(), "list-of-todos.json");
52
+
53
+ export async function deleteTodoById(id) {
54
+ const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
55
+ const todos = JSON.parse(data);
56
+ const newTodos = todos.filter((todo) => todo.id !== id);
57
+ fs.writeFileSync(TODO_FILE_PATH, JSON.stringify(newTodos, null, 2));
58
+ }
59
+
60
+ export async function saveTodoToJsonFile(todo) {
61
+ const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
62
+ const todos = JSON.parse(data);
63
+ todos.push(todo);
64
+ fs.writeFileSync(TODO_FILE_PATH, JSON.stringify(todos, null, 2));
65
+ }
66
+
67
+ export async function listTodos() {
68
+ const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
69
+ return JSON.parse(data);
70
+ }
71
+ ```
72
+
73
+ 4. Import and use your server actions in your client-side code:
74
+
75
+ ```svelte
76
+ <!-- ex: src/App.svelte -->
77
+ <script>
78
+ import { deleteTodoById, listTodos, saveTodoToJsonFile } from "./actions/todo.server.js";
79
+
80
+ let todos = [];
81
+ let newTodoText = "";
82
+
83
+ async function fetchTodos() {
84
+ todos = await listTodos();
85
+ }
86
+
87
+ async function addTodo() {
88
+ await saveTodoToJsonFile({ id: Math.random(), text: newTodoText });
89
+ newTodoText = "";
90
+ await fetchTodos();
91
+ }
92
+
93
+ async function removeTodo(id) {
94
+ await deleteTodoById(id);
95
+ await fetchTodos();
96
+ }
97
+
98
+ fetchTodos();
99
+ </script>
100
+
101
+ <div>
102
+ <h1>Todos</h1>
103
+ <ul>
104
+ {#each todos as todo}
105
+ <li>
106
+ {todo.text}
107
+ <button on:click="{() => removeTodo(todo.id)}">Remove</button>
108
+ </li>
109
+ {/each}
110
+ </ul>
111
+ <input type="text" bind:value="{newTodoText}" />
112
+ <button on:click="{addTodo}">Add Todo</button>
113
+ </div>
114
+ ```
115
+
116
+ ## 🤯 How it works
117
+
118
+ Vite Server Actions works by creating an API endpoint for each server function you define.
119
+
120
+ When you import a server action in your client-side code, Vite Server Actions will intercept the import and return a
121
+ proxy function that sends a request to the server endpoint instead of executing the function locally.
122
+
123
+ In development mode, the server is run as a middleware in the Vite dev server, while in production mode, the server is
124
+ bundled into a single file that can be run with Node.js.
125
+
126
+ ### Sequence Diagram
127
+
128
+ ```mermaid
129
+ sequenceDiagram
130
+ participant Client
131
+ participant Vite Dev Server
132
+ participant Plugin Middleware
133
+ participant Server Function
134
+ participant File System
135
+ Client ->> Vite Dev Server: import { addTodo } from './server/todos.server.js'
136
+ Vite Dev Server ->> Client: Returns proxied function
137
+ Client ->> Client: Call addTodo({ text: 'New todo' })
138
+ Client ->> Vite Dev Server: POST /api/todos/addTodo
139
+ Vite Dev Server ->> Plugin Middleware: Handle POST request
140
+ Plugin Middleware ->> Server Function: Call addTodo function
141
+ Server Function ->> File System: Read todos.json
142
+ File System ->> Server Function: Return current todos
143
+ Server Function ->> Server Function: Add new todo
144
+ Server Function ->> File System: Write updated todos.json
145
+ File System ->> Server Function: Write confirmation
146
+ Server Function ->> Plugin Middleware: Return new todo
147
+ Plugin Middleware ->> Vite Dev Server: Send JSON response
148
+ Vite Dev Server ->> Client: Return new todo data
149
+ ```
150
+
151
+ ## 🔧 Configuration
152
+
153
+ Vite Server Actions works out of the box, but you can customize it:
154
+
155
+ ```javascript
156
+ serverActions({
157
+ // Options (coming soon)
158
+ });
159
+ ```
160
+
161
+ ## 🛠️ Configuration Options
162
+
163
+ TODO: Add configuration options and descriptions
164
+
165
+ | Option | Type | Default | Description |
166
+ |------------------|----------------------------------------|-------------|----------------------------------|
167
+ | logLevel | 'error' \| 'warn' \| 'info' \| 'debug' | 'info' | Server log level |
168
+ | serverPath | string | '/api' | Base path for server endpoints |
169
+ | serverPort | number | 3000 | Port for the server |
170
+ | serverHost | string | 'localhost' | Host for the server |
171
+ | serverMiddleware | (app: Express) => void | - | Custom middleware for the server |
172
+
173
+ ## TODO
174
+
175
+ This is a proof of concept, and things are still missing, such as:
176
+
177
+ - [ ] Add configuration options
178
+ - [ ] Add tests
179
+ - [ ] Allow customizing the HTTP method for each action (e.g. `GET`, `POST`, `PUT`, `DELETE`)
180
+ - [ ] Make sure name collisions are handled correctly
181
+ - [ ] Make sure the actions are only available on the server when running in production mode.
182
+ - [ ] Add more examples (Vue, React, etc.)
183
+ - [ ] Publish to npm
184
+
185
+ ## 🤝 Contributing
186
+
187
+ Contributions, issues, and feature requests are welcome! Feel free to
188
+ check [issues page](https://github.com/helgesverre/vite-plugin-server-actions/issues).
189
+
190
+ ## 📝 License
191
+
192
+ This project is [MIT](https://opensource.org/licenses/MIT) licensed.
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,58 @@
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)
@@ -0,0 +1,12 @@
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>
@@ -0,0 +1,32 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,155 @@
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>
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,57 @@
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
+ }
File without changes
@@ -0,0 +1,8 @@
1
+ import "./app.css";
2
+ import App from "./App.svelte";
3
+
4
+ const app = new App({
5
+ target: document.getElementById("app"),
6
+ });
7
+
8
+ export default app;
@@ -0,0 +1,7 @@
1
+ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2
+
3
+ export default {
4
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5
+ // for more information about preprocessors
6
+ preprocess: vitePreprocess(),
7
+ };
@@ -0,0 +1,27 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "text": "Finish writing documentation",
5
+ "completed": false
6
+ },
7
+ {
8
+ "id": 2,
9
+ "text": "Implement authentication logic",
10
+ "completed": true
11
+ },
12
+ {
13
+ "id": 3,
14
+ "text": "Design user interface",
15
+ "completed": false
16
+ },
17
+ {
18
+ "id": 4,
19
+ "text": "Test server actions",
20
+ "completed": true
21
+ },
22
+ {
23
+ "id": 5,
24
+ "text": "Deploy application",
25
+ "completed": false
26
+ }
27
+ ]
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+ import serverActions from "../../src/index.js";
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [
8
+ svelte(),
9
+ serverActions(),
10
+ // ...
11
+ ],
12
+ });