@ubean/routes 0.2.1 → 0.3.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,12 @@
1
+ import { ActionResult, ServerAction } from "@ubean/shared";
2
+ //#region src/actions/invoke.d.ts
3
+ declare function unwrapActionResult<T>(result: ActionResult<T>): T;
4
+ declare function unwrapServerFnResult<T>(value: unknown): Promise<T>;
5
+ /**
6
+ * Call a server function from a loader, `useAsyncData`, or the client.
7
+ *
8
+ * Reuses the action ID / `POST /__actions` envelope. Does not invent RPC.
9
+ */
10
+ declare function invokeServerFn<TInput = unknown, TOutput = unknown>(fn: ServerAction<TInput, TOutput>, input?: TInput): Promise<TOutput>;
11
+ //#endregion
12
+ export { unwrapActionResult as n, unwrapServerFnResult as r, invokeServerFn as t };
@@ -0,0 +1,149 @@
1
+ import { n as unwrapActionResult, r as unwrapServerFnResult, t as invokeServerFn } from "./invoke-CzODqUg-.js";
2
+ import { ActionResult, ServerAction } from "@ubean/shared";
3
+ import { Ref } from "vue";
4
+ //#region src/runtime.d.ts
5
+ /**
6
+ * Create a client-side stub for a server action.
7
+ *
8
+ * The Vite plugin replaces `defineAction(...)` calls on the client with
9
+ * `createActionStub('<id>')` calls. The stub is a `ServerAction`-compatible
10
+ * object whose `handler` performs an RPC POST to `/__actions` with the
11
+ * action's stable ID.
12
+ *
13
+ * Application code should not call this directly — it is injected by the
14
+ * Vite plugin. Use `defineAction()` in source code; the plugin handles the
15
+ * client/server split automatically.
16
+ *
17
+ * @param actionId The stable action ID (e.g. `act_xxxxxxxxxxxx`)
18
+ */
19
+ declare function createActionStub<TInput = unknown, TOutput = unknown>(actionId: string): ServerAction<TInput, TOutput>;
20
+ /**
21
+ * Low-level RPC: invoke a registered server action by ID.
22
+ *
23
+ * Used by the Vite plugin's client-side RPC stubs (generated from
24
+ * `'use server'` modules). Application code should use `useAction()`
25
+ * instead — `callAction` is the building block.
26
+ *
27
+ * @param actionId The stable action ID (e.g. `act_xxxxxxxxxxxx`)
28
+ * @param args Arguments to pass to the action handler (serialized as JSON)
29
+ * @returns The `ActionResult` returned by the server
30
+ */
31
+ declare function callAction<T = unknown>(actionId: string, args?: unknown[]): Promise<ActionResult<T>>;
32
+ interface UseActionReturn<TInput = unknown, TOutput = unknown> {
33
+ /** Reactive flag: `true` while the action is in flight. */
34
+ pending: Ref<boolean>;
35
+ /** The latest `ActionResult.data` (success), or `null` on error. */
36
+ data: Ref<TOutput | null>;
37
+ /** The latest `ActionResult.error`, or `null` on success. */
38
+ error: Ref<{
39
+ message: string;
40
+ code?: string;
41
+ } | null>;
42
+ /** The latest `ActionResult.errors` (field-level), or `null`. */
43
+ errors: Ref<Record<string, string> | null>;
44
+ /** The latest HTTP status code. */
45
+ status: Ref<number>;
46
+ /** The full `ActionResult` from the last invocation. */
47
+ result: Ref<ActionResult<TOutput> | null>;
48
+ /**
49
+ * Invoke the action. Pass either:
50
+ * - the action's typed input (when no schema), or
51
+ * - the validated data shape (when schema is provided)
52
+ *
53
+ * On the client, the action is an RPC stub created by the Vite plugin;
54
+ * arguments are forwarded positionally to the server handler.
55
+ */
56
+ submit: (...args: TInput extends unknown[] ? TInput : [TInput]) => Promise<ActionResult<TOutput>>;
57
+ /** Reset all reactive state to initial values. */
58
+ reset: () => void;
59
+ }
60
+ /**
61
+ * Composable for invoking a server action reactively.
62
+ *
63
+ * Two usage modes:
64
+ *
65
+ * ## 1. Action from `defineAction()` (typed input)
66
+ *
67
+ * ```ts
68
+ * import { useAction } from 'ubean/runtime/vue';
69
+ * import { login } from '~/actions/auth';
70
+ *
71
+ * const { submit, pending, data, error } = useAction(login);
72
+ * await submit({ email, password });
73
+ * ```
74
+ *
75
+ * ## 2. Action ID string
76
+ *
77
+ * ```ts
78
+ * const { submit, pending, data, error } = useAction('act_xxxxxxxxxxxx');
79
+ * await submit(email, password);
80
+ * ```
81
+ */
82
+ declare function useAction<TInput = unknown, TOutput = unknown>(actionOrId: ServerAction<TInput, TOutput> | string): UseActionReturn<TInput, TOutput>;
83
+ interface UseFormActionReturn {
84
+ /** The form `action` attribute value (e.g. `?/login`). */
85
+ action: string;
86
+ /** Reactive flag: `true` while the form is submitting via SPA. */
87
+ pending: Ref<boolean>;
88
+ /** The latest `ActionResult.data` from a SPA submit, or `null`. */
89
+ data: Ref<unknown | null>;
90
+ /** The latest `ActionResult.error`, or `null`. */
91
+ error: Ref<{
92
+ message: string;
93
+ code?: string;
94
+ } | null>;
95
+ /** The latest `ActionResult.errors` (field-level), or `null`. */
96
+ errors: Ref<Record<string, string> | null>;
97
+ /**
98
+ * Submit a `FormData` (or HTMLFormElement) via SPA-style navigation.
99
+ *
100
+ * Posts to the current page URL with `?/<actionName>` and the form's
101
+ * fields as the body. The server's `handlePageRequest` dispatches the
102
+ * named form action and returns a `PageObject` with `errors`/`props`.
103
+ *
104
+ * For progressive enhancement, set this as the form's `@submit` handler:
105
+ *
106
+ * ```vue
107
+ * <form method="POST" :action="formAction" @submit.prevent="formAction.onSubmit">
108
+ * ...
109
+ * </form>
110
+ * ```
111
+ */
112
+ onSubmit: (event: Event | FormData) => Promise<ActionResult>;
113
+ /** Reset all reactive state. */
114
+ reset: () => void;
115
+ }
116
+ /**
117
+ * Composable for SvelteKit-style page-level form actions.
118
+ *
119
+ * Generates the form `action` attribute for progressive enhancement and
120
+ * provides a SPA-style submit handler. The form action is invoked via
121
+ * `POST /currentPage?/<actionName>` — the server dispatches it to the
122
+ * page module's `actions.<name>` handler.
123
+ *
124
+ * ```vue
125
+ * <script setup>
126
+ * import { useFormAction } from 'ubean/runtime/vue';
127
+ *
128
+ * const login = useFormAction('login');
129
+ * </script>
130
+ *
131
+ * <template>
132
+ * <form method="POST" :action="login.action" @submit.prevent="login.onSubmit">
133
+ * <input name="email" type="email" />
134
+ * <input name="password" type="password" />
135
+ * <button :disabled="login.pending.value">
136
+ * {{ login.pending.value ? 'Logging in…' : 'Login' }}
137
+ * </button>
138
+ * </form>
139
+ * <p v-if="login.error.value" class="error">{{ login.error.value.message }}</p>
140
+ * </template>
141
+ * ```
142
+ *
143
+ * Without JavaScript, the browser submits the form natively to
144
+ * `?/login` — the server renders the result HTML, providing full
145
+ * progressive enhancement.
146
+ */
147
+ declare function useFormAction(actionName?: string): UseFormActionReturn;
148
+ //#endregion
149
+ export { UseActionReturn, UseFormActionReturn, callAction, createActionStub, invokeServerFn, unwrapActionResult, unwrapServerFnResult, useAction, useFormAction };
@@ -0,0 +1,278 @@
1
+ import { a as ACTION_RESPONSE_HEADER, c as unwrapServerFnResult, i as ACTIONS_ENDPOINT, o as invokeServerFn, s as unwrapActionResult, t as buildFormActionUrl } from "./form-action-CZA0dSMO.js";
2
+ import { ACTION_BRAND } from "@ubean/shared";
3
+ import { ref } from "vue";
4
+ //#region src/runtime.ts
5
+ /**
6
+ * Client-side runtime for server actions (P9-02).
7
+ *
8
+ * This module is browser-only — it MUST NOT import any Node.js APIs or
9
+ * server-only types. The Vite plugin replaces `defineAction()` calls on
10
+ * the client with `createActionStub()` calls (imported from here), and
11
+ * `useAction()` / `useFormAction()` are auto-imported from
12
+ * `ubean/runtime/vue` (which re-exports these).
13
+ *
14
+ * The runtime communicates with the server via the `/__actions` POST
15
+ * endpoint (for RPC) or via page POST (for form actions with
16
+ * progressive enhancement).
17
+ */
18
+ /**
19
+ * Create a client-side stub for a server action.
20
+ *
21
+ * The Vite plugin replaces `defineAction(...)` calls on the client with
22
+ * `createActionStub('<id>')` calls. The stub is a `ServerAction`-compatible
23
+ * object whose `handler` performs an RPC POST to `/__actions` with the
24
+ * action's stable ID.
25
+ *
26
+ * Application code should not call this directly — it is injected by the
27
+ * Vite plugin. Use `defineAction()` in source code; the plugin handles the
28
+ * client/server split automatically.
29
+ *
30
+ * @param actionId The stable action ID (e.g. `act_xxxxxxxxxxxx`)
31
+ */
32
+ function createActionStub(actionId) {
33
+ const rpc = ((...args) => callAction(actionId, args));
34
+ const stub = {
35
+ id: actionId,
36
+ handler: rpc,
37
+ name: "stub",
38
+ filePath: "client"
39
+ };
40
+ Object.defineProperty(stub, ACTION_BRAND, {
41
+ value: true,
42
+ enumerable: false,
43
+ configurable: false,
44
+ writable: false
45
+ });
46
+ return stub;
47
+ }
48
+ /**
49
+ * Low-level RPC: invoke a registered server action by ID.
50
+ *
51
+ * Used by the Vite plugin's client-side RPC stubs (generated from
52
+ * `'use server'` modules). Application code should use `useAction()`
53
+ * instead — `callAction` is the building block.
54
+ *
55
+ * @param actionId The stable action ID (e.g. `act_xxxxxxxxxxxx`)
56
+ * @param args Arguments to pass to the action handler (serialized as JSON)
57
+ * @returns The `ActionResult` returned by the server
58
+ */
59
+ async function callAction(actionId, args = []) {
60
+ const res = await fetch(ACTIONS_ENDPOINT, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ [ACTION_RESPONSE_HEADER]: "true"
65
+ },
66
+ body: JSON.stringify({
67
+ id: actionId,
68
+ args
69
+ }),
70
+ redirect: "manual"
71
+ });
72
+ if (!(res.headers.get("Content-Type") || "").includes("application/json")) return {
73
+ error: { message: `Unexpected response (status ${res.status})` },
74
+ status: res.status
75
+ };
76
+ try {
77
+ return await res.json();
78
+ } catch {
79
+ return {
80
+ error: { message: "Failed to parse action response" },
81
+ status: res.status
82
+ };
83
+ }
84
+ }
85
+ /**
86
+ * Composable for invoking a server action reactively.
87
+ *
88
+ * Two usage modes:
89
+ *
90
+ * ## 1. Action from `defineAction()` (typed input)
91
+ *
92
+ * ```ts
93
+ * import { useAction } from 'ubean/runtime/vue';
94
+ * import { login } from '~/actions/auth';
95
+ *
96
+ * const { submit, pending, data, error } = useAction(login);
97
+ * await submit({ email, password });
98
+ * ```
99
+ *
100
+ * ## 2. Action ID string
101
+ *
102
+ * ```ts
103
+ * const { submit, pending, data, error } = useAction('act_xxxxxxxxxxxx');
104
+ * await submit(email, password);
105
+ * ```
106
+ */
107
+ function useAction(actionOrId) {
108
+ const pending = ref(false);
109
+ const data = ref(null);
110
+ const error = ref(null);
111
+ const errors = ref(null);
112
+ const status = ref(0);
113
+ const result = ref(null);
114
+ const actionId = typeof actionOrId === "string" ? actionOrId : actionOrId.id;
115
+ async function submit(...args) {
116
+ pending.value = true;
117
+ error.value = null;
118
+ errors.value = null;
119
+ try {
120
+ const res = await callAction(actionId, args);
121
+ result.value = res;
122
+ status.value = res.status;
123
+ if (res.error) {
124
+ error.value = res.error;
125
+ data.value = null;
126
+ } else if (res.errors) {
127
+ errors.value = res.errors;
128
+ data.value = null;
129
+ } else data.value = res.data ?? null;
130
+ return res;
131
+ } catch (err) {
132
+ const fallback = {
133
+ error: { message: err instanceof Error ? err.message : String(err) },
134
+ status: 0
135
+ };
136
+ result.value = fallback;
137
+ status.value = 0;
138
+ error.value = fallback.error;
139
+ return fallback;
140
+ } finally {
141
+ pending.value = false;
142
+ }
143
+ }
144
+ function reset() {
145
+ pending.value = false;
146
+ data.value = null;
147
+ error.value = null;
148
+ errors.value = null;
149
+ status.value = 0;
150
+ result.value = null;
151
+ }
152
+ return {
153
+ pending,
154
+ data,
155
+ error,
156
+ errors,
157
+ status,
158
+ result,
159
+ submit,
160
+ reset
161
+ };
162
+ }
163
+ /**
164
+ * Composable for SvelteKit-style page-level form actions.
165
+ *
166
+ * Generates the form `action` attribute for progressive enhancement and
167
+ * provides a SPA-style submit handler. The form action is invoked via
168
+ * `POST /currentPage?/<actionName>` — the server dispatches it to the
169
+ * page module's `actions.<name>` handler.
170
+ *
171
+ * ```vue
172
+ * <script setup>
173
+ * import { useFormAction } from 'ubean/runtime/vue';
174
+ *
175
+ * const login = useFormAction('login');
176
+ * <\/script>
177
+ *
178
+ * <template>
179
+ * <form method="POST" :action="login.action" @submit.prevent="login.onSubmit">
180
+ * <input name="email" type="email" />
181
+ * <input name="password" type="password" />
182
+ * <button :disabled="login.pending.value">
183
+ * {{ login.pending.value ? 'Logging in…' : 'Login' }}
184
+ * </button>
185
+ * </form>
186
+ * <p v-if="login.error.value" class="error">{{ login.error.value.message }}</p>
187
+ * </template>
188
+ * ```
189
+ *
190
+ * Without JavaScript, the browser submits the form natively to
191
+ * `?/login` — the server renders the result HTML, providing full
192
+ * progressive enhancement.
193
+ */
194
+ function useFormAction(actionName = "default") {
195
+ const action = buildFormActionUrl(actionName);
196
+ const pending = ref(false);
197
+ const data = ref(null);
198
+ const error = ref(null);
199
+ const errors = ref(null);
200
+ async function onSubmit(event) {
201
+ let formData;
202
+ if (event instanceof FormData) formData = event;
203
+ else {
204
+ const form = event.target;
205
+ formData = new FormData(form);
206
+ }
207
+ pending.value = true;
208
+ error.value = null;
209
+ errors.value = null;
210
+ try {
211
+ const url = window.location.pathname + action;
212
+ const res = await fetch(url, {
213
+ method: "POST",
214
+ body: formData,
215
+ headers: { "x-ubeanpages": "true" },
216
+ redirect: "manual"
217
+ });
218
+ const redirectUrl = res.headers.get("X-Ubean-Redirect");
219
+ if (redirectUrl) {
220
+ window.location.href = redirectUrl;
221
+ return {
222
+ data: { redirect: redirectUrl },
223
+ status: res.status
224
+ };
225
+ }
226
+ if ((res.headers.get("Content-Type") || "").includes("application/json")) {
227
+ const json = await res.json();
228
+ if (json.redirect && typeof json.redirect === "string") {
229
+ window.location.href = json.redirect;
230
+ return {
231
+ data: json,
232
+ status: res.status
233
+ };
234
+ }
235
+ const pageObj = json;
236
+ if (pageObj.errors) {
237
+ errors.value = pageObj.errors;
238
+ return {
239
+ errors: pageObj.errors,
240
+ status: res.status
241
+ };
242
+ }
243
+ data.value = pageObj.props ?? null;
244
+ return {
245
+ data: pageObj.props,
246
+ status: res.status
247
+ };
248
+ }
249
+ return { status: res.status };
250
+ } catch (err) {
251
+ const fallback = {
252
+ error: { message: err instanceof Error ? err.message : String(err) },
253
+ status: 0
254
+ };
255
+ error.value = fallback.error;
256
+ return fallback;
257
+ } finally {
258
+ pending.value = false;
259
+ }
260
+ }
261
+ function reset() {
262
+ pending.value = false;
263
+ data.value = null;
264
+ error.value = null;
265
+ errors.value = null;
266
+ }
267
+ return {
268
+ action,
269
+ pending,
270
+ data,
271
+ error,
272
+ errors,
273
+ onSubmit,
274
+ reset
275
+ };
276
+ }
277
+ //#endregion
278
+ export { callAction, createActionStub, invokeServerFn, unwrapActionResult, unwrapServerFnResult, useAction, useFormAction };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ubean/routes",
3
- "version": "0.2.1",
4
- "description": "Server routes runtime for ubean (defineHandler, defineMiddleware, registerRoutes, rou3 server router, route rules, ISR, internal-fetch, OpenAPI)",
3
+ "version": "0.3.0",
4
+ "description": "Server routes runtime for ubean (defineHandler, defineMiddleware, registerRoutes, Server Actions, rou3 server router, route rules, ISR, internal-fetch, OpenAPI)",
5
5
  "files": [
6
6
  "dist"
7
7
  ],
@@ -13,6 +13,10 @@
13
13
  ".": {
14
14
  "types": "./dist/index.d.ts",
15
15
  "import": "./dist/index.js"
16
+ },
17
+ "./runtime": {
18
+ "types": "./dist/runtime.d.ts",
19
+ "import": "./dist/runtime.js"
16
20
  }
17
21
  },
18
22
  "dependencies": {
@@ -23,21 +27,21 @@
23
27
  "hono": "4.13.3",
24
28
  "hono-openapi": "^1.3.1",
25
29
  "rou3": "^0.9.2",
26
- "@ubean/actions": "0.2.1",
27
- "@ubean/shared": "0.2.1"
30
+ "@ubean/shared": "0.3.0",
31
+ "@ubean/i18n": "0.3.0"
28
32
  },
29
33
  "devDependencies": {
30
34
  "@types/node": "^26.2.0",
31
35
  "typescript": "7.0.2",
32
36
  "vite-plus": "0.2.9",
33
- "@ubean/i18n": "0.2.1",
34
- "@ubean/pages": "0.2.1",
35
- "@ubean/scan": "0.2.1"
37
+ "vue": "^3.5.41",
38
+ "@ubean/pages": "0.3.0",
39
+ "@ubean/scan": "0.3.0"
36
40
  },
37
41
  "peerDependencies": {
38
- "@ubean/i18n": "0.2.1",
39
- "@ubean/scan": "0.2.1",
40
- "@ubean/pages": "0.2.1"
42
+ "vue": "^3.0.0",
43
+ "@ubean/pages": "0.3.0",
44
+ "@ubean/scan": "0.3.0"
41
45
  },
42
46
  "peerDependenciesMeta": {
43
47
  "@ubean/scan": {
@@ -46,7 +50,7 @@
46
50
  "@ubean/pages": {
47
51
  "optional": true
48
52
  },
49
- "@ubean/i18n": {
53
+ "vue": {
50
54
  "optional": true
51
55
  }
52
56
  },