oxidejs 0.2.4 → 0.3.1

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.
@@ -0,0 +1,915 @@
1
+ import { t as runWithRequest } from "./context-zrTZyYpF.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Layer } from "effect";
5
+ import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
6
+ import { HttpRouter } from "effect/unstable/http";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/actions.ts
20
+ var actions_exports = /* @__PURE__ */ __exportAll({
21
+ ACTION_PATH: () => ACTION_PATH,
22
+ RESOLVED_VIRTUAL_ACTIONS_ID: () => RESOLVED_VIRTUAL_ACTIONS_ID,
23
+ RESOLVED_VIRTUAL_CLIENT_ID: () => RESOLVED_VIRTUAL_CLIENT_ID,
24
+ RESOLVED_VIRTUAL_WORKER_ID: () => RESOLVED_VIRTUAL_WORKER_ID,
25
+ RequestBodyTooLargeError: () => RequestBodyTooLargeError,
26
+ VIRTUAL_ACTIONS_ID: () => VIRTUAL_ACTIONS_ID,
27
+ VIRTUAL_CLIENT_ID: () => VIRTUAL_CLIENT_ID,
28
+ VIRTUAL_WORKER_ID: () => VIRTUAL_WORKER_ID,
29
+ generateActionsClientModule: () => generateActionsClientModule,
30
+ generateActionsModule: () => generateActionsModule,
31
+ generateClientModule: () => generateClientModule,
32
+ generateClientStub: () => generateClientStub,
33
+ generateWorkerWrapper: () => generateWorkerWrapper,
34
+ isServerFileId: () => isServerFileId,
35
+ loadClientStub: () => loadClientStub,
36
+ matchesActionPath: () => matchesActionPath,
37
+ moduleKey: () => moduleKey,
38
+ nodeToWebRequest: () => nodeToWebRequest,
39
+ parseExportedNames: () => parseExportedNames,
40
+ parseStreamExports: () => parseStreamExports,
41
+ pluginShouldStub: () => pluginShouldStub,
42
+ scanServerFiles: () => scanServerFiles,
43
+ sendWebResponseFrom: () => sendWebResponseFrom,
44
+ shouldStubServerModule: () => shouldStubServerModule
45
+ });
46
+ const VIRTUAL_ACTIONS_ID = "virtual:oxide/actions";
47
+ const RESOLVED_VIRTUAL_ACTIONS_ID = `\0${VIRTUAL_ACTIONS_ID}`;
48
+ const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
49
+ const RESOLVED_VIRTUAL_WORKER_ID = `\0${VIRTUAL_WORKER_ID}`;
50
+ const VIRTUAL_CLIENT_ID = "virtual:oxide/client";
51
+ const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
52
+ const ACTION_PATH = "/__oxide/action";
53
+ /** Match the action endpoint with or without a trailing slash (Effect RPC posts to `path/`). */
54
+ function matchesActionPath(pathname, path = ACTION_PATH) {
55
+ return pathname === path || pathname === `${path}/`;
56
+ }
57
+ const IGNORE_DIRS = /* @__PURE__ */ new Set([
58
+ "node_modules",
59
+ "dist",
60
+ ".git",
61
+ ".wrangler"
62
+ ]);
63
+ /** Only `export const name = action(...)` become remote RPC actions. Everything else stays server-local. */
64
+ const EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?action\s*\(/gm;
65
+ const STREAM_EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*action\s*\(\s*async\s+function\s*\*/gm;
66
+ function isServerFileId(id) {
67
+ const file = id.split("?")[0]?.replace(/\\/g, "/") ?? "";
68
+ return [
69
+ ".ts",
70
+ ".tsx",
71
+ ".js",
72
+ ".jsx"
73
+ ].some((ext) => file.endsWith(`.server${ext}`));
74
+ }
75
+ function moduleKey(absFile) {
76
+ return path.basename(absFile).replace(/\.server\.(?:[jt]sx?)$/i, "");
77
+ }
78
+ function parseExportedNames(source) {
79
+ const names = /* @__PURE__ */ new Set();
80
+ for (const match of source.matchAll(EXPORT_RE)) {
81
+ const name = match[1];
82
+ if (name) names.add(name);
83
+ }
84
+ return [...names];
85
+ }
86
+ function parseStreamExports(source) {
87
+ const names = /* @__PURE__ */ new Set();
88
+ for (const match of source.matchAll(STREAM_EXPORT_RE)) {
89
+ const name = match[1];
90
+ if (name) names.add(name);
91
+ }
92
+ return [...names];
93
+ }
94
+ function scanServerFiles(root) {
95
+ const files = [];
96
+ const walk = (dir) => {
97
+ let entries;
98
+ try {
99
+ entries = fs.readdirSync(dir, { withFileTypes: true });
100
+ } catch {
101
+ return;
102
+ }
103
+ for (const entry of entries) {
104
+ if (entry.name.startsWith(".")) continue;
105
+ const abs = path.join(dir, entry.name);
106
+ if (entry.isDirectory()) {
107
+ if (IGNORE_DIRS.has(entry.name)) continue;
108
+ walk(abs);
109
+ continue;
110
+ }
111
+ if (entry.isFile() && isServerFileId(entry.name)) files.push(abs);
112
+ }
113
+ };
114
+ walk(root);
115
+ const byKey = /* @__PURE__ */ new Map();
116
+ const modules = [];
117
+ for (const abs of files.sort()) {
118
+ const key = moduleKey(abs);
119
+ if (!key) throw new Error(`oxidejs: invalid server module name: ${abs}`);
120
+ const existing = byKey.get(key);
121
+ if (existing) throw new Error(`oxidejs: duplicate server module key "${key}": ${existing} and ${abs}`);
122
+ byKey.set(key, abs);
123
+ const source = fs.readFileSync(abs, "utf8");
124
+ modules.push({
125
+ abs,
126
+ key,
127
+ exports: parseExportedNames(source),
128
+ streams: parseStreamExports(source)
129
+ });
130
+ }
131
+ return modules;
132
+ }
133
+ function generateClientModule(transport = "http", headers, path = ACTION_PATH) {
134
+ const opts = { url: path };
135
+ if (transport === "ws") opts.transport = "ws";
136
+ if (headers) opts.headers = headers;
137
+ if (transport === "ws") return `import { createClient } from "oxidejs/rpc/client";
138
+ import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
139
+ const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
140
+ const __host = typeof location === "undefined" ? "localhost" : location.host;
141
+ export const client = createClient(actionsGroup, { ...${JSON.stringify(opts)}, url: __proto + "//" + __host + ${JSON.stringify(path)} });
142
+ `;
143
+ return `import { createClient } from "oxidejs/rpc/client";
144
+ import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
145
+ export const client = createClient(actionsGroup, ${JSON.stringify(opts)});
146
+ `;
147
+ }
148
+ function generateClientStub(mod) {
149
+ const streams = new Set(mod.streams ?? []);
150
+ const lines = [
151
+ `// oxidejs:client-stub`,
152
+ `import { wrapClientRpc, wrapClientStreamRpc } from "oxidejs";`,
153
+ `import { client } from ${JSON.stringify(VIRTUAL_CLIENT_ID)};`
154
+ ];
155
+ for (const name of mod.exports) {
156
+ const call = `client[${JSON.stringify(mod.key)}][${JSON.stringify(name)}]`;
157
+ const peel = `(...args) => {
158
+ const opts = args.at(-1);
159
+ // ponytail: peel last { signal } only. A lone payload { signal: AbortSignal } is treated as CallOptions.
160
+ return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1
161
+ ? ${call}(...args.slice(0, -1), opts)
162
+ : ${call}(...args);
163
+ }`;
164
+ if (streams.has(name)) lines.push(`export const ${name} = wrapClientStreamRpc(${peel});`);
165
+ else lines.push(`export const ${name} = wrapClientRpc(${peel});`);
166
+ }
167
+ return `${lines.join("\n")}\n`;
168
+ }
169
+ function generateActionsClientModule(modules) {
170
+ const lines = [`import { Schema } from "effect";`, `import { Rpc, RpcGroup } from "effect/unstable/rpc";`];
171
+ const rpcNames = [];
172
+ modules.forEach((mod, i) => {
173
+ for (const name of mod.exports) {
174
+ const rpc = `__rpc_${i}_${name}`;
175
+ rpcNames.push(rpc);
176
+ const tag = `${mod.key}.${name}`;
177
+ const stream = mod.streams?.includes(name) ?? false;
178
+ lines.push(`const ${rpc} = Rpc.make(${JSON.stringify(tag)}, { payload: Schema.Struct({ args: Schema.Array(Schema.Unknown) }), success: Schema.Unknown${stream ? ", stream: true" : ""} });`);
179
+ }
180
+ });
181
+ lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`);
182
+ lines.push(`export default actionsGroup;`);
183
+ lines.push(`export { actionsGroup as actions };`);
184
+ return `${lines.join("\n")}\n`;
185
+ }
186
+ function generateActionsModule(modules, opts) {
187
+ const lines = [
188
+ `import { Effect } from "effect";`,
189
+ `import { Schema } from "effect";`,
190
+ `import { Rpc, RpcGroup } from "effect/unstable/rpc";`,
191
+ `import { AsyncLocalStorage } from "node:async_hooks";`,
192
+ `import { asyncGenToStreamInContext } from "oxidejs/rpc";`,
193
+ `const __alsKey = Symbol.for("oxidejs.requestContext");`,
194
+ `const __als = globalThis[__alsKey] ??= new AsyncLocalStorage();`,
195
+ `const __store = () => {`,
196
+ ` const ctx = __als.getStore();`,
197
+ ` if (!ctx) throw new Error("oxidejs: request context is unavailable");`,
198
+ ` return ctx;`,
199
+ `};`,
200
+ `const __run = (fn) =>`,
201
+ ` Effect.promise(() => __als.run(__store(), fn)).pipe(`,
202
+ ` Effect.map((value) => {`,
203
+ ` if (value instanceof Response) {`,
204
+ ` console.error("oxidejs: action() returned a Response; actions must return serializable data. Return a Response from src/server.ts for raw HTTP responses.");`,
205
+ ` throw new Error("action() returned a Response; return it from src/server.ts instead");`,
206
+ ` }`,
207
+ ` return value === undefined ? null : value;`,
208
+ ` }),`,
209
+ ` );`,
210
+ `const __withStore = (store, fn) => __als.run(store, fn);`
211
+ ];
212
+ const rpcNames = [];
213
+ const aliases = modules.map((mod, i) => {
214
+ const alias = `__m${i}`;
215
+ const spec = opts?.bust === true ? `${mod.abs}?t=${fs.statSync(mod.abs).mtimeMs}` : mod.abs;
216
+ lines.push(`import * as ${alias} from ${JSON.stringify(spec)};`);
217
+ for (const name of mod.exports) {
218
+ const rpc = `__rpc_${i}_${name}`;
219
+ rpcNames.push(rpc);
220
+ const tag = `${mod.key}.${name}`;
221
+ const stream = mod.streams?.includes(name) ?? false;
222
+ lines.push(`const ${rpc} = Rpc.make(${JSON.stringify(tag)}, { payload: Schema.Struct({ args: Schema.Array(Schema.Unknown) }), success: Schema.Unknown${stream ? ", stream: true" : ""} });`);
223
+ }
224
+ return {
225
+ alias,
226
+ mod
227
+ };
228
+ });
229
+ lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`);
230
+ lines.push(`export const actionsHandlers = actionsGroup.toLayer({`);
231
+ for (const { alias, mod } of aliases) for (const name of mod.exports) {
232
+ const tag = `${mod.key}.${name}`;
233
+ const stream = mod.streams?.includes(name) ?? false;
234
+ lines.push(stream ? ` ${JSON.stringify(tag)}: ({ args }) => { const __s = __store(); return asyncGenToStreamInContext(() => ${alias}[${JSON.stringify(name)}].apply(null, args), (fn) => __withStore(__s, fn)); },` : ` ${JSON.stringify(tag)}: ({ args }) => __run(() => ${alias}[${JSON.stringify(name)}].apply(null, args)),`);
235
+ }
236
+ lines.push(`});`);
237
+ lines.push(`export default actionsGroup;`);
238
+ lines.push(`export { actionsGroup as actions };`);
239
+ return `${lines.join("\n")}\n`;
240
+ }
241
+ function pipeResponse(req, res, response) {
242
+ return new Promise((resolve, reject) => {
243
+ res.statusCode = response.status;
244
+ response.headers.forEach((value, key) => {
245
+ res.setHeader(key, value);
246
+ });
247
+ if (!response.body) {
248
+ res.end();
249
+ resolve();
250
+ return;
251
+ }
252
+ const reader = response.body.getReader();
253
+ const abort = () => {
254
+ reader.cancel();
255
+ };
256
+ req.once("aborted", abort);
257
+ const pull = () => {
258
+ reader.read().then(({ done, value }) => {
259
+ if (done) {
260
+ req.off("aborted", abort);
261
+ res.end();
262
+ resolve();
263
+ return;
264
+ }
265
+ if (value) res.write(value);
266
+ pull();
267
+ }, reject);
268
+ };
269
+ pull();
270
+ });
271
+ }
272
+ function generateWorkerWrapper(userWorkerAbs, opts = {}) {
273
+ const preset = opts.preset ?? "fetch";
274
+ const clientDir = opts.clientDir ?? "client";
275
+ const actionPath = opts.actionPath ?? "/__oxide/action";
276
+ const sameOrigin = opts.actionSameOrigin ?? false;
277
+ const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
278
+ const hasActions = opts.hasActions !== false;
279
+ const ws = hasActions && opts.actions === "ws";
280
+ const bodyLimit = opts.bodyLimit ?? 1048576;
281
+ 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`;
282
+ const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
283
+ import { extname, join } from "node:path";
284
+ const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
285
+ const __types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".webp": "image/webp" };
286
+ function __nav(request) {
287
+ const dest = request.headers.get("sec-fetch-dest");
288
+ if (dest) return dest === "document";
289
+ return (request.headers.get("accept") ?? "").includes("text/html");
290
+ }
291
+ function __cache(file) {
292
+ if (file === "index.html") return "no-cache";
293
+ return /[-.][0-9a-f]{8,}.[a-z0-9]+$/i.test(file) ? "public, max-age=31536000, immutable" : undefined;
294
+ }
295
+ function __rel(pathname, spa) {
296
+ if (pathname.includes("\0")) return;
297
+ let file;
298
+ try { file = decodeURIComponent(pathname); } catch { return; }
299
+ if (file.includes("\0")) return;
300
+ if (file === "/" || spa) file = "/index.html";
301
+ if (!file.startsWith("/") || file.split("/").includes("..")) return;
302
+ const rel = file.slice(1);
303
+ if (rel.startsWith("/")) return;
304
+ return rel;
305
+ }
306
+ async function __asset(request, spa) {
307
+ const file = __rel(new URL(request.url).pathname, spa);
308
+ if (!file) return;
309
+ try {
310
+ const body = await readFile(join(__assets, file));
311
+ const headers = { "content-type": __types[extname(file)] ?? "application/octet-stream" };
312
+ const cache = __cache(file);
313
+ if (cache) headers["cache-control"] = cache;
314
+ const etag = '"' + body.length.toString(16) + "-" + file + '"';
315
+ headers["etag"] = etag;
316
+ if (request.headers.get("if-none-match") === etag) {
317
+ return new Response(null, { status: 304, headers });
318
+ }
319
+ return new Response(body, { headers });
320
+ } catch {
321
+ return;
322
+ }
323
+ }
324
+ ` : "";
325
+ const envJson = JSON.stringify(opts.env ?? {});
326
+ const celldAfterAction = `{
327
+ const hit = typeof user.fetch === "function" ? await user.fetch(request, env ?? ${envJson}, ctx) : undefined;
328
+ if (hit) return hit;
329
+ const assets = env?.ASSETS;
330
+ if (assets && typeof assets.fetch === "function") return assets.fetch(request);
331
+ return __nf();
332
+ }`;
333
+ const afterAction = serveAssets ? `if (typeof user.fetch === "function") {
334
+ const hit = await user.fetch(request, env ?? ${envJson}, ctx);
335
+ if (hit) return hit;
336
+ }
337
+ return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? __nf();` : preset === "celld" ? celldAfterAction : `return typeof user.fetch === "function"
338
+ ? user.fetch(request, env, ctx)
339
+ : __nf();`;
340
+ const listen = preset === "fetch" ? `
341
+ import { createServer } from "node:http";
342
+ import { pathToFileURL } from "node:url";
343
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
344
+ const port = Number(process.env.PORT) || 3000;
345
+ const bodyLimit = ${bodyLimit};
346
+ const server = createServer(async (req, res) => {
347
+ const url = \`http://\${req.headers.host ?? "localhost"}\${req.url ?? "/"}\`;
348
+ const headers = new Headers();
349
+ for (const [key, value] of Object.entries(req.headers)) {
350
+ if (value === undefined) continue;
351
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item);
352
+ else headers.set(key, value);
353
+ }
354
+ const ac = new AbortController();
355
+ req.once("aborted", () => ac.abort());
356
+ const method = req.method ?? "GET";
357
+ const chunks = [];
358
+ let size = 0;
359
+ if (method !== "GET" && method !== "HEAD") for await (const chunk of req) {
360
+ size += chunk.length;
361
+ // Bound request buffering — unbounded bodies are a memory DoS vector.
362
+ if (size > ${bodyLimit}) { res.statusCode = 413; res.end(); return; }
363
+ chunks.push(chunk);
364
+ }
365
+ const init = { method, headers, signal: ac.signal };
366
+ if (chunks.length) init.body = Buffer.concat(chunks);
367
+ const response = await app.fetch(new Request(url, init));
368
+ res.statusCode = response.status;
369
+ response.headers.forEach((value, key) => res.setHeader(key, value));
370
+ if (!response.body) { res.end(); return; }
371
+ const reader = response.body.getReader();
372
+ for (;;) {
373
+ const { done, value } = await reader.read();
374
+ if (done) break;
375
+ if (value) res.write(value);
376
+ }
377
+ res.end();
378
+ });${ws ? `
379
+ import("crossws/adapters/node").then(({ default: crossws }) => {
380
+ const ws = crossws({ hooks: __ws });
381
+ server.on("upgrade", (req, socket, head) => {
382
+ if ((__actionMatch)(req.url?.split("?")[0] ?? "")) ws.handleUpgrade(req, socket, head);
383
+ });
384
+ });` : ""}
385
+ server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
386
+ }
387
+ for (const signal of ["SIGTERM", "SIGINT"]) {
388
+ process.on(signal, () => {
389
+ server.close(() => process.exit(0));
390
+ setTimeout(() => process.exit(0), 5000).unref();
391
+ });
392
+ }
393
+ ` : "";
394
+ const actionImports = hasActions ? ws ? `import { createWsHooks } from "oxidejs/rpc";
395
+ import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
396
+ const __ws = createWsHooks(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin} });
397
+ ` : `import { createActionHandler } from "oxidejs/rpc";
398
+ import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
399
+ const __fetch = Symbol.for("oxidejs.fetch");
400
+ const __rpc = createActionHandler(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin}, createContext: (req) => req[__fetch] ?? {} });
401
+ ` : "";
402
+ const actionMatchFn = `const __actionMatch = (p) => p === ${JSON.stringify(actionPath)} || p === ${JSON.stringify(`${actionPath}/`)};`;
403
+ const actionGate = hasActions && !ws ? `if ((__actionMatch)(new URL(request.url).pathname)) {
404
+ return __rpc(request);
405
+ }
406
+ ` : "";
407
+ const ILHA_SSR_IMPLICIT = ["ilha:pages/server", "ilha:loaders"];
408
+ const middlewareImports = (opts.middleware ?? []).map((m) => typeof m === "string" ? {
409
+ module: m,
410
+ imports: m === "@ilha/router/ssr" ? ILHA_SSR_IMPLICIT : []
411
+ } : 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";
412
+ const middlewareList = (opts.middleware ?? []).map((_, i) => `__mw${i}`).join(", ");
413
+ const middlewareGate = opts.middleware?.length ? `for (const __mw of [${middlewareList}]) {
414
+ const hit = await __mw(request, { env, ctx });
415
+ if (hit) return hit;
416
+ }
417
+ ` : "";
418
+ return `${(opts.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}${preset === "celld" ? `import "oxidejs/worker-dom/install";\n` : ""}export * from ${JSON.stringify(userWorkerAbs)};
419
+ import user from ${JSON.stringify(userWorkerAbs)};
420
+ ${middlewareImports}${actionImports}${hasActions ? `${actionMatchFn}\n` : ""}${assetBlock}${nfBlock}const app = {
421
+ ...user,
422
+ async fetch(request, env, ctx) {
423
+ request[__fetch] = { env, fetchCtx: ctx };
424
+ ${middlewareGate}${actionGate}${afterAction}
425
+ },
426
+ };
427
+ export default app;
428
+ ${listen}`;
429
+ }
430
+ const SERVER_TARGETS = /* @__PURE__ */ new Set([
431
+ "node",
432
+ "async-node",
433
+ "webworker",
434
+ "web-worker"
435
+ ]);
436
+ const CLIENT_TARGETS = /* @__PURE__ */ new Set(["web", "browserslist"]);
437
+ const SERVER_NAMES = /* @__PURE__ */ new Set([
438
+ "server",
439
+ "ssr",
440
+ "worker",
441
+ "node"
442
+ ]);
443
+ const CLIENT_NAMES = /* @__PURE__ */ new Set(["client", "web"]);
444
+ /** Stub unless the graph is a known server. Unknown graphs stub so *.server.ts never ships. */
445
+ function shouldStubServerModule(environment, extra) {
446
+ if (extra?.ssr) return false;
447
+ const consumer = environment?.config?.consumer ?? environment?.consumer;
448
+ if (consumer === "server") return false;
449
+ if (consumer === "client") return true;
450
+ const name = environment?.name;
451
+ if (name && CLIENT_NAMES.has(name)) return true;
452
+ if (name && SERVER_NAMES.has(name)) return false;
453
+ const targets = extra?.target == null ? [] : [extra.target].flat();
454
+ if (targets.some((target) => SERVER_TARGETS.has(target))) return false;
455
+ if (targets.some((target) => CLIENT_TARGETS.has(target))) return true;
456
+ return true;
457
+ }
458
+ function pluginShouldStub(pluginThis, options) {
459
+ const ctx = pluginThis;
460
+ const compiler = ctx.getNativeBuildContext?.()?.compiler;
461
+ const env = {};
462
+ const name = ctx.environment?.name ?? compiler?.name ?? compiler?.options?.name;
463
+ if (name) env.name = name;
464
+ if (ctx.environment?.consumer) env.consumer = ctx.environment.consumer;
465
+ if (ctx.environment?.config) env.config = ctx.environment.config;
466
+ const extra = {};
467
+ if (options?.ssr) extra.ssr = true;
468
+ if (compiler?.options?.target) extra.target = compiler.options.target;
469
+ return shouldStubServerModule(env, extra);
470
+ }
471
+ function loadClientStub(id) {
472
+ const file = id.split("?")[0] ?? id;
473
+ const source = fs.readFileSync(file, "utf8");
474
+ return generateClientStub({
475
+ key: moduleKey(file),
476
+ exports: parseExportedNames(source),
477
+ streams: parseStreamExports(source)
478
+ });
479
+ }
480
+ var RequestBodyTooLargeError = class extends Error {};
481
+ async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
482
+ const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
483
+ const headers = new Headers();
484
+ for (const [key, value] of Object.entries(req.headers)) {
485
+ if (value === void 0) continue;
486
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item);
487
+ else headers.set(key, value);
488
+ }
489
+ const ac = new AbortController();
490
+ req.once("aborted", () => ac.abort());
491
+ const method = req.method ?? "GET";
492
+ const init = {
493
+ method,
494
+ headers,
495
+ signal: ac.signal
496
+ };
497
+ if (method === "GET" || method === "HEAD") return new Request(url, init);
498
+ const chunks = [];
499
+ let size = 0;
500
+ for await (const chunk of req) {
501
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
502
+ size += buffer.length;
503
+ if (size > maxBytes) throw new RequestBodyTooLargeError();
504
+ chunks.push(buffer);
505
+ }
506
+ const body = Buffer.concat(chunks);
507
+ if (body.length > 0) init.body = body;
508
+ return new Request(url, init);
509
+ }
510
+ async function sendWebResponseFrom(req, res, response) {
511
+ return pipeResponse(req, res, response);
512
+ }
513
+ //#endregion
514
+ //#region src/rpc/same-origin.ts
515
+ function isSameOrigin(request) {
516
+ const site = request.headers.get("sec-fetch-site");
517
+ const origin = request.headers.get("origin");
518
+ if (!origin && !site) return false;
519
+ if (site && site !== "same-origin" && site !== "none") return false;
520
+ if (!origin) return site === "same-origin" || site === "none";
521
+ try {
522
+ return new URL(origin).host === (request.headers.get("host") ?? new URL(request.url).host);
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
527
+ //#endregion
528
+ //#region src/rpc/scrub.ts
529
+ /** Strip Effect RPC `Defect` / `Cause` payloads down to plain JSON-RPC errors. */
530
+ const INTERNAL = {
531
+ code: -32603,
532
+ message: "Internal error"
533
+ };
534
+ const NDJSON_CONTENT = "application/json-rpc";
535
+ function isRecord(value) {
536
+ return value !== null && typeof value === "object";
537
+ }
538
+ function classifyCause(error) {
539
+ const blob = `${String(error["message"] ?? "")}${JSON.stringify(error["data"] ?? "")}`;
540
+ if (/Unknown request tag/i.test(blob)) return {
541
+ code: -32601,
542
+ message: "Method not found"
543
+ };
544
+ if (/Missing key/i.test(blob) || /Expected/i.test(blob) && /\["args"\]|\[\\"args\\"\]/.test(blob)) return {
545
+ code: -32602,
546
+ message: "Invalid params"
547
+ };
548
+ return { ...INTERNAL };
549
+ }
550
+ function scrubError(error) {
551
+ if (!isRecord(error)) return { ...INTERNAL };
552
+ if (error["_tag"] === "Defect") return { ...INTERNAL };
553
+ if (error["_tag"] === "Cause") return classifyCause(error);
554
+ if (typeof error["code"] === "number" && typeof error["message"] === "string") return {
555
+ code: error["code"],
556
+ message: error["message"]
557
+ };
558
+ return { ...INTERNAL };
559
+ }
560
+ function createIdRepairState(requestIds = []) {
561
+ return { remaining: new Set(requestIds) };
562
+ }
563
+ /**
564
+ * Scrub one JSON-RPC response object.
565
+ * Effect encodes Defects with `id: -32603`; reclaim the originating request id from `state.remaining`.
566
+ */
567
+ function scrubRpcMessage(msg, requestIds = [], state = createIdRepairState(requestIds)) {
568
+ if (!isRecord(msg) || !("error" in msg) || msg["error"] == null) {
569
+ if (isRecord(msg) && msg["chunk"] !== true && msg["id"] !== -32603 && "id" in msg) state.remaining.delete(msg["id"]);
570
+ return msg;
571
+ }
572
+ const error = scrubError(msg["error"]);
573
+ let id = msg["id"];
574
+ if (id === -32603) {
575
+ const next = state.remaining.values().next();
576
+ if (!next.done) {
577
+ id = next.value;
578
+ state.remaining.delete(next.value);
579
+ } else id = null;
580
+ } else if (id !== void 0 && id !== null) state.remaining.delete(id);
581
+ if (id === void 0) id = null;
582
+ return {
583
+ jsonrpc: "2.0",
584
+ id,
585
+ error
586
+ };
587
+ }
588
+ /**
589
+ * Rewrite a JSON / NDJSON body so clients never see Effect `_tag` / `data` trees.
590
+ * Accepts a single object, a JSON array, or newline-delimited frames.
591
+ */
592
+ function scrubRpcJson(body, requestIds = []) {
593
+ const trimmed = body.replace(/^\uFEFF/, "");
594
+ if (!trimmed) return body;
595
+ const state = createIdRepairState(requestIds);
596
+ if (trimmed.includes("\n")) {
597
+ const lines = trimmed.split("\n");
598
+ const out = [];
599
+ for (const line of lines) {
600
+ if (line === "") continue;
601
+ out.push(scrubRpcLine(line, state));
602
+ }
603
+ return trimmed.endsWith("\n") ? `${out.join("\n")}\n` : out.join("\n");
604
+ }
605
+ try {
606
+ const parsed = JSON.parse(trimmed);
607
+ if (Array.isArray(parsed)) return JSON.stringify(parsed.map((msg) => scrubRpcMessage(msg, requestIds, state)));
608
+ return JSON.stringify(scrubRpcMessage(parsed, requestIds, state));
609
+ } catch {
610
+ return body;
611
+ }
612
+ }
613
+ function scrubRpcLine(line, state) {
614
+ try {
615
+ return JSON.stringify(scrubRpcMessage(JSON.parse(line), [], state));
616
+ } catch {
617
+ return line;
618
+ }
619
+ }
620
+ /** Collect JSON-RPC request ids from a unary object or batch array body. */
621
+ function extractJsonRpcRequestIds(body) {
622
+ try {
623
+ let text = typeof body === "string" ? body : new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));
624
+ text = text.replace(/^\uFEFF/, "").trimEnd();
625
+ if (text.includes("\n")) {
626
+ const ids = [];
627
+ for (const line of text.split("\n")) {
628
+ if (!line) continue;
629
+ const parsed = JSON.parse(line);
630
+ if (isRecord(parsed) && "id" in parsed) ids.push(parsed["id"]);
631
+ }
632
+ return ids;
633
+ }
634
+ const parsed = JSON.parse(text);
635
+ if (Array.isArray(parsed)) return parsed.filter(isRecord).filter((item) => "id" in item).map((item) => item["id"]);
636
+ if (isRecord(parsed) && "id" in parsed) return [parsed["id"]];
637
+ } catch {}
638
+ return [];
639
+ }
640
+ /** Ensure a body is a valid NDJSON frame (Effect's ndJsonRpc decode requires a trailing newline). */
641
+ function ensureNdjsonBody(buf) {
642
+ const bytes = new Uint8Array(buf);
643
+ if (bytes.length > 0 && bytes[bytes.length - 1] === 10) return bytes;
644
+ const out = new Uint8Array(bytes.length + 1);
645
+ out.set(bytes);
646
+ out[bytes.length] = 10;
647
+ return out;
648
+ }
649
+ /**
650
+ * TransformStream that scrubs Effect defect payloads one NDJSON line at a time,
651
+ * so long-running stream actions stay incremental.
652
+ */
653
+ function scrubNdjsonTransform(requestIds = []) {
654
+ const decoder = new TextDecoder();
655
+ const encoder = new TextEncoder();
656
+ const state = createIdRepairState(requestIds);
657
+ let pending = "";
658
+ return new TransformStream({
659
+ transform(chunk, controller) {
660
+ pending += decoder.decode(chunk, { stream: true });
661
+ let nl = pending.indexOf("\n");
662
+ while (nl !== -1) {
663
+ const line = pending.slice(0, nl);
664
+ pending = pending.slice(nl + 1);
665
+ if (line.length > 0) controller.enqueue(encoder.encode(`${scrubRpcLine(line, state)}\n`));
666
+ nl = pending.indexOf("\n");
667
+ }
668
+ },
669
+ flush(controller) {
670
+ pending += decoder.decode();
671
+ if (pending.length > 0) {
672
+ controller.enqueue(encoder.encode(`${scrubRpcLine(pending, state)}\n`));
673
+ pending = "";
674
+ }
675
+ }
676
+ });
677
+ }
678
+ //#endregion
679
+ //#region src/rpc/server.ts
680
+ const JSON_RPC_FORBIDDEN = {
681
+ jsonrpc: "2.0",
682
+ error: {
683
+ code: -32600,
684
+ message: "Forbidden"
685
+ },
686
+ id: null
687
+ };
688
+ const bundles = /* @__PURE__ */ new Map();
689
+ const groupIds = /* @__PURE__ */ new WeakMap();
690
+ let nextGroupId = 0;
691
+ const serialization = RpcSerialization.layerNdJsonRpc();
692
+ function bundleKey(group, path, transport) {
693
+ let id = groupIds.get(group);
694
+ if (id === void 0) {
695
+ id = nextGroupId++;
696
+ groupIds.set(group, id);
697
+ }
698
+ return `${id}:${path}:${transport}`;
699
+ }
700
+ function forbidden() {
701
+ return new Response(JSON.stringify(JSON_RPC_FORBIDDEN), {
702
+ status: 403,
703
+ headers: { "content-type": "application/json" }
704
+ });
705
+ }
706
+ function methodNotAllowed() {
707
+ return new Response("Method Not Allowed", {
708
+ status: 405,
709
+ headers: { Allow: "POST" }
710
+ });
711
+ }
712
+ function buildBundle(group, handlers, path, transport) {
713
+ const app = RpcServer.layerHttp({
714
+ group,
715
+ path,
716
+ protocol: transport === "ws" ? "websocket" : "http"
717
+ }).pipe(Layer.provide(handlers), Layer.provide(serialization));
718
+ return HttpRouter.toWebHandler(app, { disableLogger: true });
719
+ }
720
+ function bundleFor(group, handlers, path, transport) {
721
+ const key = bundleKey(group, path, transport);
722
+ const cached = bundles.get(key);
723
+ if (cached) return cached;
724
+ const built = buildBundle(group, handlers, path, transport);
725
+ bundles.set(key, built);
726
+ return built;
727
+ }
728
+ function scrubJsonResponse(response, requestIds) {
729
+ const contentType = response.headers.get("content-type") ?? "";
730
+ if (!contentType.includes("json")) return response;
731
+ if (response.body && (contentType.includes("application/json-rpc") || contentType.includes("ndjson"))) {
732
+ const headers = new Headers(response.headers);
733
+ headers.delete("content-length");
734
+ return new Response(response.body.pipeThrough(scrubNdjsonTransform(requestIds)), {
735
+ status: response.status,
736
+ statusText: response.statusText,
737
+ headers
738
+ });
739
+ }
740
+ return response;
741
+ }
742
+ async function scrubBufferedJson(response, requestIds) {
743
+ const text = await response.text();
744
+ const headers = new Headers(response.headers);
745
+ headers.delete("content-length");
746
+ return new Response(scrubRpcJson(text, requestIds), {
747
+ status: response.status,
748
+ statusText: response.statusText,
749
+ headers
750
+ });
751
+ }
752
+ function createActionHandler(group, handlers, options = {}) {
753
+ const path = options.path ?? "/__oxide/action";
754
+ const transport = options.transport ?? "http";
755
+ const sameOrigin = options.sameOrigin ?? true;
756
+ return async (request) => {
757
+ if (!matchesActionPath(new URL(request.url).pathname, path)) return new Response("Not Found", { status: 404 });
758
+ if (transport === "http" && request.method !== "POST") return methodNotAllowed();
759
+ if (sameOrigin && !isSameOrigin(request)) return forbidden();
760
+ const rawBody = ensureNdjsonBody(await request.arrayBuffer());
761
+ const requestIds = extractJsonRpcRequestIds(rawBody);
762
+ const headers = new Headers(request.headers);
763
+ headers.set("content-type", NDJSON_CONTENT);
764
+ const forwarded = new Request(request.url, {
765
+ method: request.method,
766
+ headers,
767
+ body: rawBody,
768
+ signal: request.signal
769
+ });
770
+ const extra = await options.createContext?.(forwarded) ?? {};
771
+ const { handler } = bundleFor(group, handlers, path, transport);
772
+ const response = await runWithRequest(forwarded, () => handler(forwarded), extra);
773
+ const contentType = response.headers.get("content-type") ?? "";
774
+ if (contentType.includes("application/json-rpc") || contentType.includes("ndjson")) return scrubJsonResponse(response, requestIds);
775
+ if (contentType.includes("json")) return scrubBufferedJson(response, requestIds);
776
+ return response;
777
+ };
778
+ }
779
+ function disposeActionHandler(group, path = ACTION_PATH, transport = "http") {
780
+ const key = bundleKey(group, path, transport);
781
+ const bundle = bundles.get(key);
782
+ bundles.delete(key);
783
+ return bundle?.dispose() ?? Promise.resolve();
784
+ }
785
+ //#endregion
786
+ //#region src/rpc/ws.ts
787
+ function parseMessage(message, maxBytes) {
788
+ try {
789
+ const raw = message.text();
790
+ if (new TextEncoder().encode(raw).byteLength > maxBytes) return {
791
+ ok: false,
792
+ tooLarge: true
793
+ };
794
+ return {
795
+ ok: true,
796
+ value: raw
797
+ };
798
+ } catch {
799
+ return { ok: false };
800
+ }
801
+ }
802
+ /**
803
+ * Effect's socket client sends `@effect/rpc/Ping` keepalives (no id) and hangs
804
+ * up unless the server answers `@effect/rpc/Pong`. Handle control messages here
805
+ * so they never reach the action handler.
806
+ */
807
+ function controlReply(raw) {
808
+ try {
809
+ const parsed = JSON.parse(raw);
810
+ if (parsed && typeof parsed === "object" && parsed.method === "@effect/rpc/Ping") return JSON.stringify({
811
+ jsonrpc: "2.0",
812
+ method: "@effect/rpc/Pong"
813
+ });
814
+ } catch {}
815
+ }
816
+ /** Forward each complete NDJSON line as its own WS message (keeps streams incremental). */
817
+ async function sendNdjsonFrames(peer, response, signal) {
818
+ if (signal.aborted) {
819
+ await response.body?.cancel();
820
+ return;
821
+ }
822
+ if (!response.body) {
823
+ const text = await response.text();
824
+ if (text && !signal.aborted) peer.send(text);
825
+ return;
826
+ }
827
+ const reader = response.body.getReader();
828
+ const decoder = new TextDecoder();
829
+ let pending = "";
830
+ const onAbort = () => {
831
+ reader.cancel();
832
+ };
833
+ signal.addEventListener("abort", onAbort, { once: true });
834
+ try {
835
+ while (!signal.aborted) {
836
+ const { done, value } = await reader.read();
837
+ if (done) break;
838
+ pending += decoder.decode(value, { stream: true });
839
+ let nl = pending.indexOf("\n");
840
+ while (nl !== -1) {
841
+ const line = pending.slice(0, nl);
842
+ pending = pending.slice(nl + 1);
843
+ if (line.length > 0 && !signal.aborted) peer.send(`${line}\n`);
844
+ nl = pending.indexOf("\n");
845
+ }
846
+ }
847
+ if (!signal.aborted) {
848
+ pending += decoder.decode();
849
+ if (pending.length > 0) peer.send(pending.endsWith("\n") ? pending : `${pending}\n`);
850
+ }
851
+ } finally {
852
+ signal.removeEventListener("abort", onAbort);
853
+ }
854
+ }
855
+ function createWsHooks(group, handlers, options = {}) {
856
+ const path = options.path ?? "/__oxide/action";
857
+ const maxBytes = options.maxMessageSize ?? 1048576;
858
+ const sameOrigin = options.sameOrigin ?? true;
859
+ const baseOptions = {
860
+ path,
861
+ transport: "http",
862
+ sameOrigin
863
+ };
864
+ return {
865
+ upgrade(req) {
866
+ let pathname;
867
+ try {
868
+ pathname = new URL(req.url).pathname;
869
+ } catch {
870
+ return new Response("Bad Request", { status: 400 });
871
+ }
872
+ if (!matchesActionPath(pathname, path)) return new Response("Not Found", { status: 404 });
873
+ if (sameOrigin && !isSameOrigin(req)) return new Response("Forbidden", { status: 403 });
874
+ },
875
+ async message(peer, message) {
876
+ const parsed = parseMessage(message, maxBytes);
877
+ if (!parsed.ok) {
878
+ peer.send(JSON.stringify({
879
+ jsonrpc: "2.0",
880
+ error: {
881
+ code: -32600,
882
+ message: parsed.tooLarge ? "Payload too large" : "Parse error"
883
+ },
884
+ id: null
885
+ }));
886
+ return;
887
+ }
888
+ const pingReply = controlReply(parsed.value);
889
+ if (pingReply !== void 0) {
890
+ peer.send(pingReply);
891
+ return;
892
+ }
893
+ const abort = new AbortController();
894
+ peer.onClose?.(() => abort.abort());
895
+ const host = peer.request?.headers.get("host") ?? "localhost";
896
+ const headers = new Headers(peer.request?.headers);
897
+ headers.set("content-type", NDJSON_CONTENT);
898
+ const peerCtx = await options.createContext?.(peer) ?? peer.context;
899
+ await sendNdjsonFrames(peer, await createActionHandler(group, handlers, {
900
+ ...baseOptions,
901
+ createContext: (req) => ({
902
+ ...peerCtx,
903
+ req
904
+ })
905
+ })(new Request(`http://${host}${path}`, {
906
+ method: "POST",
907
+ headers,
908
+ body: parsed.value,
909
+ signal: abort.signal
910
+ })), abort.signal);
911
+ }
912
+ };
913
+ }
914
+ //#endregion
915
+ export { pluginShouldStub as A, isServerFileId as C, nodeToWebRequest as D, moduleKey as E, sendWebResponseFrom as M, parseExportedNames as O, generateWorkerWrapper as S, matchesActionPath as T, actions_exports as _, extractJsonRpcRequestIds as a, generateClientModule as b, scrubRpcMessage as c, RESOLVED_VIRTUAL_CLIENT_ID as d, RESOLVED_VIRTUAL_WORKER_ID as f, VIRTUAL_WORKER_ID as g, VIRTUAL_CLIENT_ID as h, ensureNdjsonBody as i, scanServerFiles as j, parseStreamExports as k, ACTION_PATH as l, VIRTUAL_ACTIONS_ID as m, createActionHandler as n, scrubNdjsonTransform as o, RequestBodyTooLargeError as p, disposeActionHandler as r, scrubRpcJson as s, createWsHooks as t, RESOLVED_VIRTUAL_ACTIONS_ID as u, generateActionsClientModule as v, loadClientStub as w, generateClientStub as x, generateActionsModule as y };