oxidejs 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -34,7 +34,7 @@ vite build
34
34
  node dist/server.js
35
35
  ```
36
36
 
37
- Default preset is `"fetch"`. No `index.html` → only `dist/server.js`. With `index.html` → client to `dist/client/`, then `/_action` (if you have a `*.server.{ts,tsx,js,jsx}` file) → `src/server.ts` (`undefined` continues) → static file → `index.html` for navigations. `public/` is copied next to the client. Hashed assets get `Cache-Control: immutable`. No `wrangler.jsonc`.
37
+ Default preset is `"fetch"`. No `index.html` → only `dist/server.js`. With `index.html` → client to `dist/client/`, then `/__oxide/action` (if you have a `*.server.{ts,tsx,js,jsx}` file) → `src/server.ts` (`undefined` continues) → static file → `index.html` for navigations. `public/` is copied next to the client. Hashed assets get `Cache-Control: immutable`. No `wrangler.jsonc`.
38
38
 
39
39
  ```ts
40
40
  oxide({
@@ -47,18 +47,21 @@ oxide({
47
47
 
48
48
  ## Server actions
49
49
 
50
- Install `tacho` if you use actions. Files named `*.server.ts`, `*.server.tsx`, `*.server.js`, or `*.server.jsx` are server-only. A client import is replaced with a tacho stub that POSTs `/_action`. The original module never enters the client graph. Server and Vite SSR (`import.meta.env.SSR === true`) keep the real functions. Methods are `<file>.<fn>` (`test.ping`). Call `useRequest()` inside an action for the inbound `Request`. `useCtx()` is tacho `ctx` (`{ req }` plus anything middleware or `createContext` added). On `preset: "celld"`, `useEnv()` and `useFetchCtx()` are the Worker `env` and `ctx` from `fetch(request, env, ctx)` — same values as `useCtx().env` / `useCtx().fetchCtx`. Return `undefined` from `src/server.ts` to fall through to static files. No server action files → the bundle does not import tacho.
50
+ Install `tacho` if you use actions. Files named `*.server.ts`, `*.server.tsx`, `*.server.js`, or `*.server.jsx` are server-only. A client import is replaced with a tacho stub that POSTs `/__oxide/action`. The original module never enters the client graph. **Only exports wrapped in `action()` become remote actions** — any other export stays server-local and is not callable over the wire. Server and Vite SSR (`import.meta.env.SSR === true`) keep the real functions. Methods are `<file>.<fn>` (`test.ping`). Call `useRequest()` inside an action for the inbound `Request`. `useCtx()` is tacho `ctx` (`{ req }` plus anything middleware or `createContext` added). On `preset: "celld"`, `useEnv()` and `useFetchCtx()` are the Worker `env` and `ctx` from `fetch(request, env, ctx)` — same values as `useCtx().env` / `useCtx().fetchCtx`. Return `undefined` from `src/server.ts` to fall through to static files. No server action files → the bundle does not import tacho.
51
51
 
52
52
  ```ts
53
53
  // src/test.server.ts
54
- import { useRequest } from "oxidejs";
54
+ import { action, useRequest } from "oxidejs";
55
55
 
56
- export async function who() {
56
+ export const who = action(async () => {
57
57
  return useRequest().headers.get("x-user");
58
- }
58
+ });
59
+
60
+ export const ping = action(async () => "pong");
59
61
 
60
- export async function ping() {
61
- return "pong";
62
+ // a non-action export is never exposed over the wire
63
+ async function internalHelper() {
64
+ /* server-only */
62
65
  }
63
66
 
64
67
  // src/client.ts
@@ -73,15 +76,16 @@ export default {
73
76
  };
74
77
  ```
75
78
 
76
- `async function*` exports stream over tacho SSE. `oxidejs/tsconfig` makes `await ticks()` typecheck. Pass `{ signal }` last on any action to abort the fetch. Types come from the real `*.server.ts`, so declare the last argument there:
79
+ `action()` is identity — it only marks the export. Wrap `async function*` in it to stream over tacho SSE. `oxidejs/tsconfig` makes `await ticks()` typecheck. Pass `{ signal }` last on any action to abort the fetch. Types come from the real `*.server.ts`, so declare the last argument there:
77
80
 
78
81
  ```ts
79
82
  // src/test.server.ts
83
+ import { action } from "oxidejs";
80
84
  import type { ActionOptions } from "oxidejs";
81
85
 
82
- export async function* ticks(n: number, _opts?: ActionOptions) {
86
+ export const ticks = action(async function* (n: number, _opts?: ActionOptions) {
83
87
  for (let i = 0; i < n; i++) yield i;
84
- }
88
+ });
85
89
 
86
90
  // src/client.ts
87
91
  import { ticks } from "./test.server";
@@ -91,7 +95,7 @@ const stream = await ticks(10, { signal: ac.signal });
91
95
  ac.abort();
92
96
  ```
93
97
 
94
- `vite dev` and `rsbuild dev` serve `/_action` via middleware. `oxide({ actions: "ws" })` uses a WebSocket instead (needs `crossws`; not with `preset: "celld"`). `actionHeaders` are static headers on the shared HTTP client.
98
+ `vite dev` and `rsbuild dev` serve the endpoint via middleware. `actions: "http"` (default) serves `/__oxide/action`; `actions: "ws"` uses a WebSocket instead (needs `crossws`; not with `preset: "celld"`). `actions.sameOrigin` defaults to `true` for both transports; set it to `false` only when you intentionally accept cross-origin requests. Set `actions.path` to move the endpoint. `actionHeaders` are static headers on the shared HTTP client.
95
99
 
96
100
  ## Rsbuild
97
101
 
@@ -105,26 +109,26 @@ export default defineConfig({
105
109
  });
106
110
  ```
107
111
 
108
- Same factory as Vite: client stubs, `/_action`, and `dist/server.js`.
112
+ Same factory as Vite: client stubs, `/__oxide/action`, and `dist/server.js`.
109
113
 
110
114
  ## Options
111
115
 
112
- | Option | Default | Notes |
113
- | ------------------------------ | ------------------------ | -------------------------------------- |
114
- | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
115
- | `workerEntry` | `src/server.ts` | Relative to project root |
116
- | `outDir` | `dist` | Output root |
117
- | `clientDir` | `client` | Must stay inside `outDir` |
118
- | `wrangler.name` | required if `emitConfig` | |
119
- | `wrangler.compatibility_date` | required if `emitConfig` | |
120
- | `wrangler.compatibility_flags` | — | optional |
121
- | `wrangler.durable_objects` | — | optional |
122
- | `wrangler.migrations` | — | optional |
123
- | `wrangler.services` | — | optional |
124
- | `wrangler.vars` | — | optional |
125
- | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
126
- | `actions` | `"http"` | `"ws"` needs `crossws`; not with celld |
127
- | `actionHeaders` | — | Static headers on the HTTP client |
116
+ | Option | Default | Notes |
117
+ | ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
118
+ | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
119
+ | `workerEntry` | `src/server.ts` | Relative to project root |
120
+ | `outDir` | `dist` | Output root |
121
+ | `clientDir` | `client` | Must stay inside `outDir` |
122
+ | `wrangler.name` | required if `emitConfig` | |
123
+ | `wrangler.compatibility_date` | required if `emitConfig` | |
124
+ | `wrangler.compatibility_flags` | — | optional |
125
+ | `wrangler.durable_objects` | — | optional |
126
+ | `wrangler.migrations` | — | optional |
127
+ | `wrangler.services` | — | optional |
128
+ | `wrangler.vars` | — | optional |
129
+ | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
130
+ | `actions` | `"http"` | `"ws"` needs `crossws`; object form: `{ transport, path, sameOrigin }` (`sameOrigin: true`) |
131
+ | `actionHeaders` | — | Static headers on the HTTP client |
128
132
 
129
133
  `main` is always `./server.js`. `assets` is added only when `index.html` exists. Unknown wrangler keys fail at build time.
130
134
 
@@ -154,8 +158,9 @@ The generated `__asset` function uses `path.join` — not `path.resolve` — so
154
158
 
155
159
  ### Server actions (`*.server.{ts,tsx,js,jsx}`)
156
160
 
157
- - Server action code is **never bundled into the client**. Client imports are replaced with tacho stubs that POST `/_action`. The original source stays server-only.
158
- - `/_action` is POST-only. Non-POST requests return `405`.
161
+ - Server action code is **never bundled into the client**. Client imports are replaced with tacho stubs that POST the action endpoint (default `/__oxide/action`). The original source stays server-only.
162
+ - Only `action()`-wrapped exports are exposed as RPC; other exports stay server-local.
163
+ - The endpoint is POST-only. Non-POST requests return `405`.
159
164
  - Method dispatch uses `Object.hasOwn`, blocking `__proto__` / `constructor` walks.
160
165
  - Unknown or missing content-types → `415`.
161
166
  - Body size capped at 1 MB by default (enforced on the actual body, not just `Content-Length`).
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-CFWyYfds.mjs";
1
+ import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-C3ualQ39.mjs";
2
2
  import { UnpluginFactory } from "unplugin";
3
3
  //#region src/context.d.ts
4
4
  type ExecutionContext = {
@@ -12,9 +12,9 @@ type ActionContext = {
12
12
  fetchCtx?: ExecutionContext;
13
13
  [key: string]: unknown;
14
14
  };
15
- /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/_action`. */
15
+ /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/__oxide/action`. */
16
16
  declare function useCtx<C extends ActionContext = ActionContext>(): C;
17
- /** Current action `Request`. Throws outside `*.server.ts` running over `/_action`. */
17
+ /** Current action `Request`. Throws outside `*.server.ts` running over `/__oxide/action`. */
18
18
  declare function useRequest(): Request;
19
19
  /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
20
20
  declare function useEnv<E = unknown>(): E | undefined;
@@ -24,10 +24,16 @@ declare function useFetchCtx(): ExecutionContext | undefined;
24
24
  type ActionOptions = {
25
25
  signal?: AbortSignal;
26
26
  };
27
+ /**
28
+ * Marks a `*.server.ts` export as a remote RPC action. Identity: returns `fn` unchanged.
29
+ * Only exports wrapped in `action()` become callable over the wire; other exports stay
30
+ * server-local. Wrap async functions and async generators.
31
+ */
32
+ declare function action<T>(fn: T): T;
27
33
  //#endregion
28
34
  //#region src/index.d.ts
29
35
  declare const unpluginFactory: UnpluginFactory<OxidejsOptions | undefined>;
30
36
  declare const oxidejs: import("unplugin").UnpluginInstance<OxidejsOptions | undefined, boolean>;
31
37
  declare const vite: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
32
38
  //#endregion
33
- export { type ActionContext, type ActionOptions, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
39
+ export { type ActionContext, type ActionOptions, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as useEnv, i as useCtx, n as unpluginFactory, o as useFetchCtx, r as vite, s as useRequest, t as oxidejs } from "./src-BWalutn_.mjs";
2
- export { oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
1
+ import { a as useCtx, c as useRequest, i as action, n as unpluginFactory, o as useEnv, r as vite, s as useFetchCtx, t as oxidejs } from "./src-GLi2EcPa.mjs";
2
+ export { action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
@@ -1,4 +1,4 @@
1
- import { r as OxidejsOptions } from "./types-CFWyYfds.mjs";
1
+ import { r as OxidejsOptions } from "./types-C3ualQ39.mjs";
2
2
  //#region src/rsbuild.d.ts
3
3
  declare const _default: (options?: OxidejsOptions | undefined) => any;
4
4
  //#endregion
package/dist/rsbuild.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as oxidejs } from "./src-BWalutn_.mjs";
1
+ import { t as oxidejs } from "./src-GLi2EcPa.mjs";
2
2
  //#region src/rsbuild.ts
3
3
  var rsbuild_default = oxidejs.rsbuild;
4
4
  //#endregion
@@ -10,14 +10,15 @@ const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
10
10
  const RESOLVED_VIRTUAL_WORKER_ID = `\0${VIRTUAL_WORKER_ID}`;
11
11
  const VIRTUAL_CLIENT_ID = "virtual:oxide/client";
12
12
  const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
13
- const ACTION_PATH = "/_action";
13
+ const ACTION_PATH = "/__oxide/action";
14
14
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
15
15
  "node_modules",
16
16
  "dist",
17
17
  ".git",
18
18
  ".wrangler"
19
19
  ]);
20
- const EXPORT_RE = /^\s*export\s+(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)|^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=/gm;
20
+ /** Only `export const name = action(...)` become remote RPC actions. Everything else stays server-local. */
21
+ const EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?action\s*\(/gm;
21
22
  function isServerFileId(id) {
22
23
  const file = id.split("?")[0]?.replace(/\\/g, "/") ?? "";
23
24
  return [
@@ -33,7 +34,7 @@ function moduleKey(absFile) {
33
34
  function parseExportedNames(source) {
34
35
  const names = /* @__PURE__ */ new Set();
35
36
  for (const match of source.matchAll(EXPORT_RE)) {
36
- const name = match[1] ?? match[2];
37
+ const name = match[1];
37
38
  if (name) names.add(name);
38
39
  }
39
40
  return [...names];
@@ -75,20 +76,20 @@ function scanServerFiles(root) {
75
76
  }
76
77
  return modules;
77
78
  }
78
- function generateClientModule(transport = "http", headers) {
79
+ function generateClientModule(transport = "http", headers, path = ACTION_PATH) {
79
80
  if (transport === "ws") return `import { createClient } from "tacho/client/ws";
80
81
  const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
81
82
  const __host = typeof location === "undefined" ? "localhost" : location.host;
82
- export const client = createClient({ url: __proto + "//" + __host + ${JSON.stringify(ACTION_PATH)} });
83
+ export const client = createClient({ url: __proto + "//" + __host + ${JSON.stringify(path)} });
83
84
  `;
84
- const opts = { url: ACTION_PATH };
85
+ const opts = { url: path };
85
86
  if (headers) opts.headers = headers;
86
87
  return `import { createClient } from "tacho/client/http";
87
88
  export const client = createClient(${JSON.stringify(opts)});
88
89
  `;
89
90
  }
90
91
  function generateClientStub(mod) {
91
- const lines = [`import { client } from ${JSON.stringify(VIRTUAL_CLIENT_ID)};`];
92
+ const lines = [`// oxidejs:client-stub`, `import { client } from ${JSON.stringify(VIRTUAL_CLIENT_ID)};`];
92
93
  for (const name of mod.exports) lines.push(`export const ${name} = (...args) => {
93
94
  const opts = args.at(-1);
94
95
  // ponytail: peel last { signal } only. A lone payload { signal: AbortSignal } is treated as CallOptions.
@@ -102,7 +103,8 @@ function generateActionsModule(modules, opts) {
102
103
  const lines = [
103
104
  `import { AsyncLocalStorage } from "node:async_hooks";`,
104
105
  `import { tacho } from "tacho";`,
105
- `const __als = globalThis.__oxidejsRequest ??= new AsyncLocalStorage();`
106
+ `const __alsKey = Symbol.for("oxidejs.requestContext");`,
107
+ `const __als = globalThis[__alsKey] ??= new AsyncLocalStorage();`
106
108
  ];
107
109
  const aliases = modules.map((mod, i) => {
108
110
  const alias = `__m${i}`;
@@ -168,6 +170,8 @@ function pipeResponse(req, res, response) {
168
170
  function generateWorkerWrapper(userWorkerAbs, opts = {}) {
169
171
  const preset = opts.preset ?? "fetch";
170
172
  const clientDir = opts.clientDir ?? "client";
173
+ const actionPath = opts.actionPath ?? "/__oxide/action";
174
+ const sameOrigin = opts.actionSameOrigin ?? false;
171
175
  const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
172
176
  const hasActions = opts.hasActions !== false;
173
177
  const ws = hasActions && opts.actions === "ws";
@@ -251,7 +255,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
251
255
  import("crossws/adapters/node").then(({ default: crossws }) => {
252
256
  const ws = crossws({ hooks: __ws });
253
257
  server.on("upgrade", (req, socket, head) => {
254
- if (req.url?.split("?")[0] === ${JSON.stringify(ACTION_PATH)}) ws.handleUpgrade(req, socket, head);
258
+ if (req.url?.split("?")[0] === ${JSON.stringify(actionPath)}) ws.handleUpgrade(req, socket, head);
255
259
  });
256
260
  });` : ""}
257
261
  server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
@@ -259,13 +263,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
259
263
  ` : "";
260
264
  const actionImports = hasActions ? ws ? `import { handle as handleWs } from "tacho/transport/ws";
261
265
  import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
262
- const __ws = handleWs(actions, { path: ${JSON.stringify(ACTION_PATH)} });
266
+ const __ws = handleWs(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin ? `, sameOrigin: true` : ``} });
263
267
  ` : `import { handle } from "tacho/transport/fetch";
264
268
  import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
265
269
  const __fetch = Symbol.for("oxidejs.fetch");
266
- const __rpc = handle(actions, { path: ${JSON.stringify(ACTION_PATH)}, createContext: (req) => req[__fetch] ?? {} });
270
+ const __rpc = handle(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin ? `, sameOrigin: true` : ``}, createContext: (req) => req[__fetch] ?? {} });
267
271
  ` : "";
268
- const actionGate = hasActions && !ws ? `if (new URL(request.url).pathname === ${JSON.stringify(ACTION_PATH)}) {
272
+ const actionGate = hasActions && !ws ? `if (new URL(request.url).pathname === ${JSON.stringify(actionPath)}) {
269
273
  request[__fetch] = { env, fetchCtx: ctx };
270
274
  return __rpc(request);
271
275
  }
@@ -408,11 +412,30 @@ function hasHtmlEntry(root, config) {
408
412
  return file.endsWith(".html") && fs.existsSync(file);
409
413
  });
410
414
  }
415
+ function resolveActions(raw) {
416
+ if (raw === void 0 || typeof raw === "string") {
417
+ const transport = raw ?? "http";
418
+ if (transport !== "http" && transport !== "ws") throw new Error(`oxidejs: unknown actions transport "${String(transport)}"`);
419
+ return {
420
+ transport,
421
+ path: ACTION_PATH,
422
+ sameOrigin: true
423
+ };
424
+ }
425
+ const transport = raw.transport ?? "http";
426
+ if (transport !== "http" && transport !== "ws") throw new Error(`oxidejs: unknown actions transport "${String(transport)}"`);
427
+ const path = raw.path ?? "/__oxide/action";
428
+ if (!path.startsWith("/") || path.includes("?")) throw new Error(`oxidejs: actions.path must start with "/" and contain no query string (got "${path}")`);
429
+ return {
430
+ transport,
431
+ path,
432
+ sameOrigin: raw.sameOrigin ?? true
433
+ };
434
+ }
411
435
  function resolveOptions(raw, root, config) {
412
436
  const preset = raw?.preset ?? "fetch";
413
437
  if (preset !== "fetch" && preset !== "celld") throw new Error(`oxidejs: unknown preset "${String(preset)}"`);
414
- const actions = raw?.actions ?? "http";
415
- if (actions !== "http" && actions !== "ws") throw new Error(`oxidejs: unknown actions transport "${String(actions)}"`);
438
+ const { transport: actions, path: actionPath, sameOrigin: actionSameOrigin } = resolveActions(raw?.actions);
416
439
  if (actions === "ws" && preset === "celld") throw new Error("oxidejs: actions: \"ws\" is not supported with preset: \"celld\"");
417
440
  const workerEntry = raw?.workerEntry ?? "src/server.ts";
418
441
  const outDirInput = raw?.outDir ?? "dist";
@@ -437,6 +460,8 @@ function resolveOptions(raw, root, config) {
437
460
  hasClient,
438
461
  hasPublic,
439
462
  actions,
463
+ actionPath,
464
+ actionSameOrigin,
440
465
  actionHeaders: raw?.actionHeaders
441
466
  };
442
467
  }
@@ -581,7 +606,7 @@ function applyRsbuildEnvironments(config, opts) {
581
606
  }
582
607
  //#endregion
583
608
  //#region src/context.ts
584
- const ALS_KEY = "__oxidejsRequest";
609
+ const ALS_KEY = Symbol.for("oxidejs.requestContext");
585
610
  function als() {
586
611
  const g = globalThis;
587
612
  return g[ALS_KEY] ??= new AsyncLocalStorage();
@@ -591,11 +616,11 @@ function store() {
591
616
  if (!current) throw new Error("oxidejs: useRequest() called outside an action");
592
617
  return current;
593
618
  }
594
- /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/_action`. */
619
+ /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/__oxide/action`. */
595
620
  function useCtx() {
596
621
  return store();
597
622
  }
598
- /** Current action `Request`. Throws outside `*.server.ts` running over `/_action`. */
623
+ /** Current action `Request`. Throws outside `*.server.ts` running over `/__oxide/action`. */
599
624
  function useRequest() {
600
625
  return store().req;
601
626
  }
@@ -607,11 +632,19 @@ function useEnv() {
607
632
  function useFetchCtx() {
608
633
  return store().fetchCtx;
609
634
  }
635
+ /**
636
+ * Marks a `*.server.ts` export as a remote RPC action. Identity: returns `fn` unchanged.
637
+ * Only exports wrapped in `action()` become callable over the wire; other exports stay
638
+ * server-local. Wrap async functions and async generators.
639
+ */
640
+ function action(fn) {
641
+ return fn;
642
+ }
610
643
  //#endregion
611
644
  //#region src/index.ts
612
- function actionMiddleware(loadRouter) {
645
+ function actionMiddleware(loadRouter, path, sameOrigin) {
613
646
  return (req, res, next) => {
614
- if ((req.url ?? "").split("?")[0] !== "/_action") {
647
+ if ((req.url ?? "").split("?")[0] !== path) {
615
648
  next();
616
649
  return;
617
650
  }
@@ -620,11 +653,14 @@ function actionMiddleware(loadRouter) {
620
653
  /* @vite-ignore */
621
654
  "tacho/transport/fetch"
622
655
  );
623
- await sendWebResponseFrom(req, res, await handle(await loadRouter(), { path: ACTION_PATH })(await nodeToWebRequest(req)));
656
+ await sendWebResponseFrom(req, res, await handle(await loadRouter(), {
657
+ path,
658
+ ...sameOrigin ? { sameOrigin: true } : {}
659
+ })(await nodeToWebRequest(req)));
624
660
  })().catch(next);
625
661
  };
626
662
  }
627
- function attachActionUpgrade(httpServer, loadRouter) {
663
+ function attachActionUpgrade(httpServer, loadRouter, path, sameOrigin) {
628
664
  if (!httpServer) return;
629
665
  Promise.all([import(
630
666
  /* @vite-ignore */
@@ -634,8 +670,11 @@ function attachActionUpgrade(httpServer, loadRouter) {
634
670
  "crossws/adapters/node"
635
671
  )]).then(([{ handle }, { default: crossws }]) => {
636
672
  httpServer.on("upgrade", (req, socket, head) => {
637
- if ((req.url ?? "").split("?")[0] !== "/_action") return;
638
- loadRouter().then((router) => crossws({ hooks: handle(router, { path: ACTION_PATH }) }).handleUpgrade(req, socket, head)).catch(() => {
673
+ if ((req.url ?? "").split("?")[0] !== path) return;
674
+ loadRouter().then((router) => crossws({ hooks: handle(router, {
675
+ path,
676
+ sameOrigin
677
+ }) }).handleUpgrade(req, socket, head)).catch(() => {
639
678
  socket.destroy();
640
679
  });
641
680
  });
@@ -675,7 +714,7 @@ const unpluginFactory = (options) => {
675
714
  return null;
676
715
  },
677
716
  load(id, extra) {
678
- if (id === RESOLVED_VIRTUAL_CLIENT_ID) return generateClientModule(resolved?.actions ?? options?.actions ?? "http", resolved?.actionHeaders ?? options?.actionHeaders);
717
+ if (id === RESOLVED_VIRTUAL_CLIENT_ID) return generateClientModule(resolved?.actions ?? (typeof options?.actions === "string" || options?.actions === void 0 ? options?.actions : options.actions.transport) ?? "http", resolved?.actionHeaders ?? options?.actionHeaders, resolved?.actionPath);
679
718
  if (id === RESOLVED_VIRTUAL_ACTIONS_ID || id === RESOLVED_VIRTUAL_WORKER_ID) {
680
719
  if (pluginShouldStub(this, extra)) throw new Error(`oxidejs: ${id === RESOLVED_VIRTUAL_ACTIONS_ID ? VIRTUAL_ACTIONS_ID : VIRTUAL_WORKER_ID} is server-only`);
681
720
  }
@@ -695,7 +734,9 @@ const unpluginFactory = (options) => {
695
734
  hasClient: resolved.hasClient,
696
735
  hasPublic: resolved.hasPublic,
697
736
  hasActions: modules.length > 0,
698
- actions: resolved.actions
737
+ actions: resolved.actions,
738
+ actionPath: resolved.actionPath,
739
+ actionSameOrigin: resolved.actionSameOrigin
699
740
  });
700
741
  }
701
742
  if (isServerFileId(id) && pluginShouldStub(this, extra)) {
@@ -705,7 +746,7 @@ const unpluginFactory = (options) => {
705
746
  }
706
747
  },
707
748
  transform(code, id, extra) {
708
- if (!isServerFileId(id) || !pluginShouldStub(this, extra)) return;
749
+ if (!isServerFileId(id) || !pluginShouldStub(this, extra) || code.startsWith("// oxidejs:client-stub\n")) return;
709
750
  return generateClientStub({
710
751
  key: moduleKey(id.split("?")[0] ?? id),
711
752
  exports: parseExportedNames(code)
@@ -729,8 +770,8 @@ const unpluginFactory = (options) => {
729
770
  const loadRouter = async () => {
730
771
  return (await server.ssrLoadModule(VIRTUAL_ACTIONS_ID)).default;
731
772
  };
732
- if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
733
- else server.middlewares.use(actionMiddleware(loadRouter));
773
+ if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
774
+ else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin));
734
775
  },
735
776
  configurePreviewServer(server) {
736
777
  if (resolved?.preset !== "fetch") return;
@@ -746,8 +787,8 @@ const unpluginFactory = (options) => {
746
787
  const loadRouter = async () => {
747
788
  return (await loadActions(resolved?.root ?? process.cwd())).default;
748
789
  };
749
- if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
750
- else server.middlewares.use(actionMiddleware(loadRouter));
790
+ if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
791
+ else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin));
751
792
  });
752
793
  api.onBeforeStartPreviewServer?.(({ server }) => {
753
794
  if (resolved?.preset !== "fetch") return;
@@ -764,4 +805,4 @@ const unpluginFactory = (options) => {
764
805
  const oxidejs = /* @__PURE__ */ createUnplugin(unpluginFactory);
765
806
  const vite = /* @__PURE__ */ (() => oxidejs.vite)();
766
807
  //#endregion
767
- export { useEnv as a, useCtx as i, unpluginFactory as n, useFetchCtx as o, vite as r, useRequest as s, oxidejs as t };
808
+ export { useCtx as a, useRequest as c, action as i, unpluginFactory as n, useEnv as o, vite as r, useFetchCtx as s, oxidejs as t };
@@ -10,6 +10,14 @@ interface OxidejsWranglerOptions {
10
10
  vars?: Record<string, unknown>;
11
11
  }
12
12
  type OxidejsActionTransport = "http" | "ws";
13
+ /** `actions` config: transport string, or an object with `transport`, `path`, `sameOrigin`. */
14
+ type OxidejsActions = OxidejsActionTransport | {
15
+ transport?: OxidejsActionTransport;
16
+ /** Endpoint path for actions. Default: `/__oxide/action`. */
17
+ path?: string;
18
+ /** Reject cross-origin action requests (CSRF defense). Default: true. */
19
+ sameOrigin?: boolean;
20
+ };
13
21
  /** Static headers inlined into the shared action client. Functions cannot ship to the browser. */
14
22
  type OxidejsActionHeaders = Record<string, string> | [string, string][];
15
23
  interface OxidejsOptions {
@@ -25,8 +33,8 @@ interface OxidejsOptions {
25
33
  wrangler?: OxidejsWranglerOptions;
26
34
  /** Skip config emission. Defaults to false for celld, true for fetch. */
27
35
  emitConfig?: boolean;
28
- /** Transport for `*.server.ts` stubs. Default: "http". */
29
- actions?: OxidejsActionTransport;
36
+ /** Transport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`. */
37
+ actions?: OxidejsActions;
30
38
  /** Extra headers on the shared HTTP action client. Ignored when `actions` is "ws". */
31
39
  actionHeaders?: OxidejsActionHeaders;
32
40
  }
@@ -46,6 +54,10 @@ interface ResolvedOptions {
46
54
  /** True when `<root>/public` exists. Copied next to client assets on fetch. */
47
55
  hasPublic: boolean;
48
56
  actions: OxidejsActionTransport;
57
+ /** Endpoint path for actions. Default: `/__oxide/action`. */
58
+ actionPath: string;
59
+ /** Reject cross-origin action requests (CSRF defense). Default: true. */
60
+ actionSameOrigin: boolean;
49
61
  actionHeaders: OxidejsActionHeaders | undefined;
50
62
  }
51
63
  //#endregion
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as OxidejsOptions } from "./types-CFWyYfds.mjs";
1
+ import { r as OxidejsOptions } from "./types-C3ualQ39.mjs";
2
2
  //#region src/vite.d.ts
3
3
  declare const _default: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
4
4
  //#endregion
package/dist/vite.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as oxidejs } from "./src-BWalutn_.mjs";
1
+ import { t as oxidejs } from "./src-GLi2EcPa.mjs";
2
2
  //#region src/vite.ts
3
3
  var vite_default = oxidejs.vite;
4
4
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxidejs",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Vite/Rsbuild plugin. One build → dist/server.js + optional client. Server actions via *.server.ts.",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -59,7 +59,7 @@
59
59
  "unplugin": "^3.3.0"
60
60
  },
61
61
  "devDependencies": {
62
- "tacho": "^0.4.2"
62
+ "tacho": "^0.4.3"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@rsbuild/core": "*",