@ubean/routes 0.2.2 → 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.
- package/dist/form-action-CZA0dSMO.js +511 -0
- package/dist/index.d.ts +382 -3
- package/dist/index.js +448 -94
- package/dist/invoke-CzODqUg-.d.ts +12 -0
- package/dist/runtime.d.ts +149 -0
- package/dist/runtime.js +278 -0
- package/package.json +15 -11
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { ACTION_BRAND, ActionError, isActionFailure } from "@ubean/shared";
|
|
2
|
+
//#region src/actions/id.ts
|
|
3
|
+
/**
|
|
4
|
+
* Stable Action ID generation (P9-02).
|
|
5
|
+
*
|
|
6
|
+
* Action IDs are derived from the action's source location (file path +
|
|
7
|
+
* export name) so the client and server agree on the same ID without
|
|
8
|
+
* runtime coordination. The Vite plugin injects the ID at build time,
|
|
9
|
+
* but a runtime fallback is provided for actions defined outside the
|
|
10
|
+
* `'use server'` pipeline (e.g. inline `defineAction` in page modules).
|
|
11
|
+
*
|
|
12
|
+
* Format: `act_<base32(sha1(relPath:exportName)).slice(0, 12)>`
|
|
13
|
+
* - `act_` prefix avoids collisions with other URL components
|
|
14
|
+
* - 12 chars of base32 gives ~60 bits of entropy (collision-resistant
|
|
15
|
+
* for any reasonable project size)
|
|
16
|
+
* - base32 (RFC 4648, lowercase) is URL-safe and case-insensitive
|
|
17
|
+
*/
|
|
18
|
+
const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
19
|
+
/**
|
|
20
|
+
* Compute a stable action ID from a file path and export name.
|
|
21
|
+
*
|
|
22
|
+
* @param filePath Project-relative file path (e.g. `src/actions/auth.ts`)
|
|
23
|
+
* @param exportName The export name (e.g. `login`, `default`)
|
|
24
|
+
* @returns A stable action ID string
|
|
25
|
+
*/
|
|
26
|
+
function createActionId(filePath, exportName) {
|
|
27
|
+
return `act_${base32HashSync(`${filePath}:${exportName}`).slice(0, 12)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Synchronous base32 hash. Uses a simple FNV-1a hash (32-bit) when
|
|
31
|
+
* `crypto.subtle` is unavailable or async is undesirable. The hash space
|
|
32
|
+
* is smaller than SHA-1 but sufficient for action IDs within a single
|
|
33
|
+
* project (collision probability ~1 in 4 billion for 12 chars).
|
|
34
|
+
*
|
|
35
|
+
* For the Vite plugin path, a stronger SHA-1 hash is injected at build
|
|
36
|
+
* time, so this runtime fallback only affects inline `defineAction` calls
|
|
37
|
+
* without a file context.
|
|
38
|
+
*/
|
|
39
|
+
function base32HashSync(input) {
|
|
40
|
+
let hash = 2166136261;
|
|
41
|
+
for (let i = 0; i < input.length; i++) {
|
|
42
|
+
hash ^= input.charCodeAt(i);
|
|
43
|
+
hash = Math.imul(hash, 16777619);
|
|
44
|
+
}
|
|
45
|
+
let hash2 = 16777619;
|
|
46
|
+
for (let i = 0; i < input.length; i++) {
|
|
47
|
+
hash2 ^= input.charCodeAt(i) * 31;
|
|
48
|
+
hash2 = Math.imul(hash2, 16777619);
|
|
49
|
+
}
|
|
50
|
+
return bigIntToBase32(BigInt(hash >>> 0) * (1n << 28n) + BigInt(hash2 >>> 0), 12);
|
|
51
|
+
}
|
|
52
|
+
function bigIntToBase32(value, length) {
|
|
53
|
+
let result = "";
|
|
54
|
+
let v = value;
|
|
55
|
+
for (let i = 0; i < length; i++) {
|
|
56
|
+
result = BASE32_ALPHABET[Number(v & 31n)] + result;
|
|
57
|
+
v >>= 5n;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate that a string is a well-formed action ID.
|
|
63
|
+
*
|
|
64
|
+
* Used by the dispatcher to reject malformed requests early.
|
|
65
|
+
*/
|
|
66
|
+
function isValidActionId(id) {
|
|
67
|
+
return /^act_[a-z2-7]{12}$/.test(id);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/actions/registry.ts
|
|
71
|
+
const _registry = /* @__PURE__ */ new Map();
|
|
72
|
+
/**
|
|
73
|
+
* Register a server action in the global registry.
|
|
74
|
+
*
|
|
75
|
+
* Called by `defineAction` (implicitly) and by the Vite plugin's virtual
|
|
76
|
+
* module (explicitly) for `'use server'` modules.
|
|
77
|
+
*
|
|
78
|
+
* If an action with the same ID is already registered, the call is a
|
|
79
|
+
* no-op (HMR-safe: re-registering the same action on hot reload doesn't
|
|
80
|
+
* throw).
|
|
81
|
+
*/
|
|
82
|
+
function registerAction(action) {
|
|
83
|
+
if (!_registry.has(action.id)) _registry.set(action.id, action);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Look up a registered server action by ID.
|
|
87
|
+
*/
|
|
88
|
+
function getAction(id) {
|
|
89
|
+
return _registry.get(id);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check whether an action with the given ID is registered.
|
|
93
|
+
*/
|
|
94
|
+
function hasAction(id) {
|
|
95
|
+
return _registry.has(id);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* List all registered actions (for debugging / DevTools).
|
|
99
|
+
*/
|
|
100
|
+
function listActions() {
|
|
101
|
+
return [..._registry.values()];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Clear the registry (for tests).
|
|
105
|
+
*
|
|
106
|
+
* Not intended for application code — actions are registered once at
|
|
107
|
+
* module load and remain for the lifetime of the process.
|
|
108
|
+
*/
|
|
109
|
+
function clearActions() {
|
|
110
|
+
_registry.clear();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Register a map of actions at once (bulk registration helper for the
|
|
114
|
+
* Vite plugin's virtual module).
|
|
115
|
+
*/
|
|
116
|
+
function registerActions(actions) {
|
|
117
|
+
for (const action of actions) registerAction(action);
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/actions/define.ts
|
|
121
|
+
/**
|
|
122
|
+
* `defineAction` — Astro-style server action creator (P9-02).
|
|
123
|
+
*
|
|
124
|
+
* Wraps an async function as a server action with a stable ID. The action
|
|
125
|
+
* can be invoked:
|
|
126
|
+
*
|
|
127
|
+
* - Server-side: call the returned function directly (typed input/output).
|
|
128
|
+
* - Client-side: the Vite plugin replaces the export with an RPC stub that
|
|
129
|
+
* POSTs to `/__actions` (transparent to the caller).
|
|
130
|
+
* - Form (progressive enhancement): use the action's `id` as a form
|
|
131
|
+
* `action` attribute, e.g. `<form method="POST" action="?/login">`.
|
|
132
|
+
*
|
|
133
|
+
* ## With a schema
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { defineAction } from 'ubean';
|
|
137
|
+
* import { z } from 'zod';
|
|
138
|
+
*
|
|
139
|
+
* export const login = defineAction(
|
|
140
|
+
* z.object({ email: z.string().email(), password: z.string() }),
|
|
141
|
+
* async (data, ctx) => {
|
|
142
|
+
* // data is typed as { email: string; password: string }
|
|
143
|
+
* return { user: data.email };
|
|
144
|
+
* }
|
|
145
|
+
* );
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* ## Without a schema
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* export const ping = defineAction(async (input, ctx) => {
|
|
152
|
+
* // input is the raw parsed body (FormData or JSON object)
|
|
153
|
+
* return { ok: true, echo: input };
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* ## Errors and validation failures
|
|
158
|
+
*
|
|
159
|
+
* - Throw `ActionError` to signal a user-facing error (with optional `code`).
|
|
160
|
+
* - Return `fail(status, { field: 'message' })` for field-level validation
|
|
161
|
+
* errors (SvelteKit-style).
|
|
162
|
+
*/
|
|
163
|
+
function defineAction(schemaOrHandler, handlerOrOptions, options) {
|
|
164
|
+
let schema;
|
|
165
|
+
let handler;
|
|
166
|
+
let opts = {};
|
|
167
|
+
if (typeof schemaOrHandler === "function") {
|
|
168
|
+
handler = schemaOrHandler;
|
|
169
|
+
if (handlerOrOptions && typeof handlerOrOptions === "object") opts = handlerOrOptions;
|
|
170
|
+
} else {
|
|
171
|
+
schema = schemaOrHandler;
|
|
172
|
+
handler = handlerOrOptions;
|
|
173
|
+
if (options) opts = options;
|
|
174
|
+
}
|
|
175
|
+
const name = opts.name || handler.name || "anonymous";
|
|
176
|
+
const filePath = opts.filePath || _guessCallerPath();
|
|
177
|
+
const action = {
|
|
178
|
+
id: opts.id || createActionId(filePath, name),
|
|
179
|
+
handler,
|
|
180
|
+
schema,
|
|
181
|
+
name,
|
|
182
|
+
filePath
|
|
183
|
+
};
|
|
184
|
+
Object.defineProperty(action, ACTION_BRAND, {
|
|
185
|
+
value: true,
|
|
186
|
+
enumerable: false,
|
|
187
|
+
configurable: false,
|
|
188
|
+
writable: false
|
|
189
|
+
});
|
|
190
|
+
registerAction(action);
|
|
191
|
+
return action;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Type-safe server function. Same ID, registry, and `POST /__actions` RPC as
|
|
195
|
+
* `defineAction` — use this alias when the function is a loader/query as well
|
|
196
|
+
* as a mutation. Do not invent a second RPC.
|
|
197
|
+
*/
|
|
198
|
+
const defineServerFn = defineAction;
|
|
199
|
+
/**
|
|
200
|
+
* Best-effort guess of the caller's file path for action ID generation.
|
|
201
|
+
*
|
|
202
|
+
* Uses `Error.stack` parsing (works in Node and most browsers). Falls back
|
|
203
|
+
* to `'inline'` when the stack is unavailable. The Vite plugin overrides
|
|
204
|
+
* this with the actual transformed file path.
|
|
205
|
+
*/
|
|
206
|
+
function _guessCallerPath() {
|
|
207
|
+
const stack = (/* @__PURE__ */ new Error()).stack;
|
|
208
|
+
if (!stack) return "inline";
|
|
209
|
+
const lines = stack.split("\n");
|
|
210
|
+
for (let i = 3; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i];
|
|
212
|
+
if (line && !line.includes("/actions/src/define.ts") && !line.includes("/actions/src/id.ts")) {
|
|
213
|
+
const match = line.match(/\(([^)]+)\)|at\s+(https?:\/\/\S+)/);
|
|
214
|
+
if (match) return (match[1] || match[2]).replace(/[?#].*$/, "").replace(/:\d+:\d+$/, "");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return "inline";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Parse a Request body into a plain object suitable for action handlers.
|
|
221
|
+
*
|
|
222
|
+
* - `application/json` → parsed JSON object
|
|
223
|
+
* - `multipart/form-data` or `application/x-www-form-urlencoded` → Object
|
|
224
|
+
* from FormData entries (string values, files skipped)
|
|
225
|
+
* - Otherwise → empty object (handler can read `ctx.request` directly)
|
|
226
|
+
*/
|
|
227
|
+
async function parseActionInput(request) {
|
|
228
|
+
const contentType = request.headers.get("Content-Type") || "";
|
|
229
|
+
if (contentType.includes("application/json")) try {
|
|
230
|
+
const text = await request.text();
|
|
231
|
+
return text ? JSON.parse(text) : {};
|
|
232
|
+
} catch {
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) try {
|
|
236
|
+
const formData = await request.formData();
|
|
237
|
+
const obj = {};
|
|
238
|
+
for (const [key, value] of formData.entries()) if (typeof value === "string") obj[key] = value;
|
|
239
|
+
return obj;
|
|
240
|
+
} catch {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
return {};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Validate input against an action schema.
|
|
247
|
+
*
|
|
248
|
+
* Returns `{ success: true, data }` on success, or
|
|
249
|
+
* `{ success: false, errors }` on failure (per-field messages).
|
|
250
|
+
*/
|
|
251
|
+
function issuesToErrors(issues) {
|
|
252
|
+
const errors = {};
|
|
253
|
+
for (const issue of issues || []) {
|
|
254
|
+
const key = Array.isArray(issue.path) && issue.path.length > 0 ? String(issue.path[0]) : "_error";
|
|
255
|
+
errors[key] = issue.message || "Invalid value";
|
|
256
|
+
}
|
|
257
|
+
if (Object.keys(errors).length === 0) errors._error = "Invalid value";
|
|
258
|
+
return errors;
|
|
259
|
+
}
|
|
260
|
+
function validateActionInput(schema, input) {
|
|
261
|
+
const standard = schema["~standard"];
|
|
262
|
+
if (standard && typeof standard.validate === "function") {
|
|
263
|
+
const result = standard.validate(input);
|
|
264
|
+
if (result && typeof result === "object" && "then" in result) return {
|
|
265
|
+
success: false,
|
|
266
|
+
errors: { _error: "Async Standard Schema validation is not supported in actions" }
|
|
267
|
+
};
|
|
268
|
+
if (result && typeof result === "object" && "issues" in result && result.issues) return {
|
|
269
|
+
success: false,
|
|
270
|
+
errors: issuesToErrors(result.issues)
|
|
271
|
+
};
|
|
272
|
+
if (result && typeof result === "object" && "value" in result) return {
|
|
273
|
+
success: true,
|
|
274
|
+
data: result.value
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (schema.safeParse) {
|
|
278
|
+
const result = schema.safeParse(input);
|
|
279
|
+
if (result.success) return {
|
|
280
|
+
success: true,
|
|
281
|
+
data: result.data
|
|
282
|
+
};
|
|
283
|
+
const errors = {};
|
|
284
|
+
const issues = result.error?.issues || [];
|
|
285
|
+
for (const issue of issues) {
|
|
286
|
+
const key = issue.message ? String(issue.message) : "Invalid";
|
|
287
|
+
errors[key] = issue.message || "Invalid value";
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
errors
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
if (schema.parse) return {
|
|
296
|
+
success: true,
|
|
297
|
+
data: schema.parse(input)
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
data: input
|
|
302
|
+
};
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
errors: { _error: err instanceof Error ? err.message : String(err) }
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Normalize a handler return value into an `ActionResult`.
|
|
312
|
+
*
|
|
313
|
+
* - `ActionFailure` → `{ errors, status: failure.status }`
|
|
314
|
+
* - `ActionError` (thrown) → `{ error: { message, code }, status }`
|
|
315
|
+
* - Other thrown error → `{ error: { message }, status: 500 }`
|
|
316
|
+
* - Plain value → `{ data: value, status: 200 }`
|
|
317
|
+
*/
|
|
318
|
+
function normalizeActionResult(result, error, failureStatus = 400) {
|
|
319
|
+
if (error) {
|
|
320
|
+
if (error instanceof ActionError) return {
|
|
321
|
+
error: {
|
|
322
|
+
message: error.message,
|
|
323
|
+
code: error.code
|
|
324
|
+
},
|
|
325
|
+
status: error.status
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
329
|
+
status: 500
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (isActionFailure(result)) return {
|
|
333
|
+
errors: result.errors,
|
|
334
|
+
status: result.status || failureStatus
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
data: result,
|
|
338
|
+
status: 200
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Build an `ActionContext` from a Hono context.
|
|
343
|
+
*
|
|
344
|
+
* - `request`: the underlying `c.req.raw` Request
|
|
345
|
+
* - `context`: the Hono context (for `c.set`, `c.get`, etc.)
|
|
346
|
+
* - `params`: route params from `c.req.param()`
|
|
347
|
+
*/
|
|
348
|
+
function buildActionContext(c) {
|
|
349
|
+
return {
|
|
350
|
+
request: c.req.raw,
|
|
351
|
+
context: c,
|
|
352
|
+
params: c.req.param()
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/actions/request-context.ts
|
|
357
|
+
const STORAGE_KEY = "__UBEAN_ACTION_CTX_STORAGE__";
|
|
358
|
+
function bindActionContextStorage(storage) {
|
|
359
|
+
globalThis[STORAGE_KEY] = storage;
|
|
360
|
+
}
|
|
361
|
+
function getActionContext() {
|
|
362
|
+
return globalThis[STORAGE_KEY]?.getStore();
|
|
363
|
+
}
|
|
364
|
+
function runWithActionContext(ctx, fn) {
|
|
365
|
+
const storage = globalThis[STORAGE_KEY];
|
|
366
|
+
if (storage) return storage.run(ctx, fn);
|
|
367
|
+
return fn();
|
|
368
|
+
}
|
|
369
|
+
function createDetachedActionContext(request) {
|
|
370
|
+
const req = request ?? new Request("http://ubean.local/__actions");
|
|
371
|
+
const params = {};
|
|
372
|
+
return {
|
|
373
|
+
request: req,
|
|
374
|
+
context: {
|
|
375
|
+
req: {
|
|
376
|
+
raw: req,
|
|
377
|
+
param: () => params,
|
|
378
|
+
path: new URL(req.url).pathname,
|
|
379
|
+
method: req.method,
|
|
380
|
+
header: (name) => req.headers.get(name) ?? void 0
|
|
381
|
+
},
|
|
382
|
+
get: () => void 0,
|
|
383
|
+
set: () => void 0
|
|
384
|
+
},
|
|
385
|
+
params
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/actions/invoke.ts
|
|
390
|
+
/**
|
|
391
|
+
* Isomorphic invocation for `defineAction` / `defineServerFn`.
|
|
392
|
+
*
|
|
393
|
+
* Same ID and `/__actions` RPC as Server Actions — not a second protocol.
|
|
394
|
+
* Server with ALS: call the real handler. Otherwise (client stub or no
|
|
395
|
+
* request scope): call `handler`, which on the client is the Vite RPC stub.
|
|
396
|
+
*/
|
|
397
|
+
function unwrapActionResult(result) {
|
|
398
|
+
if (result.error) throw new ActionError(result.error.message, {
|
|
399
|
+
code: result.error.code,
|
|
400
|
+
status: result.status
|
|
401
|
+
});
|
|
402
|
+
if (result.errors) {
|
|
403
|
+
const first = Object.values(result.errors)[0] || "Validation failed";
|
|
404
|
+
throw new ActionError(first, { status: result.status || 400 });
|
|
405
|
+
}
|
|
406
|
+
if (result.response) throw result.response;
|
|
407
|
+
return result.data;
|
|
408
|
+
}
|
|
409
|
+
function isActionResultShape(value) {
|
|
410
|
+
return typeof value === "object" && value !== null && "status" in value && ("data" in value || "error" in value || "errors" in value || "response" in value);
|
|
411
|
+
}
|
|
412
|
+
async function unwrapServerFnResult(value) {
|
|
413
|
+
if (isActionFailure(value)) {
|
|
414
|
+
const first = Object.values(value.errors)[0] || "Validation failed";
|
|
415
|
+
throw new ActionError(first, { status: value.status });
|
|
416
|
+
}
|
|
417
|
+
if (isActionResultShape(value)) return unwrapActionResult(value);
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Call a server function from a loader, `useAsyncData`, or the client.
|
|
422
|
+
*
|
|
423
|
+
* Reuses the action ID / `POST /__actions` envelope. Does not invent RPC.
|
|
424
|
+
*/
|
|
425
|
+
async function invokeServerFn(fn, input) {
|
|
426
|
+
let data = input;
|
|
427
|
+
if (fn.schema) {
|
|
428
|
+
const validated = validateActionInput(fn.schema, input);
|
|
429
|
+
if (!validated.success) {
|
|
430
|
+
const first = Object.values(validated.errors)[0] || "Validation failed";
|
|
431
|
+
throw new ActionError(first, { status: 400 });
|
|
432
|
+
}
|
|
433
|
+
data = validated.data;
|
|
434
|
+
}
|
|
435
|
+
const ctx = getActionContext() ?? createDetachedActionContext();
|
|
436
|
+
return unwrapServerFnResult(await fn.handler(data, ctx));
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/actions/constants.ts
|
|
440
|
+
/**
|
|
441
|
+
* Shared constants for Server Actions RPC.
|
|
442
|
+
*
|
|
443
|
+
* Kept in a leaf module so the browser runtime can import them without
|
|
444
|
+
* pulling the Hono middleware barrel.
|
|
445
|
+
*/
|
|
446
|
+
const ACTIONS_ENDPOINT = "/__actions";
|
|
447
|
+
const ACTION_RESPONSE_HEADER = "x-ubean-action";
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/actions/form-action.ts
|
|
450
|
+
/**
|
|
451
|
+
* SvelteKit-style form action URL parsing (P9-02).
|
|
452
|
+
*
|
|
453
|
+
* Form actions are invoked via `POST /page?/<actionName>`:
|
|
454
|
+
*
|
|
455
|
+
* - `POST /login` → `actions.default`
|
|
456
|
+
* - `POST /login?/login` → `actions.login`
|
|
457
|
+
* - `POST /login?/register` → `actions.register`
|
|
458
|
+
*
|
|
459
|
+
* The `?/<name>` syntax is URL-safe and works without JavaScript (the
|
|
460
|
+
* browser submits the form to the full URL). On the server, the page
|
|
461
|
+
* route handler parses the action name from the URL's search query.
|
|
462
|
+
*
|
|
463
|
+
* For progressive enhancement, the client can intercept the form submit
|
|
464
|
+
* and call the action via `useFormAction()` for SPA-style navigation.
|
|
465
|
+
*/
|
|
466
|
+
/**
|
|
467
|
+
* Extract the form action name from a URL's search query.
|
|
468
|
+
*
|
|
469
|
+
* SvelteKit's convention: the query string contains a single key `/<name>`
|
|
470
|
+
* with no value. URLSearchParams treats this as a key with empty value.
|
|
471
|
+
*
|
|
472
|
+
* - `?/login` → `login`
|
|
473
|
+
* - `?/register` → `register`
|
|
474
|
+
* - `?` (no action) → `default`
|
|
475
|
+
* - (no query) → `default`
|
|
476
|
+
*
|
|
477
|
+
* @returns The action name, or `'default'` when no specific action is
|
|
478
|
+
* specified.
|
|
479
|
+
*/
|
|
480
|
+
function parseFormActionName(url) {
|
|
481
|
+
const match = (typeof url === "string" ? url : url.search).match(/[?&]\/([^&]+)/);
|
|
482
|
+
return match ? match[1] : "default";
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Build a form action URL for the current page.
|
|
486
|
+
*
|
|
487
|
+
* Used by `useFormAction()` to generate the `action` attribute for
|
|
488
|
+
* `<form>` elements. The URL is relative so it works on any page.
|
|
489
|
+
*
|
|
490
|
+
* - `useFormAction()` → `?/default`
|
|
491
|
+
* - `useFormAction('login')` → `?/login`
|
|
492
|
+
* - `useFormAction('register')` → `?/register`
|
|
493
|
+
*
|
|
494
|
+
* The returned string is intended for `<form :action="formAction">` —
|
|
495
|
+
* the browser submits to the current page URL with the action name
|
|
496
|
+
* appended as a query parameter.
|
|
497
|
+
*/
|
|
498
|
+
function buildFormActionUrl(actionName = "default") {
|
|
499
|
+
if (!actionName || actionName === "default") return "?/default";
|
|
500
|
+
return `?/${encodeURIComponent(actionName)}`;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check whether a URL's query string contains a form action specifier
|
|
504
|
+
* (`?/<name>` pattern).
|
|
505
|
+
*/
|
|
506
|
+
function hasFormAction(url) {
|
|
507
|
+
const search = typeof url === "string" ? url : url.search;
|
|
508
|
+
return /[?&]\/[^&]+/.test(search);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
export { registerAction as C, isValidActionId as E, listActions as S, createActionId as T, parseActionInput as _, ACTION_RESPONSE_HEADER as a, getAction as b, unwrapServerFnResult as c, getActionContext as d, runWithActionContext as f, normalizeActionResult as g, defineServerFn as h, ACTIONS_ENDPOINT as i, bindActionContextStorage as l, defineAction as m, hasFormAction as n, invokeServerFn as o, buildActionContext as p, parseFormActionName as r, unwrapActionResult as s, buildFormActionUrl as t, createDetachedActionContext as u, validateActionInput as v, registerActions as w, hasAction as x, clearActions as y };
|