oxidejs 0.2.3 → 0.3.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 +8 -4
- package/dist/actions-DE6p5Cyp.mjs +504 -0
- package/dist/client-Bc4g9AEw.mjs +145 -0
- package/dist/context-C1UFQ0Zc.d.mts +64 -0
- package/dist/context-zrTZyYpF.mjs +42 -0
- package/dist/index.d.mts +2 -35
- package/dist/index.mjs +108 -2
- package/dist/plugin-IQa_lKkT.mjs +601 -0
- package/dist/plugin.d.mts +8 -0
- package/dist/plugin.mjs +2 -0
- package/dist/rpc/client.d.mts +13 -0
- package/dist/rpc/client.mjs +2 -0
- package/dist/rpc-DYFEdah8.mjs +382 -0
- package/dist/rpc.d.mts +75 -0
- package/dist/rpc.mjs +3 -0
- package/dist/rsbuild.mjs +1 -1
- package/dist/vite.mjs +1 -1
- package/dist/worker-dom/install.d.mts +1 -0
- package/dist/worker-dom/install.mjs +6 -0
- package/dist/worker-dom.d.mts +5 -0
- package/dist/worker-dom.mjs +16 -0
- package/package.json +19 -8
- package/virtual.d.ts +7 -4
- package/dist/src-D-qdNVqg.mjs +0 -954
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ dist/
|
|
|
12
12
|
|
|
13
13
|
v1 targets **Vite** and **Rsbuild** via unplugin. Other bundlers are out of scope for now.
|
|
14
14
|
|
|
15
|
+
The `oxidejs` entry exports runtime helpers (`action`, `useRequest`, …). The bundler plugin lives at `oxidejs/vite` or `oxidejs/rsbuild` — keep those separate so `*.server.ts` can import `oxidejs` under `preset: "celld"` without pulling Node build tooling into the worker graph.
|
|
16
|
+
|
|
15
17
|
## Vite
|
|
16
18
|
|
|
17
19
|
```ts
|
|
@@ -47,7 +49,7 @@ oxide({
|
|
|
47
49
|
|
|
48
50
|
## Server actions
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Files named `*.server.ts`, `*.server.tsx`, `*.server.js`, or `*.server.jsx` are server-only. A client import is replaced with an RPC 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 the request context (`{ 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 `oxidejs/rpc`.
|
|
51
53
|
|
|
52
54
|
```ts
|
|
53
55
|
// src/test.server.ts
|
|
@@ -76,7 +78,7 @@ export default {
|
|
|
76
78
|
};
|
|
77
79
|
```
|
|
78
80
|
|
|
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
|
|
81
|
+
`action()` is runtime identity — it marks the export and adds a typed transport-only `{ signal }` argument. Wrap `async function*` in it to stream over Effect RPC as newline-delimited JSON-RPC (`application/json-rpc` frames, not SSE). On the client, await the call to get the async generator. Inside server code, always read the non-optional signal from `useRequest().signal`:
|
|
80
82
|
|
|
81
83
|
```ts
|
|
82
84
|
// src/test.server.ts
|
|
@@ -91,7 +93,9 @@ export const ticks = action(async function* (n: number) {
|
|
|
91
93
|
import { ticks } from "./test.server";
|
|
92
94
|
|
|
93
95
|
const ac = new AbortController();
|
|
94
|
-
const
|
|
96
|
+
for await (const value of await ticks(10, { signal: ac.signal })) {
|
|
97
|
+
console.log(value);
|
|
98
|
+
}
|
|
95
99
|
ac.abort();
|
|
96
100
|
```
|
|
97
101
|
|
|
@@ -174,7 +178,7 @@ The generated `__asset` function uses `path.join` — not `path.resolve` — so
|
|
|
174
178
|
|
|
175
179
|
### Server actions (`*.server.{ts,tsx,js,jsx}`)
|
|
176
180
|
|
|
177
|
-
- Server action code is **never bundled into the client**. Client imports are replaced with
|
|
181
|
+
- Server action code is **never bundled into the client**. Client imports are replaced with RPC stubs that POST the action endpoint (default `/__oxide/action`). The original source stays server-only.
|
|
178
182
|
- Only `action()`-wrapped exports are exposed as RPC; other exports stay server-local.
|
|
179
183
|
- The endpoint is POST-only. Non-POST requests return `405`.
|
|
180
184
|
- Method dispatch uses `Object.hasOwn`, blocking `__proto__` / `constructor` walks.
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __exportAll = (all, no_symbols) => {
|
|
6
|
+
let target = {};
|
|
7
|
+
for (var name in all) __defProp(target, name, {
|
|
8
|
+
get: all[name],
|
|
9
|
+
enumerable: true
|
|
10
|
+
});
|
|
11
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
12
|
+
return target;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/actions.ts
|
|
16
|
+
var actions_exports = /* @__PURE__ */ __exportAll({
|
|
17
|
+
ACTION_PATH: () => ACTION_PATH,
|
|
18
|
+
RESOLVED_VIRTUAL_ACTIONS_ID: () => RESOLVED_VIRTUAL_ACTIONS_ID,
|
|
19
|
+
RESOLVED_VIRTUAL_CLIENT_ID: () => RESOLVED_VIRTUAL_CLIENT_ID,
|
|
20
|
+
RESOLVED_VIRTUAL_WORKER_ID: () => RESOLVED_VIRTUAL_WORKER_ID,
|
|
21
|
+
RequestBodyTooLargeError: () => RequestBodyTooLargeError,
|
|
22
|
+
VIRTUAL_ACTIONS_ID: () => VIRTUAL_ACTIONS_ID,
|
|
23
|
+
VIRTUAL_CLIENT_ID: () => VIRTUAL_CLIENT_ID,
|
|
24
|
+
VIRTUAL_WORKER_ID: () => VIRTUAL_WORKER_ID,
|
|
25
|
+
generateActionsClientModule: () => generateActionsClientModule,
|
|
26
|
+
generateActionsModule: () => generateActionsModule,
|
|
27
|
+
generateClientModule: () => generateClientModule,
|
|
28
|
+
generateClientStub: () => generateClientStub,
|
|
29
|
+
generateWorkerWrapper: () => generateWorkerWrapper,
|
|
30
|
+
isServerFileId: () => isServerFileId,
|
|
31
|
+
loadClientStub: () => loadClientStub,
|
|
32
|
+
matchesActionPath: () => matchesActionPath,
|
|
33
|
+
moduleKey: () => moduleKey,
|
|
34
|
+
nodeToWebRequest: () => nodeToWebRequest,
|
|
35
|
+
parseExportedNames: () => parseExportedNames,
|
|
36
|
+
parseStreamExports: () => parseStreamExports,
|
|
37
|
+
pluginShouldStub: () => pluginShouldStub,
|
|
38
|
+
scanServerFiles: () => scanServerFiles,
|
|
39
|
+
sendWebResponseFrom: () => sendWebResponseFrom,
|
|
40
|
+
shouldStubServerModule: () => shouldStubServerModule
|
|
41
|
+
});
|
|
42
|
+
const VIRTUAL_ACTIONS_ID = "virtual:oxide/actions";
|
|
43
|
+
const RESOLVED_VIRTUAL_ACTIONS_ID = `\0${VIRTUAL_ACTIONS_ID}`;
|
|
44
|
+
const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
|
|
45
|
+
const RESOLVED_VIRTUAL_WORKER_ID = `\0${VIRTUAL_WORKER_ID}`;
|
|
46
|
+
const VIRTUAL_CLIENT_ID = "virtual:oxide/client";
|
|
47
|
+
const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
|
|
48
|
+
const ACTION_PATH = "/__oxide/action";
|
|
49
|
+
/** Match the action endpoint with or without a trailing slash (Effect RPC posts to `path/`). */
|
|
50
|
+
function matchesActionPath(pathname, path = ACTION_PATH) {
|
|
51
|
+
return pathname === path || pathname === `${path}/`;
|
|
52
|
+
}
|
|
53
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
54
|
+
"node_modules",
|
|
55
|
+
"dist",
|
|
56
|
+
".git",
|
|
57
|
+
".wrangler"
|
|
58
|
+
]);
|
|
59
|
+
/** Only `export const name = action(...)` become remote RPC actions. Everything else stays server-local. */
|
|
60
|
+
const EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?action\s*\(/gm;
|
|
61
|
+
const STREAM_EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*action\s*\(\s*async\s+function\s*\*/gm;
|
|
62
|
+
function isServerFileId(id) {
|
|
63
|
+
const file = id.split("?")[0]?.replace(/\\/g, "/") ?? "";
|
|
64
|
+
return [
|
|
65
|
+
".ts",
|
|
66
|
+
".tsx",
|
|
67
|
+
".js",
|
|
68
|
+
".jsx"
|
|
69
|
+
].some((ext) => file.endsWith(`.server${ext}`));
|
|
70
|
+
}
|
|
71
|
+
function moduleKey(absFile) {
|
|
72
|
+
return path.basename(absFile).replace(/\.server\.(?:[jt]sx?)$/i, "");
|
|
73
|
+
}
|
|
74
|
+
function parseExportedNames(source) {
|
|
75
|
+
const names = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const match of source.matchAll(EXPORT_RE)) {
|
|
77
|
+
const name = match[1];
|
|
78
|
+
if (name) names.add(name);
|
|
79
|
+
}
|
|
80
|
+
return [...names];
|
|
81
|
+
}
|
|
82
|
+
function parseStreamExports(source) {
|
|
83
|
+
const names = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const match of source.matchAll(STREAM_EXPORT_RE)) {
|
|
85
|
+
const name = match[1];
|
|
86
|
+
if (name) names.add(name);
|
|
87
|
+
}
|
|
88
|
+
return [...names];
|
|
89
|
+
}
|
|
90
|
+
function scanServerFiles(root) {
|
|
91
|
+
const files = [];
|
|
92
|
+
const walk = (dir) => {
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.name.startsWith(".")) continue;
|
|
101
|
+
const abs = path.join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
104
|
+
walk(abs);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (entry.isFile() && isServerFileId(entry.name)) files.push(abs);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(root);
|
|
111
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
112
|
+
const modules = [];
|
|
113
|
+
for (const abs of files.sort()) {
|
|
114
|
+
const key = moduleKey(abs);
|
|
115
|
+
if (!key) throw new Error(`oxidejs: invalid server module name: ${abs}`);
|
|
116
|
+
const existing = byKey.get(key);
|
|
117
|
+
if (existing) throw new Error(`oxidejs: duplicate server module key "${key}": ${existing} and ${abs}`);
|
|
118
|
+
byKey.set(key, abs);
|
|
119
|
+
const source = fs.readFileSync(abs, "utf8");
|
|
120
|
+
modules.push({
|
|
121
|
+
abs,
|
|
122
|
+
key,
|
|
123
|
+
exports: parseExportedNames(source),
|
|
124
|
+
streams: parseStreamExports(source)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return modules;
|
|
128
|
+
}
|
|
129
|
+
function generateClientModule(transport = "http", headers, path = ACTION_PATH) {
|
|
130
|
+
const opts = { url: path };
|
|
131
|
+
if (transport === "ws") opts.transport = "ws";
|
|
132
|
+
if (headers) opts.headers = headers;
|
|
133
|
+
if (transport === "ws") return `import { createClient } from "oxidejs/rpc/client";
|
|
134
|
+
import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
135
|
+
const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
|
|
136
|
+
const __host = typeof location === "undefined" ? "localhost" : location.host;
|
|
137
|
+
export const client = createClient(actionsGroup, { ...${JSON.stringify(opts)}, url: __proto + "//" + __host + ${JSON.stringify(path)} });
|
|
138
|
+
`;
|
|
139
|
+
return `import { createClient } from "oxidejs/rpc/client";
|
|
140
|
+
import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
141
|
+
export const client = createClient(actionsGroup, ${JSON.stringify(opts)});
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
function generateClientStub(mod) {
|
|
145
|
+
const streams = new Set(mod.streams ?? []);
|
|
146
|
+
const lines = [
|
|
147
|
+
`// oxidejs:client-stub`,
|
|
148
|
+
`import { wrapClientRpc, wrapClientStreamRpc } from "oxidejs";`,
|
|
149
|
+
`import { client } from ${JSON.stringify(VIRTUAL_CLIENT_ID)};`
|
|
150
|
+
];
|
|
151
|
+
for (const name of mod.exports) {
|
|
152
|
+
const call = `client[${JSON.stringify(mod.key)}][${JSON.stringify(name)}]`;
|
|
153
|
+
const peel = `(...args) => {
|
|
154
|
+
const opts = args.at(-1);
|
|
155
|
+
// ponytail: peel last { signal } only. A lone payload { signal: AbortSignal } is treated as CallOptions.
|
|
156
|
+
return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1
|
|
157
|
+
? ${call}(...args.slice(0, -1), opts)
|
|
158
|
+
: ${call}(...args);
|
|
159
|
+
}`;
|
|
160
|
+
if (streams.has(name)) lines.push(`export const ${name} = wrapClientStreamRpc(${peel});`);
|
|
161
|
+
else lines.push(`export const ${name} = wrapClientRpc(${peel});`);
|
|
162
|
+
}
|
|
163
|
+
return `${lines.join("\n")}\n`;
|
|
164
|
+
}
|
|
165
|
+
function generateActionsClientModule(modules) {
|
|
166
|
+
const lines = [`import { Schema } from "effect";`, `import { Rpc, RpcGroup } from "effect/unstable/rpc";`];
|
|
167
|
+
const rpcNames = [];
|
|
168
|
+
modules.forEach((mod, i) => {
|
|
169
|
+
for (const name of mod.exports) {
|
|
170
|
+
const rpc = `__rpc_${i}_${name}`;
|
|
171
|
+
rpcNames.push(rpc);
|
|
172
|
+
const tag = `${mod.key}.${name}`;
|
|
173
|
+
const stream = mod.streams?.includes(name) ?? false;
|
|
174
|
+
lines.push(`const ${rpc} = Rpc.make(${JSON.stringify(tag)}, { payload: Schema.Struct({ args: Schema.Array(Schema.Unknown) }), success: Schema.Unknown${stream ? ", stream: true" : ""} });`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`);
|
|
178
|
+
lines.push(`export default actionsGroup;`);
|
|
179
|
+
lines.push(`export { actionsGroup as actions };`);
|
|
180
|
+
return `${lines.join("\n")}\n`;
|
|
181
|
+
}
|
|
182
|
+
function generateActionsModule(modules, opts) {
|
|
183
|
+
const lines = [
|
|
184
|
+
`import { Effect } from "effect";`,
|
|
185
|
+
`import { Schema } from "effect";`,
|
|
186
|
+
`import { Rpc, RpcGroup } from "effect/unstable/rpc";`,
|
|
187
|
+
`import { AsyncLocalStorage } from "node:async_hooks";`,
|
|
188
|
+
`import { asyncGenToStreamInContext } from "oxidejs/rpc";`,
|
|
189
|
+
`const __alsKey = Symbol.for("oxidejs.requestContext");`,
|
|
190
|
+
`const __als = globalThis[__alsKey] ??= new AsyncLocalStorage();`,
|
|
191
|
+
`const __store = () => {`,
|
|
192
|
+
` const ctx = __als.getStore();`,
|
|
193
|
+
` if (!ctx) throw new Error("oxidejs: request context is unavailable");`,
|
|
194
|
+
` return ctx;`,
|
|
195
|
+
`};`,
|
|
196
|
+
`const __run = (fn) =>`,
|
|
197
|
+
` Effect.promise(() => __als.run(__store(), fn)).pipe(`,
|
|
198
|
+
` Effect.map((value) => (value === undefined ? null : value)),`,
|
|
199
|
+
` );`,
|
|
200
|
+
`const __withStore = (store, fn) => __als.run(store, fn);`
|
|
201
|
+
];
|
|
202
|
+
const rpcNames = [];
|
|
203
|
+
const aliases = modules.map((mod, i) => {
|
|
204
|
+
const alias = `__m${i}`;
|
|
205
|
+
const spec = opts?.bust === true ? `${mod.abs}?t=${fs.statSync(mod.abs).mtimeMs}` : mod.abs;
|
|
206
|
+
lines.push(`import * as ${alias} from ${JSON.stringify(spec)};`);
|
|
207
|
+
for (const name of mod.exports) {
|
|
208
|
+
const rpc = `__rpc_${i}_${name}`;
|
|
209
|
+
rpcNames.push(rpc);
|
|
210
|
+
const tag = `${mod.key}.${name}`;
|
|
211
|
+
const stream = mod.streams?.includes(name) ?? false;
|
|
212
|
+
lines.push(`const ${rpc} = Rpc.make(${JSON.stringify(tag)}, { payload: Schema.Struct({ args: Schema.Array(Schema.Unknown) }), success: Schema.Unknown${stream ? ", stream: true" : ""} });`);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
alias,
|
|
216
|
+
mod
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`);
|
|
220
|
+
lines.push(`export const actionsHandlers = actionsGroup.toLayer({`);
|
|
221
|
+
for (const { alias, mod } of aliases) for (const name of mod.exports) {
|
|
222
|
+
const tag = `${mod.key}.${name}`;
|
|
223
|
+
const stream = mod.streams?.includes(name) ?? false;
|
|
224
|
+
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)),`);
|
|
225
|
+
}
|
|
226
|
+
lines.push(`});`);
|
|
227
|
+
lines.push(`export default actionsGroup;`);
|
|
228
|
+
lines.push(`export { actionsGroup as actions };`);
|
|
229
|
+
return `${lines.join("\n")}\n`;
|
|
230
|
+
}
|
|
231
|
+
function pipeResponse(req, res, response) {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
res.statusCode = response.status;
|
|
234
|
+
response.headers.forEach((value, key) => {
|
|
235
|
+
res.setHeader(key, value);
|
|
236
|
+
});
|
|
237
|
+
if (!response.body) {
|
|
238
|
+
res.end();
|
|
239
|
+
resolve();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const reader = response.body.getReader();
|
|
243
|
+
const abort = () => {
|
|
244
|
+
reader.cancel();
|
|
245
|
+
};
|
|
246
|
+
req.once("aborted", abort);
|
|
247
|
+
const pull = () => {
|
|
248
|
+
reader.read().then(({ done, value }) => {
|
|
249
|
+
if (done) {
|
|
250
|
+
req.off("aborted", abort);
|
|
251
|
+
res.end();
|
|
252
|
+
resolve();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (value) res.write(value);
|
|
256
|
+
pull();
|
|
257
|
+
}, reject);
|
|
258
|
+
};
|
|
259
|
+
pull();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function generateWorkerWrapper(userWorkerAbs, opts = {}) {
|
|
263
|
+
const preset = opts.preset ?? "fetch";
|
|
264
|
+
const clientDir = opts.clientDir ?? "client";
|
|
265
|
+
const actionPath = opts.actionPath ?? "/__oxide/action";
|
|
266
|
+
const sameOrigin = opts.actionSameOrigin ?? false;
|
|
267
|
+
const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
|
|
268
|
+
const hasActions = opts.hasActions !== false;
|
|
269
|
+
const ws = hasActions && opts.actions === "ws";
|
|
270
|
+
const bodyLimit = opts.bodyLimit ?? 1048576;
|
|
271
|
+
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`;
|
|
272
|
+
const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
|
|
273
|
+
import { extname, join } from "node:path";
|
|
274
|
+
const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
|
|
275
|
+
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" };
|
|
276
|
+
function __nav(request) {
|
|
277
|
+
const dest = request.headers.get("sec-fetch-dest");
|
|
278
|
+
if (dest) return dest === "document";
|
|
279
|
+
return (request.headers.get("accept") ?? "").includes("text/html");
|
|
280
|
+
}
|
|
281
|
+
function __cache(file) {
|
|
282
|
+
if (file === "index.html") return "no-cache";
|
|
283
|
+
return /[-.][0-9a-f]{8,}.[a-z0-9]+$/i.test(file) ? "public, max-age=31536000, immutable" : undefined;
|
|
284
|
+
}
|
|
285
|
+
function __rel(pathname, spa) {
|
|
286
|
+
if (pathname.includes("\0")) return;
|
|
287
|
+
let file;
|
|
288
|
+
try { file = decodeURIComponent(pathname); } catch { return; }
|
|
289
|
+
if (file.includes("\0")) return;
|
|
290
|
+
if (file === "/" || spa) file = "/index.html";
|
|
291
|
+
if (!file.startsWith("/") || file.split("/").includes("..")) return;
|
|
292
|
+
const rel = file.slice(1);
|
|
293
|
+
if (rel.startsWith("/")) return;
|
|
294
|
+
return rel;
|
|
295
|
+
}
|
|
296
|
+
async function __asset(request, spa) {
|
|
297
|
+
const file = __rel(new URL(request.url).pathname, spa);
|
|
298
|
+
if (!file) return;
|
|
299
|
+
try {
|
|
300
|
+
const body = await readFile(join(__assets, file));
|
|
301
|
+
const headers = { "content-type": __types[extname(file)] ?? "application/octet-stream" };
|
|
302
|
+
const cache = __cache(file);
|
|
303
|
+
if (cache) headers["cache-control"] = cache;
|
|
304
|
+
const etag = '"' + body.length.toString(16) + "-" + file + '"';
|
|
305
|
+
headers["etag"] = etag;
|
|
306
|
+
if (request.headers.get("if-none-match") === etag) {
|
|
307
|
+
return new Response(null, { status: 304, headers });
|
|
308
|
+
}
|
|
309
|
+
return new Response(body, { headers });
|
|
310
|
+
} catch {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
` : "";
|
|
315
|
+
const envJson = JSON.stringify(opts.env ?? {});
|
|
316
|
+
const celldAfterAction = `{
|
|
317
|
+
const hit = typeof user.fetch === "function" ? await user.fetch(request, env ?? ${envJson}, ctx) : undefined;
|
|
318
|
+
if (hit) return hit;
|
|
319
|
+
const assets = env?.ASSETS;
|
|
320
|
+
if (assets && typeof assets.fetch === "function") return assets.fetch(request);
|
|
321
|
+
return __nf();
|
|
322
|
+
}`;
|
|
323
|
+
const afterAction = serveAssets ? `if (typeof user.fetch === "function") {
|
|
324
|
+
const hit = await user.fetch(request, env ?? ${envJson}, ctx);
|
|
325
|
+
if (hit) return hit;
|
|
326
|
+
}
|
|
327
|
+
return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? __nf();` : preset === "celld" ? celldAfterAction : `return typeof user.fetch === "function"
|
|
328
|
+
? user.fetch(request, env, ctx)
|
|
329
|
+
: __nf();`;
|
|
330
|
+
const listen = preset === "fetch" ? `
|
|
331
|
+
import { createServer } from "node:http";
|
|
332
|
+
import { pathToFileURL } from "node:url";
|
|
333
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
334
|
+
const port = Number(process.env.PORT) || 3000;
|
|
335
|
+
const bodyLimit = ${bodyLimit};
|
|
336
|
+
const server = createServer(async (req, res) => {
|
|
337
|
+
const url = \`http://\${req.headers.host ?? "localhost"}\${req.url ?? "/"}\`;
|
|
338
|
+
const headers = new Headers();
|
|
339
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
340
|
+
if (value === undefined) continue;
|
|
341
|
+
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
342
|
+
else headers.set(key, value);
|
|
343
|
+
}
|
|
344
|
+
const ac = new AbortController();
|
|
345
|
+
req.once("aborted", () => ac.abort());
|
|
346
|
+
const method = req.method ?? "GET";
|
|
347
|
+
const chunks = [];
|
|
348
|
+
let size = 0;
|
|
349
|
+
if (method !== "GET" && method !== "HEAD") for await (const chunk of req) {
|
|
350
|
+
size += chunk.length;
|
|
351
|
+
// Bound request buffering — unbounded bodies are a memory DoS vector.
|
|
352
|
+
if (size > ${bodyLimit}) { res.statusCode = 413; res.end(); return; }
|
|
353
|
+
chunks.push(chunk);
|
|
354
|
+
}
|
|
355
|
+
const init = { method, headers, signal: ac.signal };
|
|
356
|
+
if (chunks.length) init.body = Buffer.concat(chunks);
|
|
357
|
+
const response = await app.fetch(new Request(url, init));
|
|
358
|
+
res.statusCode = response.status;
|
|
359
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
360
|
+
if (!response.body) { res.end(); return; }
|
|
361
|
+
const reader = response.body.getReader();
|
|
362
|
+
for (;;) {
|
|
363
|
+
const { done, value } = await reader.read();
|
|
364
|
+
if (done) break;
|
|
365
|
+
if (value) res.write(value);
|
|
366
|
+
}
|
|
367
|
+
res.end();
|
|
368
|
+
});${ws ? `
|
|
369
|
+
import("crossws/adapters/node").then(({ default: crossws }) => {
|
|
370
|
+
const ws = crossws({ hooks: __ws });
|
|
371
|
+
server.on("upgrade", (req, socket, head) => {
|
|
372
|
+
if ((__actionMatch)(req.url?.split("?")[0] ?? "")) ws.handleUpgrade(req, socket, head);
|
|
373
|
+
});
|
|
374
|
+
});` : ""}
|
|
375
|
+
server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
|
|
376
|
+
}
|
|
377
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
378
|
+
process.on(signal, () => {
|
|
379
|
+
server.close(() => process.exit(0));
|
|
380
|
+
setTimeout(() => process.exit(0), 5000).unref();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
` : "";
|
|
384
|
+
const actionImports = hasActions ? ws ? `import { createWsHooks } from "oxidejs/rpc";
|
|
385
|
+
import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
386
|
+
const __ws = createWsHooks(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin} });
|
|
387
|
+
` : `import { createActionHandler } from "oxidejs/rpc";
|
|
388
|
+
import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
389
|
+
const __fetch = Symbol.for("oxidejs.fetch");
|
|
390
|
+
const __rpc = createActionHandler(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin}, createContext: (req) => req[__fetch] ?? {} });
|
|
391
|
+
` : "";
|
|
392
|
+
const actionMatchFn = `const __actionMatch = (p) => p === ${JSON.stringify(actionPath)} || p === ${JSON.stringify(`${actionPath}/`)};`;
|
|
393
|
+
const actionGate = hasActions && !ws ? `if ((__actionMatch)(new URL(request.url).pathname)) {
|
|
394
|
+
return __rpc(request);
|
|
395
|
+
}
|
|
396
|
+
` : "";
|
|
397
|
+
const ILHA_SSR_IMPLICIT = ["ilha:pages/server", "ilha:loaders"];
|
|
398
|
+
const middlewareImports = (opts.middleware ?? []).map((m) => typeof m === "string" ? {
|
|
399
|
+
module: m,
|
|
400
|
+
imports: m === "@ilha/router/ssr" ? ILHA_SSR_IMPLICIT : []
|
|
401
|
+
} : 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";
|
|
402
|
+
const middlewareList = (opts.middleware ?? []).map((_, i) => `__mw${i}`).join(", ");
|
|
403
|
+
const middlewareGate = opts.middleware?.length ? `for (const __mw of [${middlewareList}]) {
|
|
404
|
+
const hit = await __mw(request, { env, ctx });
|
|
405
|
+
if (hit) return hit;
|
|
406
|
+
}
|
|
407
|
+
` : "";
|
|
408
|
+
return `${(opts.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}${preset === "celld" ? `import "oxidejs/worker-dom/install";\n` : ""}export * from ${JSON.stringify(userWorkerAbs)};
|
|
409
|
+
import user from ${JSON.stringify(userWorkerAbs)};
|
|
410
|
+
${middlewareImports}${actionImports}${hasActions ? `${actionMatchFn}\n` : ""}${assetBlock}${nfBlock}const app = {
|
|
411
|
+
...user,
|
|
412
|
+
async fetch(request, env, ctx) {
|
|
413
|
+
request[__fetch] = { env, fetchCtx: ctx };
|
|
414
|
+
${middlewareGate}${actionGate}${afterAction}
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
export default app;
|
|
418
|
+
${listen}`;
|
|
419
|
+
}
|
|
420
|
+
const SERVER_TARGETS = /* @__PURE__ */ new Set([
|
|
421
|
+
"node",
|
|
422
|
+
"async-node",
|
|
423
|
+
"webworker",
|
|
424
|
+
"web-worker"
|
|
425
|
+
]);
|
|
426
|
+
const CLIENT_TARGETS = /* @__PURE__ */ new Set(["web", "browserslist"]);
|
|
427
|
+
const SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
428
|
+
"server",
|
|
429
|
+
"ssr",
|
|
430
|
+
"worker",
|
|
431
|
+
"node"
|
|
432
|
+
]);
|
|
433
|
+
const CLIENT_NAMES = /* @__PURE__ */ new Set(["client", "web"]);
|
|
434
|
+
/** Stub unless the graph is a known server. Unknown graphs stub so *.server.ts never ships. */
|
|
435
|
+
function shouldStubServerModule(environment, extra) {
|
|
436
|
+
if (extra?.ssr) return false;
|
|
437
|
+
const consumer = environment?.config?.consumer ?? environment?.consumer;
|
|
438
|
+
if (consumer === "server") return false;
|
|
439
|
+
if (consumer === "client") return true;
|
|
440
|
+
const name = environment?.name;
|
|
441
|
+
if (name && CLIENT_NAMES.has(name)) return true;
|
|
442
|
+
if (name && SERVER_NAMES.has(name)) return false;
|
|
443
|
+
const targets = extra?.target == null ? [] : [extra.target].flat();
|
|
444
|
+
if (targets.some((target) => SERVER_TARGETS.has(target))) return false;
|
|
445
|
+
if (targets.some((target) => CLIENT_TARGETS.has(target))) return true;
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function pluginShouldStub(pluginThis, options) {
|
|
449
|
+
const ctx = pluginThis;
|
|
450
|
+
const compiler = ctx.getNativeBuildContext?.()?.compiler;
|
|
451
|
+
const env = {};
|
|
452
|
+
const name = ctx.environment?.name ?? compiler?.name ?? compiler?.options?.name;
|
|
453
|
+
if (name) env.name = name;
|
|
454
|
+
if (ctx.environment?.consumer) env.consumer = ctx.environment.consumer;
|
|
455
|
+
if (ctx.environment?.config) env.config = ctx.environment.config;
|
|
456
|
+
const extra = {};
|
|
457
|
+
if (options?.ssr) extra.ssr = true;
|
|
458
|
+
if (compiler?.options?.target) extra.target = compiler.options.target;
|
|
459
|
+
return shouldStubServerModule(env, extra);
|
|
460
|
+
}
|
|
461
|
+
function loadClientStub(id) {
|
|
462
|
+
const file = id.split("?")[0] ?? id;
|
|
463
|
+
const source = fs.readFileSync(file, "utf8");
|
|
464
|
+
return generateClientStub({
|
|
465
|
+
key: moduleKey(file),
|
|
466
|
+
exports: parseExportedNames(source),
|
|
467
|
+
streams: parseStreamExports(source)
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
var RequestBodyTooLargeError = class extends Error {};
|
|
471
|
+
async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
|
|
472
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
473
|
+
const headers = new Headers();
|
|
474
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
475
|
+
if (value === void 0) continue;
|
|
476
|
+
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
477
|
+
else headers.set(key, value);
|
|
478
|
+
}
|
|
479
|
+
const ac = new AbortController();
|
|
480
|
+
req.once("aborted", () => ac.abort());
|
|
481
|
+
const method = req.method ?? "GET";
|
|
482
|
+
const init = {
|
|
483
|
+
method,
|
|
484
|
+
headers,
|
|
485
|
+
signal: ac.signal
|
|
486
|
+
};
|
|
487
|
+
if (method === "GET" || method === "HEAD") return new Request(url, init);
|
|
488
|
+
const chunks = [];
|
|
489
|
+
let size = 0;
|
|
490
|
+
for await (const chunk of req) {
|
|
491
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
492
|
+
size += buffer.length;
|
|
493
|
+
if (size > maxBytes) throw new RequestBodyTooLargeError();
|
|
494
|
+
chunks.push(buffer);
|
|
495
|
+
}
|
|
496
|
+
const body = Buffer.concat(chunks);
|
|
497
|
+
if (body.length > 0) init.body = body;
|
|
498
|
+
return new Request(url, init);
|
|
499
|
+
}
|
|
500
|
+
async function sendWebResponseFrom(req, res, response) {
|
|
501
|
+
return pipeResponse(req, res, response);
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
export { scanServerFiles as C, pluginShouldStub as S, matchesActionPath as _, RequestBodyTooLargeError as a, parseExportedNames as b, VIRTUAL_WORKER_ID as c, generateActionsModule as d, generateClientModule as f, loadClientStub as g, isServerFileId as h, RESOLVED_VIRTUAL_WORKER_ID as i, actions_exports as l, generateWorkerWrapper as m, RESOLVED_VIRTUAL_ACTIONS_ID as n, VIRTUAL_ACTIONS_ID as o, generateClientStub as p, RESOLVED_VIRTUAL_CLIENT_ID as r, VIRTUAL_CLIENT_ID as s, ACTION_PATH as t, generateActionsClientModule as u, moduleKey as v, sendWebResponseFrom as w, parseStreamExports as x, nodeToWebRequest as y };
|