oxidejs 0.2.0 → 0.2.2

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
@@ -76,15 +76,15 @@ export default {
76
76
  };
77
77
  ```
78
78
 
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:
79
+ `action()` is runtime identity — it marks the export and adds a typed transport-only `{ signal }` argument. Wrap `async function*` in it to stream over tacho SSE. Inside server code, always read the non-optional signal from `useRequest().signal`:
80
80
 
81
81
  ```ts
82
82
  // src/test.server.ts
83
- import { action } from "oxidejs";
84
- import type { ActionOptions } from "oxidejs";
83
+ import { action, useRequest } from "oxidejs";
85
84
 
86
- export const ticks = action(async function* (n: number, _opts?: ActionOptions) {
87
- for (let i = 0; i < n; i++) yield i;
85
+ export const ticks = action(async function* (n: number) {
86
+ const { signal } = useRequest();
87
+ for (let i = 0; i < n && !signal.aborted; i++) yield i;
88
88
  });
89
89
 
90
90
  // src/client.ts
@@ -113,27 +113,6 @@ Same factory as Vite: client stubs, `/__oxide/action`, and `dist/server.js`.
113
113
 
114
114
  ## Options
115
115
 
116
- ### `middleware` and `imports`
117
-
118
- \`\`\`ts
119
- oxide({
120
- middleware: ["@ilha/router/ssr"], // string or { module, imports }
121
- imports: ["./side-effects"], // side-effect modules loaded at startup
122
- })
123
- \`\`\`
124
-
125
- Middleware handlers run in production before the action gate; the same specifiers are loaded through the SSR graph in dev, so dev and prod behave identically. Middleware entries may carry their own \`imports\`.
126
-
127
- ### Other server options
128
-
129
- | Option | Type | Default | Description |
130
- | ------------- | ------ | ------- | -------------------------------------------------------------------- |
131
- | \`bodyLimit\` | number | 1048576 | Max request body size (Node preset); larger requests get 413 |
132
- | \`notFound\` | string | — | Custom HTML 404 body when no route or asset matches |
133
- | \`env\` | object | — | Passed as \`env\` to \`fetch(request, env, ctx)\` on the Node preset |
134
-
135
- ## Options
136
-
137
116
  | Option | Default | Notes |
138
117
  | ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
139
118
  | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
@@ -150,23 +129,22 @@ Middleware handlers run in production before the action gate; the same specifier
150
129
  | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
151
130
  | `actions` | `"http"` | `"ws"` needs `crossws`; object form: `{ transport, path, sameOrigin }` (`sameOrigin: true`) |
152
131
  | `actionHeaders` | — | Static headers on the HTTP client |
153
- | `middleware` | `[]` | Default-exported production fetch middleware, run in order before actions and server entry |
132
+ | `middleware` | `[]` | Fetch middleware, run in order before actions and the server entry |
133
+ | `imports` | `[]` | Modules imported for side effects at server startup |
134
+ | `bodyLimit` | `1048576` | Max Node request body size; larger requests get 413 |
135
+ | `notFound` | — | Custom HTML 404 body when no route or asset matches |
136
+ | `env` | — | Node preset value passed to `fetch(request, env, ctx)` |
154
137
 
155
- `middleware` modules receive `(request, { env, ctx })`. They run in array order before actions, the server entry, and assets. Return a `Response` to stop the chain or `undefined` to continue.
138
+ ### `middleware` and `imports`
156
139
 
157
140
  ```ts
158
- // src/auth.ts
159
- export default function auth(request: Request) {
160
- if (!request.headers.has("authorization")) {
161
- return new Response("Unauthorized", { status: 401 });
162
- }
163
- }
164
-
165
- // vite.config.ts
166
- oxide({ middleware: ["./src/auth.ts"] });
141
+ oxide({
142
+ middleware: ["@ilha/router/ssr"], // string or { module, imports }
143
+ imports: ["./side-effects"],
144
+ });
167
145
  ```
168
146
 
169
- This option applies to production builds; use Connect middleware in Vite or Rsbuild during development.
147
+ Middleware modules receive `(request, { env, ctx })`. They run before actions, the server entry, and assets. Return a `Response` to stop the chain or `undefined` to continue. Vite loads the same modules through its SSR graph in development. Middleware entries may carry their own `imports`.
170
148
 
171
149
  `main` is always `./server.js`. `assets` is added only when `index.html` exists. Unknown wrangler keys fail at build time.
172
150
 
package/dist/index.d.mts CHANGED
@@ -12,28 +12,25 @@ type ActionContext = {
12
12
  fetchCtx?: ExecutionContext;
13
13
  [key: string]: unknown;
14
14
  };
15
- /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/__oxide/action`. */
15
+ /** Current tacho or host request context. Throws outside request handling. */
16
16
  declare function useCtx<C extends ActionContext = ActionContext>(): C;
17
- /** Current action `Request`. Throws outside `*.server.ts` running over `/__oxide/action`. */
17
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
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;
21
21
  /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
22
22
  declare function useFetchCtx(): ExecutionContext | undefined;
23
- /** Optional last argument on a `*.server.ts` export so the client can pass `{ signal }`. */
24
- type ActionOptions = {
25
- signal?: AbortSignal;
26
- };
27
23
  /**
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.
24
+ * Marks a `*.server.ts` export as a remote RPC action. Runtime identity; the
25
+ * second call signature adds the transport-only `{ signal }` argument.
31
26
  */
32
- declare function action<T>(fn: T): T;
27
+ declare function action<Args extends unknown[], Result>(fn: (...args: Args) => Result): typeof fn & ((...args: [...Args, options: {
28
+ signal?: AbortSignal;
29
+ }]) => Result);
33
30
  //#endregion
34
31
  //#region src/index.d.ts
35
32
  declare const unpluginFactory: UnpluginFactory<OxidejsOptions | undefined>;
36
33
  declare const oxidejs: import("unplugin").UnpluginInstance<OxidejsOptions | undefined, boolean>;
37
34
  declare const vite: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
38
35
  //#endregion
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 };
36
+ export { type ActionContext, 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 useCtx, c as useRequest, i as action, n as unpluginFactory, o as useEnv, r as vite, s as useFetchCtx, t as oxidejs } from "./src-zvdCGkyp.mjs";
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-D-qdNVqg.mjs";
2
2
  export { action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
package/dist/rsbuild.mjs CHANGED
@@ -1,6 +1,5 @@
1
- import { t as oxidejs } from "./src-zvdCGkyp.mjs";
1
+ import { t as oxidejs } from "./src-D-qdNVqg.mjs";
2
2
  //#region src/rsbuild.ts
3
- /** @experimental Rsbuild integration is not yet implemented. */
4
3
  var rsbuild_default = oxidejs.rsbuild;
5
4
  //#endregion
6
5
  export { rsbuild_default as default };
@@ -21,6 +21,7 @@ var actions_exports = /* @__PURE__ */ __exportAll({
21
21
  RESOLVED_VIRTUAL_ACTIONS_ID: () => RESOLVED_VIRTUAL_ACTIONS_ID,
22
22
  RESOLVED_VIRTUAL_CLIENT_ID: () => RESOLVED_VIRTUAL_CLIENT_ID,
23
23
  RESOLVED_VIRTUAL_WORKER_ID: () => RESOLVED_VIRTUAL_WORKER_ID,
24
+ RequestBodyTooLargeError: () => RequestBodyTooLargeError,
24
25
  VIRTUAL_ACTIONS_ID: () => VIRTUAL_ACTIONS_ID,
25
26
  VIRTUAL_CLIENT_ID: () => VIRTUAL_CLIENT_ID,
26
27
  VIRTUAL_WORKER_ID: () => VIRTUAL_WORKER_ID,
@@ -210,7 +211,7 @@ function generateWorkerWrapper(userWorkerAbs, opts = {}) {
210
211
  const hasActions = opts.hasActions !== false;
211
212
  const ws = hasActions && opts.actions === "ws";
212
213
  const bodyLimit = opts.bodyLimit ?? 1048576;
213
- const __nf = `() => new Response(${JSON.stringify(opts.notFound ?? "<h1>404 Not Found</h1>")}, { status: 404, headers: { "content-type": "text/html; charset=utf-8" } })`;
214
+ const nfBlock = `const __nf = ${`() => new Response(${JSON.stringify(opts.notFound ?? "<h1>404 Not Found</h1>")}, { status: 404, headers: { "content-type": "text/html; charset=utf-8" } })`};\n`;
214
215
  const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
215
216
  import { extname, join } from "node:path";
216
217
  const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
@@ -253,7 +254,6 @@ async function __asset(request, spa) {
253
254
  return;
254
255
  }
255
256
  }
256
- const __nf = ${__nf};
257
257
  ` : "";
258
258
  const envJson = JSON.stringify(opts.env ?? {});
259
259
  const afterAction = serveAssets ? `if (typeof user.fetch === "function") {
@@ -326,7 +326,6 @@ const __fetch = Symbol.for("oxidejs.fetch");
326
326
  const __rpc = handle(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin ? `, sameOrigin: true` : ``}, createContext: (req) => req[__fetch] ?? {} });
327
327
  ` : "";
328
328
  const actionGate = hasActions && !ws ? `if (new URL(request.url).pathname === ${JSON.stringify(actionPath)}) {
329
- request[__fetch] = { env, fetchCtx: ctx };
330
329
  return __rpc(request);
331
330
  }
332
331
  ` : "";
@@ -343,9 +342,10 @@ const __rpc = handle(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin
343
342
  ` : "";
344
343
  return `${(opts.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}export * from ${JSON.stringify(userWorkerAbs)};
345
344
  import user from ${JSON.stringify(userWorkerAbs)};
346
- ${middlewareImports}${actionImports}${assetBlock}const app = {
345
+ ${middlewareImports}${actionImports}${assetBlock}${nfBlock}const app = {
347
346
  ...user,
348
347
  async fetch(request, env, ctx) {
348
+ request[__fetch] = { env, fetchCtx: ctx };
349
349
  ${middlewareGate}${actionGate}${afterAction}
350
350
  },
351
351
  };
@@ -400,7 +400,8 @@ function loadClientStub(id) {
400
400
  exports: parseExportedNames(fs.readFileSync(file, "utf8"))
401
401
  });
402
402
  }
403
- async function nodeToWebRequest(req) {
403
+ var RequestBodyTooLargeError = class extends Error {};
404
+ async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
404
405
  const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
405
406
  const headers = new Headers();
406
407
  for (const [key, value] of Object.entries(req.headers)) {
@@ -418,7 +419,13 @@ async function nodeToWebRequest(req) {
418
419
  };
419
420
  if (method === "GET" || method === "HEAD") return new Request(url, init);
420
421
  const chunks = [];
421
- for await (const chunk of req) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
422
+ let size = 0;
423
+ for await (const chunk of req) {
424
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
425
+ size += buffer.length;
426
+ if (size > maxBytes) throw new RequestBodyTooLargeError();
427
+ chunks.push(buffer);
428
+ }
422
429
  const body = Buffer.concat(chunks);
423
430
  if (body.length > 0) init.body = body;
424
431
  return new Request(url, init);
@@ -679,20 +686,21 @@ function applyRsbuildEnvironments(config, opts) {
679
686
  //#endregion
680
687
  //#region src/context.ts
681
688
  const ALS_KEY = Symbol.for("oxidejs.requestContext");
689
+ const FETCH_KEY = Symbol.for("oxidejs.fetch");
682
690
  function als() {
683
691
  const g = globalThis;
684
692
  return g[ALS_KEY] ??= new AsyncLocalStorage();
685
693
  }
686
694
  function store() {
687
695
  const current = als().getStore();
688
- if (!current) throw new Error("oxidejs: useRequest() called outside an action");
696
+ if (!current) throw new Error("oxidejs: request context is unavailable");
689
697
  return current;
690
698
  }
691
- /** Current tacho `ctx`. Throws outside `*.server.ts` running over `/__oxide/action`. */
699
+ /** Current tacho or host request context. Throws outside request handling. */
692
700
  function useCtx() {
693
701
  return store();
694
702
  }
695
- /** Current action `Request`. Throws outside `*.server.ts` running over `/__oxide/action`. */
703
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
696
704
  function useRequest() {
697
705
  return store().req;
698
706
  }
@@ -704,17 +712,27 @@ function useEnv() {
704
712
  function useFetchCtx() {
705
713
  return store().fetchCtx;
706
714
  }
715
+ function runWithRequest(req, fn, extra) {
716
+ return als().run({
717
+ ...extra,
718
+ req
719
+ }, fn);
720
+ }
721
+ const HOOK_KEY = Symbol.for("oxidejs.runWithRequest");
722
+ globalThis[HOOK_KEY] ??= (req, fn) => {
723
+ const extra = req[FETCH_KEY];
724
+ return runWithRequest(req, fn, extra);
725
+ };
707
726
  /**
708
- * Marks a `*.server.ts` export as a remote RPC action. Identity: returns `fn` unchanged.
709
- * Only exports wrapped in `action()` become callable over the wire; other exports stay
710
- * server-local. Wrap async functions and async generators.
727
+ * Marks a `*.server.ts` export as a remote RPC action. Runtime identity; the
728
+ * second call signature adds the transport-only `{ signal }` argument.
711
729
  */
712
730
  function action(fn) {
713
731
  return fn;
714
732
  }
715
733
  //#endregion
716
734
  //#region src/index.ts
717
- function actionMiddleware(loadRouter, path, sameOrigin) {
735
+ function actionMiddleware(loadRouter, path, sameOrigin, bodyLimit) {
718
736
  return (req, res, next) => {
719
737
  if ((req.url ?? "").split("?")[0] !== path) {
720
738
  next();
@@ -728,8 +746,13 @@ function actionMiddleware(loadRouter, path, sameOrigin) {
728
746
  await sendWebResponseFrom(req, res, await handle(await loadRouter(), {
729
747
  path,
730
748
  ...sameOrigin ? { sameOrigin: true } : {}
731
- })(await nodeToWebRequest(req)));
732
- })().catch(next);
749
+ })(await nodeToWebRequest(req, bodyLimit)));
750
+ })().catch((error) => {
751
+ if (error instanceof RequestBodyTooLargeError) {
752
+ res.statusCode = 413;
753
+ res.end();
754
+ } else next(error);
755
+ });
733
756
  };
734
757
  }
735
758
  function attachActionUpgrade(httpServer, loadRouter, path, sameOrigin) {
@@ -847,40 +870,54 @@ const unpluginFactory = (options) => {
847
870
  const loadRouter = async () => {
848
871
  return (await server.ssrLoadModule(VIRTUAL_ACTIONS_ID)).default;
849
872
  };
850
- if ((resolved?.middleware?.length ?? 0) > 0 || (resolved?.imports?.length ?? 0) > 0) (async () => {
873
+ const wireActions = () => server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin, resolved.bodyLimit));
874
+ if (resolved?.actions === "ws") {
875
+ attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
876
+ return;
877
+ }
878
+ if ((resolved?.middleware?.length ?? 0) === 0 && (resolved?.imports?.length ?? 0) === 0) {
879
+ wireActions();
880
+ return;
881
+ }
882
+ (async () => {
851
883
  try {
852
884
  for (const spec of resolved.imports ?? []) await server.ssrLoadModule(spec);
853
885
  const handlers = [];
854
886
  for (const entry of resolved.middleware ?? []) {
855
- const spec2 = typeof entry === "string" ? entry : entry.module;
856
- const mod = await server.ssrLoadModule(spec2);
887
+ const spec = typeof entry === "string" ? entry : entry.module;
888
+ const mod = await server.ssrLoadModule(spec);
857
889
  if (typeof mod.default !== "function") continue;
858
890
  const fn = mod.default;
859
- handlers.push((request) => Promise.resolve(fn(request)));
891
+ handlers.push((request, context) => Promise.resolve(fn(request, context)));
860
892
  }
861
- if (handlers.length === 0) return;
862
- const { nodeToWebRequest, sendWebResponseFrom } = await Promise.resolve().then(() => actions_exports);
863
- server.middlewares.use((creq, cres, next) => {
864
- (async () => {
865
- try {
866
- const request = await nodeToWebRequest(creq);
867
- for (const handler of handlers) {
868
- const hit = await handler(request);
869
- if (hit) return sendWebResponseFrom(creq, cres, hit);
893
+ if (handlers.length > 0) {
894
+ const { nodeToWebRequest, sendWebResponseFrom } = await Promise.resolve().then(() => actions_exports);
895
+ server.middlewares.use((creq, cres, next) => {
896
+ (async () => {
897
+ try {
898
+ const request = await nodeToWebRequest(creq, resolved.bodyLimit);
899
+ const context = {
900
+ env: resolved.env,
901
+ ctx: void 0
902
+ };
903
+ for (const handler of handlers) {
904
+ const hit = await handler(request, context);
905
+ if (hit) return sendWebResponseFrom(creq, cres, hit);
906
+ }
907
+ next();
908
+ } catch (error) {
909
+ cres.statusCode = error instanceof RequestBodyTooLargeError ? 413 : 500;
910
+ cres.end(error instanceof RequestBodyTooLargeError ? void 0 : String(error));
870
911
  }
871
- next();
872
- } catch (error) {
873
- cres.statusCode = 500;
874
- cres.end(String(error));
875
- }
876
- })();
877
- });
912
+ })();
913
+ });
914
+ }
878
915
  } catch (error) {
879
916
  server.config.logger.error("oxidejs: failed to wire dev middleware: " + String(error));
917
+ } finally {
918
+ wireActions();
880
919
  }
881
920
  })();
882
- if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
883
- else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin));
884
921
  },
885
922
  configurePreviewServer(server) {
886
923
  if (resolved?.preset !== "fetch") return;
@@ -897,7 +934,7 @@ const unpluginFactory = (options) => {
897
934
  return (await loadActions(resolved?.root ?? process.cwd())).default;
898
935
  };
899
936
  if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
900
- else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin));
937
+ else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin, resolved.bodyLimit));
901
938
  });
902
939
  api.onBeforeStartPreviewServer?.(({ server }) => {
903
940
  if (resolved?.preset !== "fetch") return;
package/dist/vite.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as oxidejs } from "./src-zvdCGkyp.mjs";
1
+ import { t as oxidejs } from "./src-D-qdNVqg.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.2.0",
3
+ "version": "0.2.2",
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.3"
62
+ "tacho": "^0.6.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@rsbuild/core": "*",