oxidejs 0.1.9 → 0.2.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/README.md CHANGED
@@ -95,7 +95,7 @@ const stream = await ticks(10, { signal: ac.signal });
95
95
  ac.abort();
96
96
  ```
97
97
 
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.
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 and are ignored for WebSocket actions.
99
99
 
100
100
  ## Rsbuild
101
101
 
@@ -113,6 +113,27 @@ 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
+
116
137
  | Option | Default | Notes |
117
138
  | ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
118
139
  | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
@@ -129,6 +150,23 @@ Same factory as Vite: client stubs, `/__oxide/action`, and `dist/server.js`.
129
150
  | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
130
151
  | `actions` | `"http"` | `"ws"` needs `crossws`; object form: `{ transport, path, sameOrigin }` (`sameOrigin: true`) |
131
152
  | `actionHeaders` | — | Static headers on the HTTP client |
153
+ | `middleware` | `[]` | Default-exported production fetch middleware, run in order before actions and server entry |
154
+
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.
156
+
157
+ ```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"] });
167
+ ```
168
+
169
+ This option applies to production builds; use Connect middleware in Vite or Rsbuild during development.
132
170
 
133
171
  `main` is always `./server.js`. `assets` is added only when `index.html` exists. Unknown wrangler keys fail at build time.
134
172
 
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-C3ualQ39.mjs";
1
+ import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-BM4NAnzy.mjs";
2
2
  import { UnpluginFactory } from "unplugin";
3
3
  //#region src/context.d.ts
4
4
  type ExecutionContext = {
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-GLi2EcPa.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-zvdCGkyp.mjs";
2
2
  export { action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
@@ -1,4 +1,4 @@
1
- import { r as OxidejsOptions } from "./types-C3ualQ39.mjs";
1
+ import { r as OxidejsOptions } from "./types-BM4NAnzy.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,5 +1,6 @@
1
- import { t as oxidejs } from "./src-GLi2EcPa.mjs";
1
+ import { t as oxidejs } from "./src-zvdCGkyp.mjs";
2
2
  //#region src/rsbuild.ts
3
+ /** @experimental Rsbuild integration is not yet implemented. */
3
4
  var rsbuild_default = oxidejs.rsbuild;
4
5
  //#endregion
5
6
  export { rsbuild_default as default };
@@ -3,7 +3,41 @@ import path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import fs from "node:fs";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ //#region \0rolldown/runtime.js
7
+ var __defProp = Object.defineProperty;
8
+ var __exportAll = (all, no_symbols) => {
9
+ let target = {};
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
+ return target;
16
+ };
17
+ //#endregion
6
18
  //#region src/actions.ts
19
+ var actions_exports = /* @__PURE__ */ __exportAll({
20
+ ACTION_PATH: () => ACTION_PATH,
21
+ RESOLVED_VIRTUAL_ACTIONS_ID: () => RESOLVED_VIRTUAL_ACTIONS_ID,
22
+ RESOLVED_VIRTUAL_CLIENT_ID: () => RESOLVED_VIRTUAL_CLIENT_ID,
23
+ RESOLVED_VIRTUAL_WORKER_ID: () => RESOLVED_VIRTUAL_WORKER_ID,
24
+ VIRTUAL_ACTIONS_ID: () => VIRTUAL_ACTIONS_ID,
25
+ VIRTUAL_CLIENT_ID: () => VIRTUAL_CLIENT_ID,
26
+ VIRTUAL_WORKER_ID: () => VIRTUAL_WORKER_ID,
27
+ generateActionsModule: () => generateActionsModule,
28
+ generateClientModule: () => generateClientModule,
29
+ generateClientStub: () => generateClientStub,
30
+ generateWorkerWrapper: () => generateWorkerWrapper,
31
+ isServerFileId: () => isServerFileId,
32
+ loadClientStub: () => loadClientStub,
33
+ moduleKey: () => moduleKey,
34
+ nodeToWebRequest: () => nodeToWebRequest,
35
+ parseExportedNames: () => parseExportedNames,
36
+ pluginShouldStub: () => pluginShouldStub,
37
+ scanServerFiles: () => scanServerFiles,
38
+ sendWebResponseFrom: () => sendWebResponseFrom,
39
+ shouldStubServerModule: () => shouldStubServerModule
40
+ });
7
41
  const VIRTUAL_ACTIONS_ID = "virtual:oxide/actions";
8
42
  const RESOLVED_VIRTUAL_ACTIONS_ID = `\0${VIRTUAL_ACTIONS_ID}`;
9
43
  const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
@@ -175,6 +209,8 @@ function generateWorkerWrapper(userWorkerAbs, opts = {}) {
175
209
  const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
176
210
  const hasActions = opts.hasActions !== false;
177
211
  const ws = hasActions && opts.actions === "ws";
212
+ 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" } })`;
178
214
  const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
179
215
  import { extname, join } from "node:path";
180
216
  const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
@@ -207,24 +243,32 @@ async function __asset(request, spa) {
207
243
  const headers = { "content-type": __types[extname(file)] ?? "application/octet-stream" };
208
244
  const cache = __cache(file);
209
245
  if (cache) headers["cache-control"] = cache;
246
+ const etag = '"' + body.length.toString(16) + "-" + file + '"';
247
+ headers["etag"] = etag;
248
+ if (request.headers.get("if-none-match") === etag) {
249
+ return new Response(null, { status: 304, headers });
250
+ }
210
251
  return new Response(body, { headers });
211
252
  } catch {
212
253
  return;
213
254
  }
214
255
  }
256
+ const __nf = ${__nf};
215
257
  ` : "";
258
+ const envJson = JSON.stringify(opts.env ?? {});
216
259
  const afterAction = serveAssets ? `if (typeof user.fetch === "function") {
217
- const hit = await user.fetch(request, env, ctx);
260
+ const hit = await user.fetch(request, env ?? ${envJson}, ctx);
218
261
  if (hit) return hit;
219
262
  }
220
- return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? new Response("Not Found", { status: 404 });` : `return typeof user.fetch === "function"
263
+ return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? __nf();` : `return typeof user.fetch === "function"
221
264
  ? user.fetch(request, env, ctx)
222
- : new Response("Not Found", { status: 404 });`;
265
+ : __nf();`;
223
266
  const listen = preset === "fetch" ? `
224
267
  import { createServer } from "node:http";
225
268
  import { pathToFileURL } from "node:url";
226
269
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
227
270
  const port = Number(process.env.PORT) || 3000;
271
+ const bodyLimit = ${bodyLimit};
228
272
  const server = createServer(async (req, res) => {
229
273
  const url = \`http://\${req.headers.host ?? "localhost"}\${req.url ?? "/"}\`;
230
274
  const headers = new Headers();
@@ -237,7 +281,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
237
281
  req.once("aborted", () => ac.abort());
238
282
  const method = req.method ?? "GET";
239
283
  const chunks = [];
240
- if (method !== "GET" && method !== "HEAD") for await (const chunk of req) chunks.push(chunk);
284
+ let size = 0;
285
+ if (method !== "GET" && method !== "HEAD") for await (const chunk of req) {
286
+ size += chunk.length;
287
+ // Bound request buffering — unbounded bodies are a memory DoS vector.
288
+ if (size > ${bodyLimit}) { res.statusCode = 413; res.end(); return; }
289
+ chunks.push(chunk);
290
+ }
241
291
  const init = { method, headers, signal: ac.signal };
242
292
  if (chunks.length) init.body = Buffer.concat(chunks);
243
293
  const response = await app.fetch(new Request(url, init));
@@ -260,6 +310,12 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
260
310
  });` : ""}
261
311
  server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
262
312
  }
313
+ for (const signal of ["SIGTERM", "SIGINT"]) {
314
+ process.on(signal, () => {
315
+ server.close(() => process.exit(0));
316
+ setTimeout(() => process.exit(0), 5000).unref();
317
+ });
318
+ }
263
319
  ` : "";
264
320
  const actionImports = hasActions ? ws ? `import { handle as handleWs } from "tacho/transport/ws";
265
321
  import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
@@ -274,12 +330,23 @@ const __rpc = handle(actions, { path: ${JSON.stringify(actionPath)}${sameOrigin
274
330
  return __rpc(request);
275
331
  }
276
332
  ` : "";
277
- return `export * from ${JSON.stringify(userWorkerAbs)};
333
+ const ILHA_SSR_IMPLICIT = ["ilha:pages/server", "ilha:loaders"];
334
+ const middlewareImports = (opts.middleware ?? []).map((m) => typeof m === "string" ? {
335
+ module: m,
336
+ imports: m === "@ilha/router/ssr" ? ILHA_SSR_IMPLICIT : []
337
+ } : m).map((m, i) => (m.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n") + `\nimport __mw${i} from ${JSON.stringify(m.module)};`).join("\n") + "\n";
338
+ const middlewareList = (opts.middleware ?? []).map((_, i) => `__mw${i}`).join(", ");
339
+ const middlewareGate = opts.middleware?.length ? `for (const __mw of [${middlewareList}]) {
340
+ const hit = await __mw(request, { env, ctx });
341
+ if (hit) return hit;
342
+ }
343
+ ` : "";
344
+ return `${(opts.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}export * from ${JSON.stringify(userWorkerAbs)};
278
345
  import user from ${JSON.stringify(userWorkerAbs)};
279
- ${actionImports}${assetBlock}const app = {
346
+ ${middlewareImports}${actionImports}${assetBlock}const app = {
280
347
  ...user,
281
348
  async fetch(request, env, ctx) {
282
- ${actionGate}${afterAction}
349
+ ${middlewareGate}${actionGate}${afterAction}
283
350
  },
284
351
  };
285
352
  export default app;
@@ -462,7 +529,12 @@ function resolveOptions(raw, root, config) {
462
529
  actions,
463
530
  actionPath,
464
531
  actionSameOrigin,
465
- actionHeaders: raw?.actionHeaders
532
+ actionHeaders: raw?.actionHeaders,
533
+ middleware: raw?.middleware ?? [],
534
+ imports: raw?.imports ?? [],
535
+ bodyLimit: raw?.bodyLimit ?? 1048576,
536
+ notFound: raw?.notFound,
537
+ env: raw?.env
466
538
  };
467
539
  }
468
540
  function copyPublicDir(opts) {
@@ -736,7 +808,12 @@ const unpluginFactory = (options) => {
736
808
  hasActions: modules.length > 0,
737
809
  actions: resolved.actions,
738
810
  actionPath: resolved.actionPath,
739
- actionSameOrigin: resolved.actionSameOrigin
811
+ actionSameOrigin: resolved.actionSameOrigin,
812
+ middleware: resolved.middleware,
813
+ imports: resolved.imports,
814
+ bodyLimit: resolved.bodyLimit,
815
+ notFound: resolved.notFound,
816
+ env: resolved.env
740
817
  });
741
818
  }
742
819
  if (isServerFileId(id) && pluginShouldStub(this, extra)) {
@@ -770,6 +847,38 @@ const unpluginFactory = (options) => {
770
847
  const loadRouter = async () => {
771
848
  return (await server.ssrLoadModule(VIRTUAL_ACTIONS_ID)).default;
772
849
  };
850
+ if ((resolved?.middleware?.length ?? 0) > 0 || (resolved?.imports?.length ?? 0) > 0) (async () => {
851
+ try {
852
+ for (const spec of resolved.imports ?? []) await server.ssrLoadModule(spec);
853
+ const handlers = [];
854
+ for (const entry of resolved.middleware ?? []) {
855
+ const spec2 = typeof entry === "string" ? entry : entry.module;
856
+ const mod = await server.ssrLoadModule(spec2);
857
+ if (typeof mod.default !== "function") continue;
858
+ const fn = mod.default;
859
+ handlers.push((request) => Promise.resolve(fn(request)));
860
+ }
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);
870
+ }
871
+ next();
872
+ } catch (error) {
873
+ cres.statusCode = 500;
874
+ cres.end(String(error));
875
+ }
876
+ })();
877
+ });
878
+ } catch (error) {
879
+ server.config.logger.error("oxidejs: failed to wire dev middleware: " + String(error));
880
+ }
881
+ })();
773
882
  if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter, resolved.actionPath, resolved.actionSameOrigin);
774
883
  else server.middlewares.use(actionMiddleware(loadRouter, resolved.actionPath, resolved.actionSameOrigin));
775
884
  },
@@ -37,6 +37,25 @@ interface OxidejsOptions {
37
37
  actions?: OxidejsActions;
38
38
  /** Extra headers on the shared HTTP action client. Ignored when `actions` is "ws". */
39
39
  actionHeaders?: OxidejsActionHeaders;
40
+ /** Module specifiers whose default export is `(request, ctx) => Response | undefined | Promise<Response | undefined>`.
41
+ * Tried in order at the top of the production fetch handler; a Response short-circuits.
42
+ * Dev servers use connect middleware instead. */
43
+ middleware?: (string | {
44
+ module: string;
45
+ imports?: string[];
46
+ })[];
47
+ /** Module specifiers imported for side effects at the top of the production
48
+ * server bundle (e.g. virtual modules that self-register handlers). */
49
+ imports?: string[];
50
+ /** Max request body size in bytes (Node preset). Larger requests get 413.
51
+ * Default: 1048576 (1 MiB). */
52
+ bodyLimit?: number;
53
+ /** Custom 404 body (HTML) served when no route, asset, or user fetch
54
+ * handled the request (fetch preset with client assets). */
55
+ notFound?: string;
56
+ /** Extra env passed as the second argument to fetch(request, env, ctx) on
57
+ * the Node fetch preset — read it with useEnv(). */
58
+ env?: Record<string, unknown>;
40
59
  }
41
60
  interface ResolvedOptions {
42
61
  root: string;
@@ -59,6 +78,14 @@ interface ResolvedOptions {
59
78
  /** Reject cross-origin action requests (CSRF defense). Default: true. */
60
79
  actionSameOrigin: boolean;
61
80
  actionHeaders: OxidejsActionHeaders | undefined;
81
+ middleware: (string | {
82
+ module: string;
83
+ imports?: string[];
84
+ })[];
85
+ imports: string[];
86
+ bodyLimit: number;
87
+ notFound: string | undefined;
88
+ env: Record<string, unknown> | undefined;
62
89
  }
63
90
  //#endregion
64
91
  export { OxidejsWranglerOptions as a, OxidejsPreset as i, OxidejsActionTransport as n, ResolvedOptions as o, OxidejsOptions as r, OxidejsActionHeaders as t };
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as OxidejsOptions } from "./types-C3ualQ39.mjs";
1
+ import { r as OxidejsOptions } from "./types-BM4NAnzy.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-GLi2EcPa.mjs";
1
+ import { t as oxidejs } from "./src-zvdCGkyp.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.9",
3
+ "version": "0.2.0",
4
4
  "description": "Vite/Rsbuild plugin. One build → dist/server.js + optional client. Server actions via *.server.ts.",
5
5
  "keywords": [
6
6
  "cloudflare",