oxidejs 0.0.1 → 0.1.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.
- package/README.md +135 -0
- package/client.d.ts +10 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.mjs +2 -0
- package/dist/rsbuild.d.mts +5 -0
- package/dist/rsbuild.mjs +5 -0
- package/dist/src-B03PPyx9.mjs +760 -0
- package/dist/types-CFWyYfds.d.mts +52 -0
- package/dist/vite.d.mts +5 -0
- package/dist/vite.mjs +5 -0
- package/package.json +79 -4
- package/tsconfig.app.json +20 -0
- package/virtual.d.ts +10 -0
- package/bun.lock +0 -26
- package/index.ts +0 -1
- package/readme.md +0 -1
- package/tsconfig.json +0 -29
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# oxidejs
|
|
2
|
+
|
|
3
|
+
One build command → a deployable tree:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
dist/
|
|
7
|
+
├── client/ # only if index.html exists
|
|
8
|
+
└── server.js # ESM server bundle
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`preset: "celld"` also writes `dist/wrangler.jsonc` with `main: "./server.js"`.
|
|
12
|
+
|
|
13
|
+
v1 targets **Vite** and **Rsbuild** via unplugin. Other bundlers are out of scope for now.
|
|
14
|
+
|
|
15
|
+
## Vite
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// vite.config.ts
|
|
19
|
+
import { defineConfig } from "vite";
|
|
20
|
+
import oxide from "oxidejs/vite";
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [oxide()],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
// tsconfig.json
|
|
29
|
+
{ "extends": "oxidejs/tsconfig" }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
vite build
|
|
34
|
+
node dist/server.js
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Default preset is `"fetch"`. No `index.html` → only `dist/server.js`. With `index.html` → client to `dist/client/`, then `/_action` (if you have `*.server.ts`) → `src/server.ts` (`undefined` continues) → static file → `index.html` for navigations. `public/` is copied next to the client. Hashed assets get `Cache-Control: immutable`. No `wrangler.jsonc`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
oxide({
|
|
41
|
+
preset: "celld",
|
|
42
|
+
wrangler: { name: "my-app", compatibility_date: "2026-01-01" },
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`"celld"` writes `dist/wrangler.jsonc` for celld, a self-hosted alternative to Cloudflare Workers, and skips asset serving (`ASSETS` does that).
|
|
47
|
+
|
|
48
|
+
## Server actions
|
|
49
|
+
|
|
50
|
+
Install `tacho` if you use actions. Files named `*.server.ts` / `*.server.js` are server-only. A client import is replaced with a tacho stub that POSTs `/_action`. The original module never enters the client graph. Server and Vite SSR (`import.meta.env.SSR === true`) keep the real functions. Methods are `<file>.<fn>` (`test.ping`). Call `useRequest()` inside an action for the inbound `Request`. `useCtx()` is tacho `ctx` (`{ req }` plus anything middleware or `createContext` added). On `preset: "celld"`, `useEnv()` and `useFetchCtx()` are the Worker `env` and `ctx` from `fetch(request, env, ctx)` — same values as `useCtx().env` / `useCtx().fetchCtx`. Return `undefined` from `src/server.ts` to fall through to static files. No `*.server.ts` → the bundle does not import tacho.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// src/test.server.ts
|
|
54
|
+
import { useRequest } from "oxidejs";
|
|
55
|
+
|
|
56
|
+
export async function who() {
|
|
57
|
+
return useRequest().headers.get("x-user");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function ping() {
|
|
61
|
+
return "pong";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/client.ts
|
|
65
|
+
import { ping } from "./test.server";
|
|
66
|
+
console.log(await ping()); // "pong"
|
|
67
|
+
|
|
68
|
+
// src/server.ts
|
|
69
|
+
export default {
|
|
70
|
+
fetch(request: Request) {
|
|
71
|
+
if (new URL(request.url).pathname === "/api/ok") return new Response("ok");
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`async function*` exports stream over tacho SSE. `oxidejs/tsconfig` makes `await ticks()` typecheck. Pass `{ signal }` last on any action to abort the fetch. Types come from the real `*.server.ts`, so declare the last argument there:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/test.server.ts
|
|
80
|
+
import type { ActionOptions } from "oxidejs";
|
|
81
|
+
|
|
82
|
+
export async function* ticks(n: number, _opts?: ActionOptions) {
|
|
83
|
+
for (let i = 0; i < n; i++) yield i;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/client.ts
|
|
87
|
+
import { ticks } from "./test.server";
|
|
88
|
+
|
|
89
|
+
const ac = new AbortController();
|
|
90
|
+
const stream = await ticks(10, { signal: ac.signal });
|
|
91
|
+
ac.abort();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`vite dev` and `rsbuild dev` serve `/_action` via middleware. `oxide({ actions: "ws" })` uses a WebSocket instead (needs `crossws`; not with `preset: "celld"`). `actionHeaders` are static headers on the shared HTTP client.
|
|
95
|
+
|
|
96
|
+
## Rsbuild
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// rsbuild.config.ts
|
|
100
|
+
import { defineConfig } from "@rsbuild/core";
|
|
101
|
+
import oxide from "oxidejs/rsbuild";
|
|
102
|
+
|
|
103
|
+
export default defineConfig({
|
|
104
|
+
plugins: [oxide()],
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Same factory as Vite: client stubs, `/_action`, and `dist/server.js`.
|
|
109
|
+
|
|
110
|
+
## Options
|
|
111
|
+
|
|
112
|
+
| Option | Default | Notes |
|
|
113
|
+
| ------------------------------ | ------------------------ | -------------------------------------- |
|
|
114
|
+
| `preset` | `"fetch"` | `"fetch"` or `"celld"` |
|
|
115
|
+
| `workerEntry` | `src/server.ts` | Relative to project root |
|
|
116
|
+
| `outDir` | `dist` | Output root |
|
|
117
|
+
| `clientDir` | `client` | Must stay inside `outDir` |
|
|
118
|
+
| `wrangler.name` | required if `emitConfig` | |
|
|
119
|
+
| `wrangler.compatibility_date` | required if `emitConfig` | |
|
|
120
|
+
| `wrangler.compatibility_flags` | — | optional |
|
|
121
|
+
| `wrangler.durable_objects` | — | optional |
|
|
122
|
+
| `wrangler.migrations` | — | optional |
|
|
123
|
+
| `wrangler.services` | — | optional |
|
|
124
|
+
| `wrangler.vars` | — | optional |
|
|
125
|
+
| `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
|
|
126
|
+
| `actions` | `"http"` | `"ws"` needs `crossws`; not with celld |
|
|
127
|
+
| `actionHeaders` | — | Static headers on the HTTP client |
|
|
128
|
+
|
|
129
|
+
`main` is always `./server.js`. `assets` is added only when `index.html` exists. Unknown wrangler keys fail at build time.
|
|
130
|
+
|
|
131
|
+
## Non-goals
|
|
132
|
+
|
|
133
|
+
- No `wrangler dev` / workerd emulation
|
|
134
|
+
- No automatic `celld deploy`
|
|
135
|
+
- No Node-builtin polyfills — Vite `ssr.noExternal: true` is a hard-fail for stray Node imports
|
package/client.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface AsyncGenerator<T = unknown, TReturn = any, TNext = any> extends AsyncIteratorObject<
|
|
2
|
+
T,
|
|
3
|
+
TReturn,
|
|
4
|
+
TNext
|
|
5
|
+
> {
|
|
6
|
+
then<TResult1 = AsyncIterable<T>, TResult2 = never>(
|
|
7
|
+
onfulfilled?: ((value: AsyncIterable<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
|
8
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
|
9
|
+
): Promise<TResult1 | TResult2>;
|
|
10
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-CFWyYfds.mjs";
|
|
2
|
+
import { UnpluginFactory } from "unplugin";
|
|
3
|
+
//#region src/context.d.ts
|
|
4
|
+
type ExecutionContext = {
|
|
5
|
+
waitUntil?(promise: Promise<unknown>): void;
|
|
6
|
+
passThroughOnException?(): void;
|
|
7
|
+
};
|
|
8
|
+
/** Tacho procedure `ctx`. Starts as `{ req }` plus Worker extras. Middleware can add fields. */
|
|
9
|
+
type ActionContext = {
|
|
10
|
+
req: Request;
|
|
11
|
+
env?: unknown;
|
|
12
|
+
fetchCtx?: ExecutionContext;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
/** Current tacho `ctx`. Throws outside `*.server.ts` running over `/_action`. */
|
|
16
|
+
declare function useCtx<C extends ActionContext = ActionContext>(): C;
|
|
17
|
+
/** Current action `Request`. Throws outside `*.server.ts` running over `/_action`. */
|
|
18
|
+
declare function useRequest(): Request;
|
|
19
|
+
/** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
|
|
20
|
+
declare function useEnv<E = unknown>(): E | undefined;
|
|
21
|
+
/** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
|
|
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
|
+
//#endregion
|
|
28
|
+
//#region src/index.d.ts
|
|
29
|
+
declare const unpluginFactory: UnpluginFactory<OxidejsOptions | undefined>;
|
|
30
|
+
declare const oxidejs: import("unplugin").UnpluginInstance<OxidejsOptions | undefined, boolean>;
|
|
31
|
+
declare const vite: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type ActionContext, type ActionOptions, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
|
package/dist/index.mjs
ADDED
package/dist/rsbuild.mjs
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { createUnplugin } from "unplugin";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
//#region src/actions.ts
|
|
7
|
+
const VIRTUAL_ACTIONS_ID = "virtual:oxide/actions";
|
|
8
|
+
const RESOLVED_VIRTUAL_ACTIONS_ID = `\0${VIRTUAL_ACTIONS_ID}`;
|
|
9
|
+
const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
|
|
10
|
+
const RESOLVED_VIRTUAL_WORKER_ID = `\0${VIRTUAL_WORKER_ID}`;
|
|
11
|
+
const VIRTUAL_CLIENT_ID = "virtual:oxide/client";
|
|
12
|
+
const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
|
|
13
|
+
const ACTION_PATH = "/_action";
|
|
14
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
".git",
|
|
18
|
+
".wrangler"
|
|
19
|
+
]);
|
|
20
|
+
const EXPORT_RE = /^\s*export\s+(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)|^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=/gm;
|
|
21
|
+
function isServerFileId(id) {
|
|
22
|
+
const file = id.split("?")[0]?.replace(/\\/g, "/") ?? "";
|
|
23
|
+
return file.endsWith(".server.ts") || file.endsWith(".server.js");
|
|
24
|
+
}
|
|
25
|
+
function moduleKey(absFile) {
|
|
26
|
+
return path.basename(absFile).replace(/\.server\.(ts|js)$/i, "");
|
|
27
|
+
}
|
|
28
|
+
function parseExportedNames(source) {
|
|
29
|
+
const names = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const match of source.matchAll(EXPORT_RE)) {
|
|
31
|
+
const name = match[1] ?? match[2];
|
|
32
|
+
if (name) names.add(name);
|
|
33
|
+
}
|
|
34
|
+
return [...names];
|
|
35
|
+
}
|
|
36
|
+
function scanServerFiles(root) {
|
|
37
|
+
const files = [];
|
|
38
|
+
const walk = (dir) => {
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.name.startsWith(".")) continue;
|
|
47
|
+
const abs = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
50
|
+
walk(abs);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (entry.isFile() && isServerFileId(entry.name)) files.push(abs);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(root);
|
|
57
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
58
|
+
const modules = [];
|
|
59
|
+
for (const abs of files.sort()) {
|
|
60
|
+
const key = moduleKey(abs);
|
|
61
|
+
if (!key) throw new Error(`oxidejs: invalid server module name: ${abs}`);
|
|
62
|
+
const existing = byKey.get(key);
|
|
63
|
+
if (existing) throw new Error(`oxidejs: duplicate server module key "${key}": ${existing} and ${abs}`);
|
|
64
|
+
byKey.set(key, abs);
|
|
65
|
+
modules.push({
|
|
66
|
+
abs,
|
|
67
|
+
key,
|
|
68
|
+
exports: parseExportedNames(fs.readFileSync(abs, "utf8"))
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return modules;
|
|
72
|
+
}
|
|
73
|
+
function generateClientModule(transport = "http", headers) {
|
|
74
|
+
if (transport === "ws") return `import { createClient } from "tacho/client/ws";
|
|
75
|
+
const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
|
|
76
|
+
const __host = typeof location === "undefined" ? "localhost" : location.host;
|
|
77
|
+
export const client = createClient({ url: __proto + "//" + __host + ${JSON.stringify(ACTION_PATH)} });
|
|
78
|
+
`;
|
|
79
|
+
const opts = { url: ACTION_PATH };
|
|
80
|
+
if (headers) opts.headers = headers;
|
|
81
|
+
return `import { createClient } from "tacho/client/http";
|
|
82
|
+
export const client = createClient(${JSON.stringify(opts)});
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function generateClientStub(mod) {
|
|
86
|
+
const lines = [`import { client } from ${JSON.stringify(VIRTUAL_CLIENT_ID)};`];
|
|
87
|
+
for (const name of mod.exports) lines.push(`export const ${name} = (...args) => {
|
|
88
|
+
const opts = args.at(-1);
|
|
89
|
+
// ponytail: peel last { signal } only. A lone payload { signal: AbortSignal } is treated as CallOptions.
|
|
90
|
+
return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1
|
|
91
|
+
? client[${JSON.stringify(mod.key)}][${JSON.stringify(name)}](args.slice(0, -1), opts)
|
|
92
|
+
: client[${JSON.stringify(mod.key)}][${JSON.stringify(name)}](args);
|
|
93
|
+
};`);
|
|
94
|
+
return `${lines.join("\n")}\n`;
|
|
95
|
+
}
|
|
96
|
+
function generateActionsModule(modules, opts) {
|
|
97
|
+
const lines = [
|
|
98
|
+
`import { AsyncLocalStorage } from "node:async_hooks";`,
|
|
99
|
+
`import { tacho } from "tacho";`,
|
|
100
|
+
`const __als = globalThis.__oxidejsRequest ??= new AsyncLocalStorage();`
|
|
101
|
+
];
|
|
102
|
+
const aliases = modules.map((mod, i) => {
|
|
103
|
+
const alias = `__m${i}`;
|
|
104
|
+
const spec = opts?.bust === true ? `${mod.abs}?t=${fs.statSync(mod.abs).mtimeMs}` : mod.abs;
|
|
105
|
+
lines.push(`import * as ${alias} from ${JSON.stringify(spec)};`);
|
|
106
|
+
return {
|
|
107
|
+
alias,
|
|
108
|
+
mod
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
lines.push(`const rpc = tacho();`);
|
|
112
|
+
lines.push(`const actions = rpc({`);
|
|
113
|
+
for (const { alias, mod } of aliases) {
|
|
114
|
+
lines.push(` ${JSON.stringify(mod.key)}: {`);
|
|
115
|
+
for (const name of mod.exports) lines.push(` ${JSON.stringify(name)}: rpc.run(({ input, ctx }) => __als.run(ctx, () => {
|
|
116
|
+
const out = ${alias}[${JSON.stringify(name)}].apply(null, Array.isArray(input) ? input : []);
|
|
117
|
+
if (!out || typeof out !== "object" || typeof out.next !== "function") return out;
|
|
118
|
+
return {
|
|
119
|
+
next: (v) => __als.run(ctx, () => out.next(v)),
|
|
120
|
+
return: (v) => __als.run(ctx, () => out.return(v)),
|
|
121
|
+
throw: (e) => __als.run(ctx, () => out.throw(e)),
|
|
122
|
+
[Symbol.asyncIterator]() { return this; },
|
|
123
|
+
};
|
|
124
|
+
})),`);
|
|
125
|
+
lines.push(` },`);
|
|
126
|
+
}
|
|
127
|
+
lines.push(`});`);
|
|
128
|
+
lines.push(`export default actions;`);
|
|
129
|
+
lines.push(`export { actions };`);
|
|
130
|
+
return `${lines.join("\n")}\n`;
|
|
131
|
+
}
|
|
132
|
+
function pipeResponse(req, res, response) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
res.statusCode = response.status;
|
|
135
|
+
response.headers.forEach((value, key) => {
|
|
136
|
+
res.setHeader(key, value);
|
|
137
|
+
});
|
|
138
|
+
if (!response.body) {
|
|
139
|
+
res.end();
|
|
140
|
+
resolve();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const reader = response.body.getReader();
|
|
144
|
+
const abort = () => {
|
|
145
|
+
reader.cancel();
|
|
146
|
+
};
|
|
147
|
+
req.once("aborted", abort);
|
|
148
|
+
const pull = () => {
|
|
149
|
+
reader.read().then(({ done, value }) => {
|
|
150
|
+
if (done) {
|
|
151
|
+
req.off("aborted", abort);
|
|
152
|
+
res.end();
|
|
153
|
+
resolve();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (value) res.write(value);
|
|
157
|
+
pull();
|
|
158
|
+
}, reject);
|
|
159
|
+
};
|
|
160
|
+
pull();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function generateWorkerWrapper(userWorkerAbs, opts = {}) {
|
|
164
|
+
const preset = opts.preset ?? "fetch";
|
|
165
|
+
const clientDir = opts.clientDir ?? "client";
|
|
166
|
+
const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
|
|
167
|
+
const hasActions = opts.hasActions !== false;
|
|
168
|
+
const ws = hasActions && opts.actions === "ws";
|
|
169
|
+
const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
|
|
170
|
+
import { extname, join } from "node:path";
|
|
171
|
+
const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
|
|
172
|
+
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" };
|
|
173
|
+
function __nav(request) {
|
|
174
|
+
const dest = request.headers.get("sec-fetch-dest");
|
|
175
|
+
if (dest) return dest === "document";
|
|
176
|
+
return (request.headers.get("accept") ?? "").includes("text/html");
|
|
177
|
+
}
|
|
178
|
+
function __cache(file) {
|
|
179
|
+
if (file === "index.html") return "no-cache";
|
|
180
|
+
return /[-.][0-9a-f]{8,}.[a-z0-9]+$/i.test(file) ? "public, max-age=31536000, immutable" : undefined;
|
|
181
|
+
}
|
|
182
|
+
function __rel(pathname, spa) {
|
|
183
|
+
if (pathname.includes("\0")) return;
|
|
184
|
+
let file;
|
|
185
|
+
try { file = decodeURIComponent(pathname); } catch { return; }
|
|
186
|
+
if (file.includes("\0")) return;
|
|
187
|
+
if (file === "/" || spa) file = "/index.html";
|
|
188
|
+
if (!file.startsWith("/") || file.split("/").includes("..")) return;
|
|
189
|
+
return file.slice(1);
|
|
190
|
+
}
|
|
191
|
+
async function __asset(request, spa) {
|
|
192
|
+
const file = __rel(new URL(request.url).pathname, spa);
|
|
193
|
+
if (!file) return;
|
|
194
|
+
try {
|
|
195
|
+
const body = await readFile(join(__assets, file));
|
|
196
|
+
const headers = { "content-type": __types[extname(file)] ?? "application/octet-stream" };
|
|
197
|
+
const cache = __cache(file);
|
|
198
|
+
if (cache) headers["cache-control"] = cache;
|
|
199
|
+
return new Response(body, { headers });
|
|
200
|
+
} catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
` : "";
|
|
205
|
+
const afterAction = serveAssets ? `if (typeof user.fetch === "function") {
|
|
206
|
+
const hit = await user.fetch(request, env, ctx);
|
|
207
|
+
if (hit) return hit;
|
|
208
|
+
}
|
|
209
|
+
return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? new Response("Not Found", { status: 404 });` : `return typeof user.fetch === "function"
|
|
210
|
+
? user.fetch(request, env, ctx)
|
|
211
|
+
: new Response("Not Found", { status: 404 });`;
|
|
212
|
+
const listen = preset === "fetch" ? `
|
|
213
|
+
import { createServer } from "node:http";
|
|
214
|
+
import { pathToFileURL } from "node:url";
|
|
215
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
216
|
+
const port = Number(process.env.PORT) || 3000;
|
|
217
|
+
const server = createServer(async (req, res) => {
|
|
218
|
+
const url = \`http://\${req.headers.host ?? "localhost"}\${req.url ?? "/"}\`;
|
|
219
|
+
const headers = new Headers();
|
|
220
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
221
|
+
if (value === undefined) continue;
|
|
222
|
+
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
223
|
+
else headers.set(key, value);
|
|
224
|
+
}
|
|
225
|
+
const ac = new AbortController();
|
|
226
|
+
req.once("aborted", () => ac.abort());
|
|
227
|
+
const method = req.method ?? "GET";
|
|
228
|
+
const chunks = [];
|
|
229
|
+
if (method !== "GET" && method !== "HEAD") for await (const chunk of req) chunks.push(chunk);
|
|
230
|
+
const init = { method, headers, signal: ac.signal };
|
|
231
|
+
if (chunks.length) init.body = Buffer.concat(chunks);
|
|
232
|
+
const response = await app.fetch(new Request(url, init));
|
|
233
|
+
res.statusCode = response.status;
|
|
234
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
235
|
+
if (!response.body) { res.end(); return; }
|
|
236
|
+
const reader = response.body.getReader();
|
|
237
|
+
for (;;) {
|
|
238
|
+
const { done, value } = await reader.read();
|
|
239
|
+
if (done) break;
|
|
240
|
+
if (value) res.write(value);
|
|
241
|
+
}
|
|
242
|
+
res.end();
|
|
243
|
+
});${ws ? `
|
|
244
|
+
import("crossws/adapters/node").then(({ default: crossws }) => {
|
|
245
|
+
const ws = crossws({ hooks: __ws });
|
|
246
|
+
server.on("upgrade", (req, socket, head) => {
|
|
247
|
+
if (req.url?.split("?")[0] === ${JSON.stringify(ACTION_PATH)}) ws.handleUpgrade(req, socket, head);
|
|
248
|
+
});
|
|
249
|
+
});` : ""}
|
|
250
|
+
server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
|
|
251
|
+
}
|
|
252
|
+
` : "";
|
|
253
|
+
const actionImports = hasActions ? ws ? `import { handle as handleWs } from "tacho/transport/ws";
|
|
254
|
+
import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
255
|
+
const __ws = handleWs(actions, { path: ${JSON.stringify(ACTION_PATH)} });
|
|
256
|
+
` : `import { handle } from "tacho/transport/fetch";
|
|
257
|
+
import actions from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
258
|
+
const __fetch = Symbol.for("oxidejs.fetch");
|
|
259
|
+
const __rpc = handle(actions, { path: ${JSON.stringify(ACTION_PATH)}, createContext: (req) => req[__fetch] ?? {} });
|
|
260
|
+
` : "";
|
|
261
|
+
const actionGate = hasActions && !ws ? `if (new URL(request.url).pathname === ${JSON.stringify(ACTION_PATH)}) {
|
|
262
|
+
request[__fetch] = { env, fetchCtx: ctx };
|
|
263
|
+
return __rpc(request);
|
|
264
|
+
}
|
|
265
|
+
` : "";
|
|
266
|
+
return `export * from ${JSON.stringify(userWorkerAbs)};
|
|
267
|
+
import user from ${JSON.stringify(userWorkerAbs)};
|
|
268
|
+
${actionImports}${assetBlock}const app = {
|
|
269
|
+
...user,
|
|
270
|
+
async fetch(request, env, ctx) {
|
|
271
|
+
${actionGate}${afterAction}
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
export default app;
|
|
275
|
+
${listen}`;
|
|
276
|
+
}
|
|
277
|
+
const SERVER_TARGETS = /* @__PURE__ */ new Set([
|
|
278
|
+
"node",
|
|
279
|
+
"async-node",
|
|
280
|
+
"webworker",
|
|
281
|
+
"web-worker"
|
|
282
|
+
]);
|
|
283
|
+
const CLIENT_TARGETS = /* @__PURE__ */ new Set(["web", "browserslist"]);
|
|
284
|
+
const SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
285
|
+
"server",
|
|
286
|
+
"ssr",
|
|
287
|
+
"worker",
|
|
288
|
+
"node"
|
|
289
|
+
]);
|
|
290
|
+
const CLIENT_NAMES = /* @__PURE__ */ new Set(["client", "web"]);
|
|
291
|
+
/** Stub unless the graph is a known server. Unknown graphs stub so *.server.ts never ships. */
|
|
292
|
+
function shouldStubServerModule(environment, extra) {
|
|
293
|
+
if (extra?.ssr) return false;
|
|
294
|
+
const consumer = environment?.config?.consumer ?? environment?.consumer;
|
|
295
|
+
if (consumer === "server") return false;
|
|
296
|
+
if (consumer === "client") return true;
|
|
297
|
+
const name = environment?.name;
|
|
298
|
+
if (name && CLIENT_NAMES.has(name)) return true;
|
|
299
|
+
if (name && SERVER_NAMES.has(name)) return false;
|
|
300
|
+
const targets = extra?.target == null ? [] : [extra.target].flat();
|
|
301
|
+
if (targets.some((target) => SERVER_TARGETS.has(target))) return false;
|
|
302
|
+
if (targets.some((target) => CLIENT_TARGETS.has(target))) return true;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
function pluginShouldStub(pluginThis, options) {
|
|
306
|
+
const ctx = pluginThis;
|
|
307
|
+
const compiler = ctx.getNativeBuildContext?.()?.compiler;
|
|
308
|
+
const env = {};
|
|
309
|
+
const name = ctx.environment?.name ?? compiler?.name ?? compiler?.options?.name;
|
|
310
|
+
if (name) env.name = name;
|
|
311
|
+
if (ctx.environment?.consumer) env.consumer = ctx.environment.consumer;
|
|
312
|
+
if (ctx.environment?.config) env.config = ctx.environment.config;
|
|
313
|
+
const extra = {};
|
|
314
|
+
if (options?.ssr) extra.ssr = true;
|
|
315
|
+
if (compiler?.options?.target) extra.target = compiler.options.target;
|
|
316
|
+
return shouldStubServerModule(env, extra);
|
|
317
|
+
}
|
|
318
|
+
function loadClientStub(id) {
|
|
319
|
+
const file = id.split("?")[0] ?? id;
|
|
320
|
+
return generateClientStub({
|
|
321
|
+
key: moduleKey(file),
|
|
322
|
+
exports: parseExportedNames(fs.readFileSync(file, "utf8"))
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function nodeToWebRequest(req) {
|
|
326
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
327
|
+
const headers = new Headers();
|
|
328
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
329
|
+
if (value === void 0) continue;
|
|
330
|
+
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
331
|
+
else headers.set(key, value);
|
|
332
|
+
}
|
|
333
|
+
const ac = new AbortController();
|
|
334
|
+
req.once("aborted", () => ac.abort());
|
|
335
|
+
const method = req.method ?? "GET";
|
|
336
|
+
const init = {
|
|
337
|
+
method,
|
|
338
|
+
headers,
|
|
339
|
+
signal: ac.signal
|
|
340
|
+
};
|
|
341
|
+
if (method === "GET" || method === "HEAD") return new Request(url, init);
|
|
342
|
+
const chunks = [];
|
|
343
|
+
for await (const chunk of req) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
344
|
+
const body = Buffer.concat(chunks);
|
|
345
|
+
if (body.length > 0) init.body = body;
|
|
346
|
+
return new Request(url, init);
|
|
347
|
+
}
|
|
348
|
+
async function sendWebResponseFrom(req, res, response) {
|
|
349
|
+
return pipeResponse(req, res, response);
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/core.ts
|
|
353
|
+
const CELLD_ALLOWED_KEYS = [
|
|
354
|
+
"name",
|
|
355
|
+
"main",
|
|
356
|
+
"compatibility_date",
|
|
357
|
+
"compatibility_flags",
|
|
358
|
+
"durable_objects",
|
|
359
|
+
"migrations",
|
|
360
|
+
"assets",
|
|
361
|
+
"services",
|
|
362
|
+
"vars"
|
|
363
|
+
];
|
|
364
|
+
const USER_FORBIDDEN_KEYS = ["main", "assets"];
|
|
365
|
+
function createEmitState() {
|
|
366
|
+
return { emitted: false };
|
|
367
|
+
}
|
|
368
|
+
function validateWranglerOptions(wrangler) {
|
|
369
|
+
const allowed = CELLD_ALLOWED_KEYS;
|
|
370
|
+
const invalid = Object.keys(wrangler).filter((key) => !allowed.includes(key));
|
|
371
|
+
if (invalid.length) throw new Error(`oxidejs: these wrangler keys are not supported by celld deploy: ${invalid.join(", ")}`);
|
|
372
|
+
const forbidden = USER_FORBIDDEN_KEYS.filter((key) => key in wrangler);
|
|
373
|
+
if (forbidden.length) throw new Error(`oxidejs: wrangler keys ${forbidden.join(", ")} are computed by the plugin and cannot be user-supplied`);
|
|
374
|
+
}
|
|
375
|
+
function assertContained(outDirAbs, childAbs, label) {
|
|
376
|
+
const outDir = path.resolve(outDirAbs);
|
|
377
|
+
const child = path.resolve(childAbs);
|
|
378
|
+
const relative = path.relative(outDir, child);
|
|
379
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`oxidejs: ${label} must resolve inside outDir (got ${relative || "."})`);
|
|
380
|
+
}
|
|
381
|
+
function requireWranglerFields(wrangler) {
|
|
382
|
+
if (!wrangler?.name || !wrangler.compatibility_date) throw new Error("oxidejs: wrangler.name and wrangler.compatibility_date are required when emitConfig is true");
|
|
383
|
+
return wrangler;
|
|
384
|
+
}
|
|
385
|
+
function flattenInput(input) {
|
|
386
|
+
if (!input) return [];
|
|
387
|
+
if (typeof input === "string") return [input];
|
|
388
|
+
if (Array.isArray(input)) return input;
|
|
389
|
+
return Object.values(input);
|
|
390
|
+
}
|
|
391
|
+
function envInput(env) {
|
|
392
|
+
if (!env || typeof env !== "object") return;
|
|
393
|
+
const rec = env;
|
|
394
|
+
return rec.build?.rolldownOptions?.input || rec.build?.rollupOptions?.input || rec.build?.input || rec.input;
|
|
395
|
+
}
|
|
396
|
+
/** Vite client input: rolldown/rollup `input`, else `path.resolve(root, "index.html")`. */
|
|
397
|
+
function hasHtmlEntry(root, config) {
|
|
398
|
+
const cfg = config;
|
|
399
|
+
return flattenInput(envInput(cfg?.environments?.["client"]) || envInput(cfg?.environments?.["web"]) || cfg?.build?.rolldownOptions?.input || cfg?.build?.rollupOptions?.input || path.resolve(root, "index.html")).some((entry) => {
|
|
400
|
+
const file = path.resolve(root, entry);
|
|
401
|
+
return file.endsWith(".html") && fs.existsSync(file);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function resolveOptions(raw, root, config) {
|
|
405
|
+
const preset = raw?.preset ?? "fetch";
|
|
406
|
+
if (preset !== "fetch" && preset !== "celld") throw new Error(`oxidejs: unknown preset "${String(preset)}"`);
|
|
407
|
+
const actions = raw?.actions ?? "http";
|
|
408
|
+
if (actions !== "http" && actions !== "ws") throw new Error(`oxidejs: unknown actions transport "${String(actions)}"`);
|
|
409
|
+
if (actions === "ws" && preset === "celld") throw new Error("oxidejs: actions: \"ws\" is not supported with preset: \"celld\"");
|
|
410
|
+
const workerEntry = raw?.workerEntry ?? "src/server.ts";
|
|
411
|
+
const outDirInput = raw?.outDir ?? "dist";
|
|
412
|
+
const clientDir = raw?.clientDir ?? "client";
|
|
413
|
+
const emitConfig = raw?.emitConfig ?? preset === "celld";
|
|
414
|
+
const rootAbs = path.resolve(root);
|
|
415
|
+
const outDir = path.resolve(rootAbs, outDirInput);
|
|
416
|
+
const workerEntryAbs = path.resolve(rootAbs, workerEntry);
|
|
417
|
+
const hasClient = hasHtmlEntry(rootAbs, config);
|
|
418
|
+
const hasPublic = fs.existsSync(path.join(rootAbs, "public"));
|
|
419
|
+
if (hasClient || hasPublic) assertContained(outDir, path.resolve(outDir, clientDir), "clientDir");
|
|
420
|
+
if (raw?.wrangler) validateWranglerOptions(raw.wrangler);
|
|
421
|
+
return {
|
|
422
|
+
root: rootAbs,
|
|
423
|
+
preset,
|
|
424
|
+
workerEntry,
|
|
425
|
+
workerEntryAbs,
|
|
426
|
+
outDir,
|
|
427
|
+
clientDir,
|
|
428
|
+
wrangler: emitConfig ? requireWranglerFields(raw?.wrangler) : raw?.wrangler,
|
|
429
|
+
emitConfig,
|
|
430
|
+
hasClient,
|
|
431
|
+
hasPublic,
|
|
432
|
+
actions,
|
|
433
|
+
actionHeaders: raw?.actionHeaders
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function copyPublicDir(opts) {
|
|
437
|
+
if (opts.preset !== "fetch") return;
|
|
438
|
+
const src = path.join(opts.root, "public");
|
|
439
|
+
if (!fs.existsSync(src)) return;
|
|
440
|
+
fs.cpSync(src, path.join(opts.outDir, opts.clientDir), {
|
|
441
|
+
recursive: true,
|
|
442
|
+
force: true
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
function tryEmitWranglerConfig(opts, state) {
|
|
446
|
+
if (state.emitted || opts.emitConfig === false) return;
|
|
447
|
+
const wrangler = requireWranglerFields(opts.wrangler);
|
|
448
|
+
const serverFile = path.join(opts.outDir, "server.js");
|
|
449
|
+
const clientDirPath = path.join(opts.outDir, opts.clientDir);
|
|
450
|
+
if (!fs.existsSync(serverFile)) return;
|
|
451
|
+
if (opts.hasClient && !fs.existsSync(clientDirPath)) return;
|
|
452
|
+
assertContained(opts.outDir, serverFile, "main");
|
|
453
|
+
if (opts.hasClient) assertContained(opts.outDir, clientDirPath, "assets.directory");
|
|
454
|
+
const config = {
|
|
455
|
+
name: wrangler.name,
|
|
456
|
+
main: "./server.js",
|
|
457
|
+
compatibility_date: wrangler.compatibility_date,
|
|
458
|
+
...wrangler.compatibility_flags ? { compatibility_flags: wrangler.compatibility_flags } : {},
|
|
459
|
+
...wrangler.durable_objects ? { durable_objects: wrangler.durable_objects } : {},
|
|
460
|
+
...wrangler.migrations ? { migrations: wrangler.migrations } : {},
|
|
461
|
+
...wrangler.services ? { services: wrangler.services } : {},
|
|
462
|
+
...wrangler.vars ? { vars: wrangler.vars } : {},
|
|
463
|
+
...opts.hasClient ? { assets: {
|
|
464
|
+
directory: `./${opts.clientDir}`,
|
|
465
|
+
binding: "ASSETS"
|
|
466
|
+
} } : {}
|
|
467
|
+
};
|
|
468
|
+
fs.writeFileSync(path.join(opts.outDir, "wrangler.jsonc"), `${JSON.stringify(config, null, 2)}\n`);
|
|
469
|
+
state.emitted = true;
|
|
470
|
+
}
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/worker-build.ts
|
|
473
|
+
function applyViteEnvironments(config, opts) {
|
|
474
|
+
config.builder ??= {};
|
|
475
|
+
config.environments ??= {};
|
|
476
|
+
const celld = opts.preset === "celld";
|
|
477
|
+
config.environments["ssr"] = {
|
|
478
|
+
consumer: "server",
|
|
479
|
+
build: {
|
|
480
|
+
outDir: opts.outDir,
|
|
481
|
+
emptyOutDir: true,
|
|
482
|
+
ssr: true,
|
|
483
|
+
rolldownOptions: {
|
|
484
|
+
input: VIRTUAL_WORKER_ID,
|
|
485
|
+
external: celld ? [/^cloudflare:/] : [],
|
|
486
|
+
output: {
|
|
487
|
+
format: "es",
|
|
488
|
+
entryFileNames: "server.js"
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
rollupOptions: {
|
|
492
|
+
input: VIRTUAL_WORKER_ID,
|
|
493
|
+
external: celld ? [/^cloudflare:/] : [],
|
|
494
|
+
output: {
|
|
495
|
+
format: "es",
|
|
496
|
+
entryFileNames: "server.js"
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
resolve: celld ? {
|
|
501
|
+
conditions: ["worker"],
|
|
502
|
+
noExternal: true
|
|
503
|
+
} : { noExternal: true },
|
|
504
|
+
ssr: celld ? {
|
|
505
|
+
target: "webworker",
|
|
506
|
+
noExternal: true,
|
|
507
|
+
external: [/^cloudflare:/]
|
|
508
|
+
} : { noExternal: true }
|
|
509
|
+
};
|
|
510
|
+
config.build ??= {};
|
|
511
|
+
if (opts.hasClient) {
|
|
512
|
+
const clientOutDir = path.join(opts.outDir, opts.clientDir);
|
|
513
|
+
const existingClient = config.environments["client"];
|
|
514
|
+
config.environments["client"] = {
|
|
515
|
+
...existingClient,
|
|
516
|
+
consumer: "client",
|
|
517
|
+
build: {
|
|
518
|
+
...existingClient?.build,
|
|
519
|
+
outDir: clientOutDir,
|
|
520
|
+
emptyOutDir: true,
|
|
521
|
+
manifest: true
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
config.environments["ssr"].build.emptyOutDir = false;
|
|
525
|
+
config.build.outDir ??= clientOutDir;
|
|
526
|
+
config.build.manifest ??= true;
|
|
527
|
+
} else {
|
|
528
|
+
delete config.environments["client"];
|
|
529
|
+
config.appType = "custom";
|
|
530
|
+
config.build.outDir ??= opts.outDir;
|
|
531
|
+
config.build.emptyOutDir ??= true;
|
|
532
|
+
config.builder.buildApp ??= async (builder) => {
|
|
533
|
+
const server = builder.environments["ssr"];
|
|
534
|
+
if (server) await builder.build(server);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return config;
|
|
538
|
+
}
|
|
539
|
+
function applyRsbuildEnvironments(config, opts) {
|
|
540
|
+
config.environments ??= {};
|
|
541
|
+
if (opts.hasClient) {
|
|
542
|
+
const clientOutDir = path.join(opts.outDir, opts.clientDir);
|
|
543
|
+
const existingClient = config.environments["web"] ?? config.environments["client"];
|
|
544
|
+
config.environments["web"] = {
|
|
545
|
+
...existingClient,
|
|
546
|
+
output: {
|
|
547
|
+
...existingClient?.output,
|
|
548
|
+
target: "web",
|
|
549
|
+
distPath: {
|
|
550
|
+
...existingClient?.output?.distPath,
|
|
551
|
+
root: clientOutDir
|
|
552
|
+
},
|
|
553
|
+
manifest: true
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
} else {
|
|
557
|
+
delete config.environments["web"];
|
|
558
|
+
delete config.environments["client"];
|
|
559
|
+
}
|
|
560
|
+
const server = {
|
|
561
|
+
source: { entry: { server: {
|
|
562
|
+
import: VIRTUAL_WORKER_ID,
|
|
563
|
+
html: false
|
|
564
|
+
} } },
|
|
565
|
+
output: {
|
|
566
|
+
target: opts.preset === "celld" ? "web-worker" : "node",
|
|
567
|
+
filename: { js: "server.js" },
|
|
568
|
+
distPath: { root: opts.outDir }
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
if (opts.preset === "celld") server.resolve = { conditionNames: ["worker", "..."] };
|
|
572
|
+
config.environments["server"] = server;
|
|
573
|
+
return config;
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/context.ts
|
|
577
|
+
const ALS_KEY = "__oxidejsRequest";
|
|
578
|
+
function als() {
|
|
579
|
+
const g = globalThis;
|
|
580
|
+
return g[ALS_KEY] ??= new AsyncLocalStorage();
|
|
581
|
+
}
|
|
582
|
+
function store() {
|
|
583
|
+
const current = als().getStore();
|
|
584
|
+
if (!current) throw new Error("oxidejs: useRequest() called outside an action");
|
|
585
|
+
return current;
|
|
586
|
+
}
|
|
587
|
+
/** Current tacho `ctx`. Throws outside `*.server.ts` running over `/_action`. */
|
|
588
|
+
function useCtx() {
|
|
589
|
+
return store();
|
|
590
|
+
}
|
|
591
|
+
/** Current action `Request`. Throws outside `*.server.ts` running over `/_action`. */
|
|
592
|
+
function useRequest() {
|
|
593
|
+
return store().req;
|
|
594
|
+
}
|
|
595
|
+
/** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
|
|
596
|
+
function useEnv() {
|
|
597
|
+
return store().env;
|
|
598
|
+
}
|
|
599
|
+
/** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
|
|
600
|
+
function useFetchCtx() {
|
|
601
|
+
return store().fetchCtx;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/index.ts
|
|
605
|
+
function actionMiddleware(loadRouter) {
|
|
606
|
+
return (req, res, next) => {
|
|
607
|
+
if ((req.url ?? "").split("?")[0] !== "/_action") {
|
|
608
|
+
next();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
(async () => {
|
|
612
|
+
const { handle } = await import(
|
|
613
|
+
/* @vite-ignore */
|
|
614
|
+
"tacho/transport/fetch"
|
|
615
|
+
);
|
|
616
|
+
await sendWebResponseFrom(req, res, await handle(await loadRouter(), { path: ACTION_PATH })(await nodeToWebRequest(req)));
|
|
617
|
+
})().catch(next);
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function attachActionUpgrade(httpServer, loadRouter) {
|
|
621
|
+
if (!httpServer) return;
|
|
622
|
+
Promise.all([import(
|
|
623
|
+
/* @vite-ignore */
|
|
624
|
+
"tacho/transport/ws"
|
|
625
|
+
), import(
|
|
626
|
+
/* @vite-ignore */
|
|
627
|
+
"crossws/adapters/node"
|
|
628
|
+
)]).then(([{ handle }, { default: crossws }]) => {
|
|
629
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
630
|
+
if ((req.url ?? "").split("?")[0] !== "/_action") return;
|
|
631
|
+
loadRouter().then((router) => crossws({ hooks: handle(router, { path: ACTION_PATH }) }).handleUpgrade(req, socket, head)).catch(() => {
|
|
632
|
+
socket.destroy();
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function previewMiddleware(file) {
|
|
638
|
+
return (req, res, next) => {
|
|
639
|
+
(async () => {
|
|
640
|
+
await sendWebResponseFrom(req, res, await (await import(
|
|
641
|
+
/* @vite-ignore */
|
|
642
|
+
pathToFileURL(file).href
|
|
643
|
+
)).default.fetch(await nodeToWebRequest(req)));
|
|
644
|
+
})().catch(next);
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function loadActions(root) {
|
|
648
|
+
const code = generateActionsModule(scanServerFiles(root), { bust: true });
|
|
649
|
+
return import(
|
|
650
|
+
/* @vite-ignore */
|
|
651
|
+
`data:text/javascript,${encodeURIComponent(code)}`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const unpluginFactory = (options) => {
|
|
655
|
+
let resolved;
|
|
656
|
+
const emitState = createEmitState();
|
|
657
|
+
return {
|
|
658
|
+
name: "oxidejs",
|
|
659
|
+
enforce: "pre",
|
|
660
|
+
buildStart() {
|
|
661
|
+
resolved ??= resolveOptions(options, process.cwd());
|
|
662
|
+
emitState.emitted = false;
|
|
663
|
+
},
|
|
664
|
+
resolveId(id) {
|
|
665
|
+
if (id === "virtual:oxide/actions") return RESOLVED_VIRTUAL_ACTIONS_ID;
|
|
666
|
+
if (id === "virtual:oxide/worker") return RESOLVED_VIRTUAL_WORKER_ID;
|
|
667
|
+
if (id === "virtual:oxide/client") return RESOLVED_VIRTUAL_CLIENT_ID;
|
|
668
|
+
return null;
|
|
669
|
+
},
|
|
670
|
+
load(id, extra) {
|
|
671
|
+
if (id === RESOLVED_VIRTUAL_CLIENT_ID) return generateClientModule(resolved?.actions ?? options?.actions ?? "http", resolved?.actionHeaders ?? options?.actionHeaders);
|
|
672
|
+
if (id === RESOLVED_VIRTUAL_ACTIONS_ID || id === RESOLVED_VIRTUAL_WORKER_ID) {
|
|
673
|
+
if (pluginShouldStub(this, extra)) throw new Error(`oxidejs: ${id === RESOLVED_VIRTUAL_ACTIONS_ID ? VIRTUAL_ACTIONS_ID : VIRTUAL_WORKER_ID} is server-only`);
|
|
674
|
+
}
|
|
675
|
+
if (id === RESOLVED_VIRTUAL_ACTIONS_ID) {
|
|
676
|
+
const modules = scanServerFiles(resolved?.root ?? process.cwd());
|
|
677
|
+
for (const mod of modules) this.addWatchFile(mod.abs);
|
|
678
|
+
return generateActionsModule(modules);
|
|
679
|
+
}
|
|
680
|
+
if (id === RESOLVED_VIRTUAL_WORKER_ID) {
|
|
681
|
+
if (!resolved) return;
|
|
682
|
+
this.addWatchFile(resolved.workerEntryAbs);
|
|
683
|
+
const modules = scanServerFiles(resolved.root);
|
|
684
|
+
for (const mod of modules) this.addWatchFile(mod.abs);
|
|
685
|
+
return generateWorkerWrapper(resolved.workerEntryAbs, {
|
|
686
|
+
preset: resolved.preset,
|
|
687
|
+
clientDir: resolved.clientDir,
|
|
688
|
+
hasClient: resolved.hasClient,
|
|
689
|
+
hasPublic: resolved.hasPublic,
|
|
690
|
+
hasActions: modules.length > 0,
|
|
691
|
+
actions: resolved.actions
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (isServerFileId(id) && pluginShouldStub(this, extra)) {
|
|
695
|
+
const file = id.split("?")[0] ?? id;
|
|
696
|
+
this.addWatchFile(file);
|
|
697
|
+
return loadClientStub(id);
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
transform(code, id, extra) {
|
|
701
|
+
if (!isServerFileId(id) || !pluginShouldStub(this, extra)) return;
|
|
702
|
+
return generateClientStub({
|
|
703
|
+
key: moduleKey(id.split("?")[0] ?? id),
|
|
704
|
+
exports: parseExportedNames(code)
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
vite: {
|
|
708
|
+
config(config) {
|
|
709
|
+
resolved = resolveOptions(options, typeof config.root === "string" ? config.root : process.cwd(), config);
|
|
710
|
+
applyViteEnvironments(config, resolved);
|
|
711
|
+
},
|
|
712
|
+
configureServer(server) {
|
|
713
|
+
const invalidateActions = () => {
|
|
714
|
+
for (const env of Object.values(server.environments)) {
|
|
715
|
+
const mod = env.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ACTIONS_ID);
|
|
716
|
+
if (mod) env.moduleGraph.invalidateModule(mod);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
server.watcher.on("all", (_event, file) => {
|
|
720
|
+
if (isServerFileId(file)) invalidateActions();
|
|
721
|
+
});
|
|
722
|
+
const loadRouter = async () => {
|
|
723
|
+
return (await server.ssrLoadModule(VIRTUAL_ACTIONS_ID)).default;
|
|
724
|
+
};
|
|
725
|
+
if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
|
|
726
|
+
else server.middlewares.use(actionMiddleware(loadRouter));
|
|
727
|
+
},
|
|
728
|
+
configurePreviewServer(server) {
|
|
729
|
+
if (resolved?.preset !== "fetch") return;
|
|
730
|
+
server.middlewares.use(previewMiddleware(path.join(resolved.outDir, "server.js")));
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
rsbuild: { setup(api) {
|
|
734
|
+
api.modifyRsbuildConfig((config) => {
|
|
735
|
+
resolved = resolveOptions(options, typeof config.root === "string" ? config.root : process.cwd(), config);
|
|
736
|
+
applyRsbuildEnvironments(config, resolved);
|
|
737
|
+
});
|
|
738
|
+
api.onBeforeStartDevServer(({ server }) => {
|
|
739
|
+
const loadRouter = async () => {
|
|
740
|
+
return (await loadActions(resolved?.root ?? process.cwd())).default;
|
|
741
|
+
};
|
|
742
|
+
if (resolved?.actions === "ws") attachActionUpgrade(server.httpServer, loadRouter);
|
|
743
|
+
else server.middlewares.use(actionMiddleware(loadRouter));
|
|
744
|
+
});
|
|
745
|
+
api.onBeforeStartPreviewServer?.(({ server }) => {
|
|
746
|
+
if (resolved?.preset !== "fetch") return;
|
|
747
|
+
server.middlewares.use(previewMiddleware(path.join(resolved.outDir, "server.js")));
|
|
748
|
+
});
|
|
749
|
+
} },
|
|
750
|
+
writeBundle() {
|
|
751
|
+
if (!resolved) return;
|
|
752
|
+
copyPublicDir(resolved);
|
|
753
|
+
tryEmitWranglerConfig(resolved, emitState);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
};
|
|
757
|
+
const oxidejs = /* @__PURE__ */ createUnplugin(unpluginFactory);
|
|
758
|
+
const vite = /* @__PURE__ */ (() => oxidejs.vite)();
|
|
759
|
+
//#endregion
|
|
760
|
+
export { useEnv as a, useCtx as i, unpluginFactory as n, useFetchCtx as o, vite as r, useRequest as s, oxidejs as t };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type OxidejsPreset = "fetch" | "celld";
|
|
3
|
+
interface OxidejsWranglerOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
compatibility_date: string;
|
|
6
|
+
compatibility_flags?: string[];
|
|
7
|
+
durable_objects?: Record<string, unknown>;
|
|
8
|
+
migrations?: unknown[];
|
|
9
|
+
services?: unknown[];
|
|
10
|
+
vars?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
type OxidejsActionTransport = "http" | "ws";
|
|
13
|
+
/** Static headers inlined into the shared action client. Functions cannot ship to the browser. */
|
|
14
|
+
type OxidejsActionHeaders = Record<string, string> | [string, string][];
|
|
15
|
+
interface OxidejsOptions {
|
|
16
|
+
/** "fetch" (default) skips wrangler.jsonc and serves client assets. "celld" emits wrangler.jsonc. */
|
|
17
|
+
preset?: OxidejsPreset;
|
|
18
|
+
/** Path to server entry, relative to project root. Default: "src/server.ts" */
|
|
19
|
+
workerEntry?: string;
|
|
20
|
+
/** Output root. Default: "dist" */
|
|
21
|
+
outDir?: string;
|
|
22
|
+
/** Client subdirectory under outDir. Default: "client" */
|
|
23
|
+
clientDir?: string;
|
|
24
|
+
/** Wrangler config fields to merge into the generated wrangler.jsonc. */
|
|
25
|
+
wrangler?: OxidejsWranglerOptions;
|
|
26
|
+
/** Skip config emission. Defaults to false for celld, true for fetch. */
|
|
27
|
+
emitConfig?: boolean;
|
|
28
|
+
/** Transport for `*.server.ts` stubs. Default: "http". */
|
|
29
|
+
actions?: OxidejsActionTransport;
|
|
30
|
+
/** Extra headers on the shared HTTP action client. Ignored when `actions` is "ws". */
|
|
31
|
+
actionHeaders?: OxidejsActionHeaders;
|
|
32
|
+
}
|
|
33
|
+
interface ResolvedOptions {
|
|
34
|
+
root: string;
|
|
35
|
+
preset: OxidejsPreset;
|
|
36
|
+
workerEntry: string;
|
|
37
|
+
workerEntryAbs: string;
|
|
38
|
+
/** Absolute output root. */
|
|
39
|
+
outDir: string;
|
|
40
|
+
/** Relative segment only. */
|
|
41
|
+
clientDir: string;
|
|
42
|
+
wrangler: OxidejsWranglerOptions | undefined;
|
|
43
|
+
emitConfig: boolean;
|
|
44
|
+
/** False when there is no index.html — server-only, no client env or assets. */
|
|
45
|
+
hasClient: boolean;
|
|
46
|
+
/** True when `<root>/public` exists. Copied next to client assets on fetch. */
|
|
47
|
+
hasPublic: boolean;
|
|
48
|
+
actions: OxidejsActionTransport;
|
|
49
|
+
actionHeaders: OxidejsActionHeaders | undefined;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { OxidejsWranglerOptions as a, OxidejsPreset as i, OxidejsActionTransport as n, ResolvedOptions as o, OxidejsOptions as r, OxidejsActionHeaders as t };
|
package/dist/vite.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { r as OxidejsOptions } from "./types-CFWyYfds.mjs";
|
|
2
|
+
//#region src/vite.d.ts
|
|
3
|
+
declare const _default: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
|
|
4
|
+
//#endregion
|
|
5
|
+
export { type OxidejsOptions, _default as default };
|
package/dist/vite.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxidejs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Vite/Rsbuild plugin. One build → dist/server.js + optional client. Server actions via *.server.ts.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"rsbuild",
|
|
8
|
+
"server-actions",
|
|
9
|
+
"typescript",
|
|
10
|
+
"unplugin",
|
|
11
|
+
"vite"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/ryuzcorp/oxide/tree/main/packages/oxidejs#readme",
|
|
14
|
+
"bugs": "https://github.com/ryuzcorp/oxide/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ryuzcorp/oxide.git",
|
|
19
|
+
"directory": "packages/oxidejs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"virtual.d.ts",
|
|
24
|
+
"client.d.ts",
|
|
25
|
+
"tsconfig.app.json"
|
|
26
|
+
],
|
|
5
27
|
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"import": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./vite": {
|
|
35
|
+
"types": "./dist/vite.d.mts",
|
|
36
|
+
"import": "./dist/vite.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./rsbuild": {
|
|
39
|
+
"types": "./dist/rsbuild.d.mts",
|
|
40
|
+
"import": "./dist/rsbuild.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./virtual": {
|
|
43
|
+
"types": "./virtual.d.ts"
|
|
44
|
+
},
|
|
45
|
+
"./client": {
|
|
46
|
+
"types": "./client.d.ts"
|
|
47
|
+
},
|
|
48
|
+
"./tsconfig": "./tsconfig.app.json"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"test": "bun test",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"lint": "oxlint",
|
|
55
|
+
"fmt": "oxfmt --write .",
|
|
56
|
+
"prepublishOnly": "bun run build"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"unplugin": "^3.3.0"
|
|
60
|
+
},
|
|
6
61
|
"devDependencies": {
|
|
7
|
-
"
|
|
62
|
+
"tacho": "^0.4.1"
|
|
8
63
|
},
|
|
9
64
|
"peerDependencies": {
|
|
10
|
-
"
|
|
65
|
+
"@rsbuild/core": "*",
|
|
66
|
+
"crossws": "*",
|
|
67
|
+
"tacho": "*",
|
|
68
|
+
"vite": "*"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@rsbuild/core": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"crossws": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"vite": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"tacho": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=20"
|
|
11
86
|
}
|
|
12
87
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
"jsx": "preserve",
|
|
18
|
+
"types": ["vite/client", "oxidejs/client", "oxidejs/virtual"]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/virtual.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare module "virtual:oxide/actions" {
|
|
2
|
+
// Generated tacho router. Typed loosely so apps can pass it to handle().
|
|
3
|
+
const actions: Record<string, never>;
|
|
4
|
+
export default actions;
|
|
5
|
+
export { actions };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module "virtual:oxide/client" {
|
|
9
|
+
export const client: Record<string, Record<string, (...args: unknown[]) => Promise<unknown>>>;
|
|
10
|
+
}
|
package/bun.lock
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "oxide-placeholder",
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest",
|
|
9
|
-
},
|
|
10
|
-
"peerDependencies": {
|
|
11
|
-
"typescript": "^5",
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
"packages": {
|
|
16
|
-
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
|
17
|
-
|
|
18
|
-
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
|
19
|
-
|
|
20
|
-
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
|
21
|
-
|
|
22
|
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
23
|
-
|
|
24
|
-
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
25
|
-
}
|
|
26
|
-
}
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Hello via Bun!");
|
package/readme.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Placeholder for https://github.com/oxidejs/oxide release
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true,
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
28
|
-
}
|
|
29
|
-
}
|