oxidejs 0.1.6 → 0.1.8

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 `*.server.ts`) → `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.js` 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.ts` → 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
 
@@ -152,10 +156,11 @@ The generated server serves static files from `dist/client/` (or the `public/` d
152
156
 
153
157
  The generated `__asset` function uses `path.join` — not `path.resolve` — so a leading `/` in the relative path stays inside the asset root.
154
158
 
155
- ### Server actions (`*.server.ts`)
159
+ ### Server actions (`*.server.{ts,tsx,js,jsx}`)
156
160
 
157
- - `*.server.ts` 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-CU7sBRGf.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-Cg_j9ZsH.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-CU7sBRGf.mjs";
1
+ import { t as oxidejs } from "./src-Cg_j9ZsH.mjs";
2
2
  //#region src/rsbuild.ts
3
3
  var rsbuild_default = oxidejs.rsbuild;
4
4
  //#endregion
@@ -10,25 +10,31 @@ 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
- return file.endsWith(".server.ts") || file.endsWith(".server.js");
24
+ return [
25
+ ".ts",
26
+ ".tsx",
27
+ ".js",
28
+ ".jsx"
29
+ ].some((ext) => file.endsWith(`.server${ext}`));
24
30
  }
25
31
  function moduleKey(absFile) {
26
- return path.basename(absFile).replace(/\.server\.(ts|js)$/i, "");
32
+ return path.basename(absFile).replace(/\.server\.(?:[jt]sx?)$/i, "");
27
33
  }
28
34
  function parseExportedNames(source) {
29
35
  const names = /* @__PURE__ */ new Set();
30
36
  for (const match of source.matchAll(EXPORT_RE)) {
31
- const name = match[1] ?? match[2];
37
+ const name = match[1];
32
38
  if (name) names.add(name);
33
39
  }
34
40
  return [...names];
@@ -70,13 +76,13 @@ function scanServerFiles(root) {
70
76
  }
71
77
  return modules;
72
78
  }
73
- function generateClientModule(transport = "http", headers) {
79
+ function generateClientModule(transport = "http", headers, path = ACTION_PATH) {
74
80
  if (transport === "ws") return `import { createClient } from "tacho/client/ws";
75
81
  const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
76
82
  const __host = typeof location === "undefined" ? "localhost" : location.host;
77
- export const client = createClient({ url: __proto + "//" + __host + ${JSON.stringify(ACTION_PATH)} });
83
+ export const client = createClient({ url: __proto + "//" + __host + ${JSON.stringify(path)} });
78
84
  `;
79
- const opts = { url: ACTION_PATH };
85
+ const opts = { url: path };
80
86
  if (headers) opts.headers = headers;
81
87
  return `import { createClient } from "tacho/client/http";
82
88
  export const client = createClient(${JSON.stringify(opts)});
@@ -97,7 +103,8 @@ function generateActionsModule(modules, opts) {
97
103
  const lines = [
98
104
  `import { AsyncLocalStorage } from "node:async_hooks";`,
99
105
  `import { tacho } from "tacho";`,
100
- `const __als = globalThis.__oxidejsRequest ??= new AsyncLocalStorage();`
106
+ `const __alsKey = Symbol.for("oxidejs.requestContext");`,
107
+ `const __als = globalThis[__alsKey] ??= new AsyncLocalStorage();`
101
108
  ];
102
109
  const aliases = modules.map((mod, i) => {
103
110
  const alias = `__m${i}`;
@@ -163,6 +170,8 @@ function pipeResponse(req, res, response) {
163
170
  function generateWorkerWrapper(userWorkerAbs, opts = {}) {
164
171
  const preset = opts.preset ?? "fetch";
165
172
  const clientDir = opts.clientDir ?? "client";
173
+ const actionPath = opts.actionPath ?? "/__oxide/action";
174
+ const sameOrigin = opts.actionSameOrigin ?? false;
166
175
  const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
167
176
  const hasActions = opts.hasActions !== false;
168
177
  const ws = hasActions && opts.actions === "ws";
@@ -246,7 +255,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
246
255
  import("crossws/adapters/node").then(({ default: crossws }) => {
247
256
  const ws = crossws({ hooks: __ws });
248
257
  server.on("upgrade", (req, socket, head) => {
249
- 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);
250
259
  });
251
260
  });` : ""}
252
261
  server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
@@ -254,13 +263,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
254
263
  ` : "";
255
264
  const actionImports = hasActions ? ws ? `import { handle as handleWs } from "tacho/transport/ws";
256
265
  import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
257
- const __ws = handleWs(actions, { path: ${JSON.stringify(ACTION_PATH)} });
266
+ const __ws = handleWs(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin ? `, sameOrigin: true` : ``} });
258
267
  ` : `import { handle } from "tacho/transport/fetch";
259
268
  import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
260
269
  const __fetch = Symbol.for("oxidejs.fetch");
261
- 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] ?? {} });
262
271
  ` : "";
263
- 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)}) {
264
273
  request[__fetch] = { env, fetchCtx: ctx };
265
274
  return __rpc(request);
266
275
  }
@@ -403,11 +412,30 @@ function hasHtmlEntry(root, config) {
403
412
  return file.endsWith(".html") && fs.existsSync(file);
404
413
  });
405
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
+ }
406
435
  function resolveOptions(raw, root, config) {
407
436
  const preset = raw?.preset ?? "fetch";
408
437
  if (preset !== "fetch" && preset !== "celld") throw new Error(`oxidejs: unknown preset "${String(preset)}"`);
409
- const actions = raw?.actions ?? "http";
410
- 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);
411
439
  if (actions === "ws" && preset === "celld") throw new Error("oxidejs: actions: \"ws\" is not supported with preset: \"celld\"");
412
440
  const workerEntry = raw?.workerEntry ?? "src/server.ts";
413
441
  const outDirInput = raw?.outDir ?? "dist";
@@ -432,6 +460,8 @@ function resolveOptions(raw, root, config) {
432
460
  hasClient,
433
461
  hasPublic,
434
462
  actions,
463
+ actionPath,
464
+ actionSameOrigin,
435
465
  actionHeaders: raw?.actionHeaders
436
466
  };
437
467
  }
@@ -576,7 +606,7 @@ function applyRsbuildEnvironments(config, opts) {
576
606
  }
577
607
  //#endregion
578
608
  //#region src/context.ts
579
- const ALS_KEY = "__oxidejsRequest";
609
+ const ALS_KEY = Symbol.for("oxidejs.requestContext");
580
610
  function als() {
581
611
  const g = globalThis;
582
612
  return g[ALS_KEY] ??= new AsyncLocalStorage();
@@ -586,11 +616,11 @@ function store() {
586
616
  if (!current) throw new Error("oxidejs: useRequest() called outside an action");
587
617
  return current;
588
618
  }
589
- /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/_action`. */
619
+ /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/__oxide/action`. */
590
620
  function useCtx() {
591
621
  return store();
592
622
  }
593
- /** Current action `Request`. Throws outside `*.server.ts` running over `/_action`. */
623
+ /** Current action `Request`. Throws outside `*.server.ts` running over `/__oxide/action`. */
594
624
  function useRequest() {
595
625
  return store().req;
596
626
  }
@@ -602,11 +632,19 @@ function useEnv() {
602
632
  function useFetchCtx() {
603
633
  return store().fetchCtx;
604
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
+ }
605
643
  //#endregion
606
644
  //#region src/index.ts
607
- function actionMiddleware(loadRouter) {
645
+ function actionMiddleware(loadRouter, path, sameOrigin) {
608
646
  return (req, res, next) => {
609
- if ((req.url ?? "").split("?")[0] !== "/_action") {
647
+ if ((req.url ?? "").split("?")[0] !== path) {
610
648
  next();
611
649
  return;
612
650
  }
@@ -615,11 +653,14 @@ function actionMiddleware(loadRouter) {
615
653
  /* @vite-ignore */
616
654
  "tacho/transport/fetch"
617
655
  );
618
- 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)));
619
660
  })().catch(next);
620
661
  };
621
662
  }
622
- function attachActionUpgrade(httpServer, loadRouter) {
663
+ function attachActionUpgrade(httpServer, loadRouter, path, sameOrigin) {
623
664
  if (!httpServer) return;
624
665
  Promise.all([import(
625
666
  /* @vite-ignore */
@@ -629,8 +670,11 @@ function attachActionUpgrade(httpServer, loadRouter) {
629
670
  "crossws/adapters/node"
630
671
  )]).then(([{ handle }, { default: crossws }]) => {
631
672
  httpServer.on("upgrade", (req, socket, head) => {
632
- if ((req.url ?? "").split("?")[0] !== "/_action") return;
633
- 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(() => {
634
678
  socket.destroy();
635
679
  });
636
680
  });
@@ -670,7 +714,7 @@ const unpluginFactory = (options) => {
670
714
  return null;
671
715
  },
672
716
  load(id, extra) {
673
- 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);
674
718
  if (id === RESOLVED_VIRTUAL_ACTIONS_ID || id === RESOLVED_VIRTUAL_WORKER_ID) {
675
719
  if (pluginShouldStub(this, extra)) throw new Error(`oxidejs: ${id === RESOLVED_VIRTUAL_ACTIONS_ID ? VIRTUAL_ACTIONS_ID : VIRTUAL_WORKER_ID} is server-only`);
676
720
  }
@@ -690,7 +734,9 @@ const unpluginFactory = (options) => {
690
734
  hasClient: resolved.hasClient,
691
735
  hasPublic: resolved.hasPublic,
692
736
  hasActions: modules.length > 0,
693
- actions: resolved.actions
737
+ actions: resolved.actions,
738
+ actionPath: resolved.actionPath,
739
+ actionSameOrigin: resolved.actionSameOrigin
694
740
  });
695
741
  }
696
742
  if (isServerFileId(id) && pluginShouldStub(this, extra)) {
@@ -724,8 +770,8 @@ const unpluginFactory = (options) => {
724
770
  const loadRouter = async () => {
725
771
  return (await server.ssrLoadModule(VIRTUAL_ACTIONS_ID)).default;
726
772
  };
727
- if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
728
- 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));
729
775
  },
730
776
  configurePreviewServer(server) {
731
777
  if (resolved?.preset !== "fetch") return;
@@ -741,8 +787,8 @@ const unpluginFactory = (options) => {
741
787
  const loadRouter = async () => {
742
788
  return (await loadActions(resolved?.root ?? process.cwd())).default;
743
789
  };
744
- if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
745
- 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));
746
792
  });
747
793
  api.onBeforeStartPreviewServer?.(({ server }) => {
748
794
  if (resolved?.preset !== "fetch") return;
@@ -759,4 +805,4 @@ const unpluginFactory = (options) => {
759
805
  const oxidejs = /* @__PURE__ */ createUnplugin(unpluginFactory);
760
806
  const vite = /* @__PURE__ */ (() => oxidejs.vite)();
761
807
  //#endregion
762
- 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-CU7sBRGf.mjs";
1
+ import { t as oxidejs } from "./src-Cg_j9ZsH.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.6",
3
+ "version": "0.1.8",
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": "*",