@thednp/rpc 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +30 -15
- package/CHANGELOG.md +83 -0
- package/README.md +35 -29
- package/dist/express/express.d.mts +13 -1
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +51 -60
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +16 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +40 -60
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +20 -3
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +39 -59
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts +94 -0
- package/dist/h3/h3.d.mts.map +1 -0
- package/dist/h3/h3.mjs +245 -0
- package/dist/h3/h3.mjs.map +1 -0
- package/dist/helpers/helpers.d.mts +2 -0
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +14 -40
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +39 -71
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -1
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +12 -1
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +46 -61
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +104 -7
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +94 -16
- package/dist/server/server.mjs.map +1 -1
- package/package.json +10 -4
package/AGENTS.md
CHANGED
|
@@ -8,6 +8,7 @@ pnpm dev:express # Run examples/express dev server
|
|
|
8
8
|
pnpm dev:fastify # Run examples/fastify dev server
|
|
9
9
|
pnpm dev:hono # Run examples/hono dev server
|
|
10
10
|
pnpm dev:koa # Run examples/koa dev server
|
|
11
|
+
pnpm dev:react-query # Run examples/react-query dev server
|
|
11
12
|
pnpm dev:ssr # Run examples/ssr dev server
|
|
12
13
|
pnpm lint # Lint + typecheck (deno lint + tsc)
|
|
13
14
|
pnpm test # Run tests with coverage
|
|
@@ -32,32 +33,45 @@ pnpm release # Publish npm + jsr (scripts/release.js)
|
|
|
32
33
|
|
|
33
34
|
## Examples
|
|
34
35
|
|
|
35
|
-
The `examples/` directory contains
|
|
36
|
-
|
|
37
|
-
| Example
|
|
38
|
-
|
|
|
39
|
-
| `spa`
|
|
40
|
-
| `express`
|
|
41
|
-
| `fastify`
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
36
|
+
The `examples/` directory contains 8 example apps:
|
|
37
|
+
|
|
38
|
+
| Example | Adapter | Type | Run Command | Config |
|
|
39
|
+
| ----------------| -------------------------------------------------------------------| ------| -------------------------| -----------------------------------------|
|
|
40
|
+
| `spa` | Vite dev server (no adapter) | SPA | `pnpm dev` | `examples/spa/rpc.config.ts` |
|
|
41
|
+
| `express` | Express | SSR | `pnpm dev:express` | `examples/express/rpc.config.ts` |
|
|
42
|
+
| `fastify` | Fastify | SSR | `pnpm dev:fastify` | `examples/fastify/rpc.config.ts` |
|
|
43
|
+
| `h3` | h3 | SSR | `pnpm dev:h3` | `examples/h3/rpc.config.ts` |
|
|
44
|
+
| `hono` | Hono | SSR | `pnpm dev:hono` | `examples/hono/rpc.config.ts` |
|
|
45
|
+
| `koa` | Koa | SSR | `pnpm dev:koa` | `examples/koa/rpc.config.ts` |
|
|
46
|
+
| `react-query` | Express (React + @tanstack/react-query SSR) | SSR | `pnpm dev:react-query` | `examples/react-query/rpc.config.ts` |
|
|
47
|
+
| `solid-query` | Express (Solid + @tanstack/solid-query SSR) | SSR | `pnpm dev:solid-query` | `examples/solid-query/rpc.config.ts` |
|
|
48
|
+
| `ssr` | Custom `http-express.ts` (Express-compatible `node:http` server ) | SSR | `pnpm dev:ssr` | `examples/ssr/rpc.config.ts` | `examples/express/rpc.config.ts` |
|
|
49
|
+
| `fastify` | Fastify | SSR | `pnpm dev:fastify` | `examples/fastify/rpc.config.ts` |
|
|
50
|
+
| `hono` | Hono | SSR | `pnpm dev:hono` | `examples/hono/rpc.config.ts` |
|
|
51
|
+
| `koa` | Koa | SSR | `pnpm dev:koa` | `examples/koa/rpc.config.ts` |
|
|
52
|
+
| `react-query` | Express (React + @tanstack/react-query SSR) | SSR | `pnpm dev:react-query` | `examples/react-query/rpc.config.ts` |
|
|
53
|
+
| `solid-query` | Express (Solid + @tanstack/solid-query SSR) | SSR | `pnpm dev:solid-query` | `examples/solid-query/rpc.config.ts` |
|
|
54
|
+
| `ssr` | Custom `http-express.ts` (Express-compatible `node:http` server ) | SSR | `pnpm dev:ssr` | `examples/ssr/rpc.config.ts` |
|
|
45
55
|
|
|
46
56
|
Each example follows the same structure:
|
|
47
57
|
|
|
48
58
|
- `server.js` / `server.ts` — production server with Vite middleware in dev, static serving in prod
|
|
49
59
|
- `src/api/server.ts` — RPC server functions (auto-scanned by the plugin)
|
|
50
|
-
- `src/entry-server.ts` — SSR entry (hydrates on client)
|
|
51
|
-
- `src/entry-client.ts` — Client entry
|
|
60
|
+
- `src/entry-server.ts` / `src/entry-server.tsx` — SSR entry (hydrates on client)
|
|
61
|
+
- `src/entry-client.ts` / `src/entry-client.tsx` — Client entry
|
|
52
62
|
- `vite.config.ts` + `rpc.config.ts` — Configuration files
|
|
53
63
|
|
|
64
|
+
> Note: the `react-query` example is SSR with React 19 + `@tanstack/react-query` (Express adapter). It prefetches queries in `entry-server.tsx`, dehydrates them into `window.__REACT_QUERY_STATE__`, and hydrates on the client via `HydrationBoundary`.
|
|
65
|
+
>
|
|
66
|
+
> Note: the `solid-query` example is SSR with Solid + `@tanstack/solid-query` (Express adapter). The greeting is prefetched and serialized with `renderToStringAsync`; the GET form does **not** use a disabled `createQuery` (which would hang SSR — see `wiki/client-usage.md`), it calls `queryClient.fetchQuery()` on submit instead.
|
|
67
|
+
|
|
54
68
|
## Key Directories
|
|
55
69
|
|
|
56
70
|
- `src/` — source for all packages (vite plugin, server, express, fastify, hono, koa adapters)
|
|
57
71
|
- `dist/` — build output (not committed, generated by tsdown)
|
|
58
72
|
- `tests/` — test files (one per adapter + plugin)
|
|
59
73
|
- `tests/fixtures/` — test fixtures (config files, vite-mock.ts)
|
|
60
|
-
- `examples/` — example apps (spa, express, fastify, hono, koa, ssr)
|
|
74
|
+
- `examples/` — example apps (spa, express, fastify, hono, koa, react-query, solid-query, ssr)
|
|
61
75
|
|
|
62
76
|
## Build Output (tsdown)
|
|
63
77
|
|
|
@@ -136,9 +150,10 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
|
|
|
136
150
|
- `wiki/quickstart.md` — Rebuild the Express SSR example from `create-vite` in under a minute (copy-paste)
|
|
137
151
|
- `wiki/getting-started.md` — Installation, project structure, auto-scanning, and your first function
|
|
138
152
|
- `wiki/configuration.md` — Configuration reference (`rpc.config.ts`, `vite.config.ts`, options)
|
|
139
|
-
- `wiki/server-functions.md` — `createServerFunction` API, methods, validation
|
|
153
|
+
- `wiki/server-functions.md` — `createServerFunction` API, methods, validation, **request context (`getRequestContext`/`provideRequestContext`)** for per-request data access across async call stacks
|
|
154
|
+
- `wiki/nojs-fallback.md` — native (no-JS) `<form>` fallback / progressive enhancement pattern
|
|
140
155
|
- `wiki/client-usage.md` — Client-side usage, type safety, react-query integration
|
|
141
156
|
- `wiki/wire-protocol.md` — HTTP contract, request/response bodies, curl debugging
|
|
142
|
-
- `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa)
|
|
157
|
+
- `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa, h3)
|
|
143
158
|
- `wiki/security.md` — Security hardening
|
|
144
159
|
- `wiki/best-practices.md` — Production patterns (auth, rate limiting, body limits, CSRF)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-08-09
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **h3 adapter**: new `@thednp/rpc/h3` adapter with `createRPCMiddleware`, `viteMiddleware`, `attachRPC`, `attachVite`, `readBody`, `redirect` — fully typed, 100% test coverage, SSR example at `examples/h3`
|
|
8
|
+
- **Request Context API**: `provideRequestContext(init, cb)` / `getRequestContext()` — per-request `AsyncLocalStorage` context available to all server function code; `RequestEvent` includes `nativeEvent`, `locals`, adapter-bound `redirect`; works across Express, Fastify, Hono, Koa, and h3
|
|
9
|
+
- **`redirect` helper**: a new `redirect(res, location, status?)` server utility exported from `@thednp/rpc/server` and every adapter. It defaults to `303 See Other` for Post/Redirect/Get flows. The core version accepts an Express `Response` or a raw Node `ServerResponse`, delegating to native `res.redirect()` when available and otherwise writing the status code and `Location` header directly — the raw path is safe on Connect-compatible middlewares and serverless adapters (e.g. Netlify's `serverless-http` mock). Each adapter exports a redirect typed for its response object: Fastify `reply.redirect(location, status)` (v5 URL-first), Koa `ctx.redirect(location)` then `ctx.status = status` (setting status *after*, per koajs/koa#857), and Hono `return redirect(c, location, status)` (the `Response` is returned from the handler)
|
|
10
|
+
|
|
11
|
+
- **`application/x-www-form-urlencoded` content type**: the `contentType` option and `BodyResult` now include urlencoded. The generated client serializes the single object argument with `new URLSearchParams(args[0]).toString()`, so native HTML forms can POST straight to an RPC endpoint without client-side serialization. Adapters parse `key=value&key2=value2` into an object via `URLSearchParams` (every value arrives as a string; repeated keys collapse to the last value) and use the framework's pre-parsed body (`express.urlencoded()`, `@fastify/formbody`, `koa-body`) when one ran first
|
|
12
|
+
- **Content-type enforcement**: the RPC middleware now validates the request's `Content-Type` against the function's declared `contentType` before parsing the body, rejecting mismatches with `415 Unsupported Media Type` on all four adapters. JSON and text functions are enforced strictly (exact match wins after stripping `charset`/`boundary` parameters); form functions (`multipart/form-data` or `application/x-www-form-urlencoded`) are lenient between the two encodings so native urlencoded form submissions keep working on multipart-declared endpoints. Requests without a `Content-Type` header (curl, GET, legacy clients) are exempt from enforcement. The check lives in the new `hasContentTypeMismatch`/`isFormContentType` helpers exported from `@thednp/rpc/server`
|
|
13
|
+
|
|
14
|
+
### Examples
|
|
15
|
+
|
|
16
|
+
- Add the `h3` example (`examples/h3`): SSR with [h3](https://h3.unjs.io/) using `createRPCMiddleware`, `serveStatic`, `toNodeListener`, and `viteMiddleware`. Dev mode bridges Vite's connect stack; prod serves static assets via `serveStatic` and falls through to SSR.
|
|
17
|
+
- The `demo` form-fallback middleware now uses the new `redirect` helper from `@thednp/rpc/express` instead of hand-writing the `303` + `Location` response
|
|
18
|
+
- Add a nojs fallback to the `demo` contact form (progressive enhancement): the form now carries native `action="/@demo/submit-contact" method="post"` attributes, and a small app-layer `createFormFallback` middleware (mounted before the RPC middleware) intercepts browser form navigations — recognized by `POST` + `application/x-www-form-urlencoded` + `Accept: text/html`, which the JS client never sends. Valid submissions get a `303` redirect to the pre-filled GitHub issue URL; invalid ones get a `303` redirect back to `/?name=..&errors=..`, where the server-rendered page (and hydration) recover the form values and show the same field-level error messages the RPC path would have returned. Validation, issue-URL building, and field lists are shared with the server function via the new `demo/src/lib/contact-form.ts`, and the server function now normalizes both multipart (`{ raw }`) and plain-object (urlencoded/JSON-with-fields) payloads
|
|
19
|
+
- Add the `react-query` example (Express + React 19 + `@tanstack/react-query` SSR): prefetches the greeting in `entry-server.tsx`, dehydrates it into `window.__REACT_QUERY_STATE__`, and hydrates on the client via `HydrationBoundary`
|
|
20
|
+
- Add the `solid-query` example (Express + Solid + `@tanstack/solid-query` SSR): prefetches the greeting, serializes it with `renderToStringAsync`, and documents the disabled-query SSR gotcha — a disabled `createQuery` hangs `renderToStringAsync` because the observer result carries a never-settling `promise` (from `experimental_prefetchInRender`) that seroval awaits forever; the example calls `queryClient.fetchQuery()` on submit instead
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
|
|
24
|
+
- Document Request Context API (`provideRequestContext`/`getRequestContext`) in `wiki/server-functions.md` — per-request `AsyncLocalStorage` context with `nativeEvent`, `locals`, adapter-bound `redirect` across all 5 adapters
|
|
25
|
+
- Document urlencoded content type in `wiki/server-functions.md` and `wiki/wire-protocol.md` (new `POST + application/x-www-form-urlencoded` section)
|
|
26
|
+
- Document the `415 Unsupported Media Type` response and content-type enforcement rules in `wiki/wire-protocol.md`
|
|
27
|
+
- Document the strict/lenient content-type behavior in `wiki/server-functions.md`
|
|
28
|
+
- Add a "Content-Type Enforcement" section to `wiki/security.md`
|
|
29
|
+
- Document the `redirect` helper in `wiki/server-functions.md` (new "Redirects (`redirect`)" section) and cross-reference it from `wiki/adapters.md`
|
|
30
|
+
- Add h3 adapter section to `wiki/adapters.md` (installation, usage, body limits)
|
|
31
|
+
- Add h3 body limits to `wiki/best-practices.md` (Content-Length fast path + streaming cap via `for await (const chunk of event.req.body)`)
|
|
32
|
+
- Update `llms.txt`, `AGENTS.md`, and `README.md` for the new adapter, examples, content type, enforcement behavior, redirect helper, and request context
|
|
33
|
+
|
|
34
|
+
### Tests
|
|
35
|
+
|
|
36
|
+
- **100% coverage achieved** across all adapters (express, fastify, h3, hono, koa) — 920/920 statements, 559/559 branches, 144/144 functions, 898/898 lines
|
|
37
|
+
- Add h3 adapter test suite (`tests/h3.test.ts`): viteMiddleware (node/web runtime paths, settle-twice guard), createMiddleware (prefix/path filtering, name deduplication), createRPCMiddleware (dispatch, content-type enforcement, origin check, redirect default 303, cancel on close, error handling)
|
|
38
|
+
- Add redirect default-status tests to Express, Fastify, Hono, and Koa adapter suites (covers `status = 303` default param branch)
|
|
39
|
+
- Add urlencoded `readBody` tests for all four adapters (pre-parsed and raw stream paths), a urlencoded client-module codegen test in `plugin.test.ts`, and an end-to-end RPC middleware dispatch test
|
|
40
|
+
- Add content-type enforcement tests to the Express, Fastify, Hono, and Koa adapter suites (415 on json↔urlencoded/text↔json mismatch, lenient acceptance of urlencoded on multipart-declared functions) and unit tests for `hasContentTypeMismatch`/`isFormContentType` (parameter stripping, case insensitivity, no-header exemption, form leniency both directions)
|
|
41
|
+
- Add `redirect` tests: raw `ServerResponse` write path and native `.redirect()` delegation in `server-helpers.test.ts`, plus per-adapter suites asserting each framework API (Express `res.redirect(status, url)` vs raw write, Fastify URL-first `reply.redirect`, Koa status-after-`ctx.redirect`, Hono returning `c.redirect`'s `Response`)
|
|
42
|
+
- Fix `scripts/dev-test.js` to verify the query-framework examples' dynamically rendered greeting (`Hello Jane!`) instead of only the static `Hello World!` SSR marker; register the `solid-query` example prefix
|
|
43
|
+
|
|
44
|
+
### Chores
|
|
45
|
+
|
|
46
|
+
- Bump version to `0.2.0`
|
|
47
|
+
- Sync `deno.json` version with `package.json`
|
|
48
|
+
|
|
49
|
+
### Security
|
|
50
|
+
|
|
51
|
+
- **Content-type check moved before body read**: all four adapters now evaluate `hasContentTypeMismatch` *before* calling `readBody`, so mismatched requests are rejected with `415 Unsupported Media Type` without ever buffering/parsing the body (previously the body was read first, contradicting the documented behavior)
|
|
52
|
+
- **Streaming body-limit in the demo**: `demo/body-limit.ts` no longer buffers the entire body via `readBody` before checking the cap. It enforces the 1MB limit while the request stream is being read (dropping buffered chunks and destroying the request on overflow) and adds a `Content-Length` fast-path for obviously oversized requests — closing a memory-exhaustion gap on the demo's raw `node:http` and Netlify entry points
|
|
53
|
+
- **Netlify URL rewrite tightened**: `demo/netlify/functions/rpc.ts` now matches the `/.netlify/functions/rpc/` marker against the parsed `pathname` only (prefix match), instead of `indexOf` on the raw URL which could match inside a query string and rewrite unintended requests
|
|
54
|
+
- **`@hono/node-server` dedupe**: add a workspace `overrides: { '@hono/node-server': '>=2.0.5' }` in `pnpm-workspace.yaml` to collapse the vulnerable `1.19.17` transitive dependency (via `@hono/vite-dev-server`, which pins `^1.19.11`) onto the already-used patched `2.1.0`. `pnpm audit` is now clean
|
|
55
|
+
|
|
56
|
+
## [0.1.1] - 2026-08-07
|
|
57
|
+
|
|
58
|
+
### Security
|
|
59
|
+
|
|
60
|
+
- Unexpected exceptions no longer expose their message in development responses: `formatError` returns the generic `{ error: "Internal Server Error" }` for any non-`RPCError` error in every environment. `RPCError` payloads (developer-authored `message`, `code`, optional `data`) are still included in development so client-side error handling keeps working, while diagnostics stay server-side via the middleware's `console.error` logging (addresses the GitHub CodeQL `js/exception-information-leak` finding)
|
|
61
|
+
|
|
62
|
+
### Refactor
|
|
63
|
+
|
|
64
|
+
- `scanForServerFiles` lazy-imports Vite inside the scan function instead of importing it statically at the top of the module — the standalone server entry no longer carries a static Vite dependency, so serverless function bundles that register API modules directly no longer drag Vite's node chunk (which imports esbuild, absent with Vite 8's rolldown) into the bundle
|
|
65
|
+
- Drop the redundant `config as ScanConfig` casts in `scanForServerFiles` (the merged config is already typed)
|
|
66
|
+
|
|
67
|
+
### Fixes
|
|
68
|
+
|
|
69
|
+
- Netlify serverless deployment: externalize Vite from the function bundle via `[functions] external_node_modules = ["vite"]` in `netlify.toml`, so `@netlify/zip-it-and-ship-it` no longer fails with "Could not resolve 'esbuild'" when bundling the RPC function
|
|
70
|
+
- Fix broken documentation links in all 6 example READMEs (`wiki/setup.md` → `wiki/wire-protocol.md`, the former never existed)
|
|
71
|
+
- Remove stale `demo/src/render-bak.ts`; clean up the pnpm lockfile
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- Update `wiki/security.md`, `wiki/wire-protocol.md`, `README.md`, and `llms.txt` to reflect that unexpected exception details never reach clients — only `RPCError` payloads, and only in development
|
|
76
|
+
|
|
77
|
+
### Tests
|
|
78
|
+
|
|
79
|
+
- `formatError` dev-mode tests updated to assert the generic message for unexpected exceptions (plain `Error` and non-`Error` values), keeping the `RPCError` payload assertions
|
|
80
|
+
|
|
81
|
+
### Chores
|
|
82
|
+
|
|
83
|
+
- Sync demo and all 6 examples to `@thednp/rpc ^0.1.0`
|
|
84
|
+
- Remove the leftover `0.0.14` changelog header (its entry was merged into `0.1.0`)
|
|
85
|
+
|
|
3
86
|
## [0.1.0] - 2026-08-06
|
|
4
87
|
|
|
5
88
|
### Features
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://jsr.io/@thednp/rpc)
|
|
7
7
|
[](http://npm-stat.com/charts.html?package=@thednp/rpc)
|
|
8
8
|
|
|
9
|
-
A Vite plugin for automatic RPC generation —
|
|
9
|
+
A Vite plugin for automatic RPC generation — write server functions, re-export them, call them. Simple and framework agnostic.
|
|
10
10
|
|
|
11
11
|
## Isomorphic Design
|
|
12
12
|
|
|
@@ -35,7 +35,7 @@ You define them in a file, import and call them where you need them. The plugin
|
|
|
35
35
|
* `createFunction.ts` — server-side definition (wrapped handler with `AbortController`)
|
|
36
36
|
* `getClientModules.ts` — build-time code generation (string template with validation)
|
|
37
37
|
* `client-helpers.ts` — client-side runtime (thin `fetch` based modules)
|
|
38
|
-
* `server-helpers.ts` — server-only utilities (`RPCError`, error formatting, glob file walking)
|
|
38
|
+
* `server-helpers.ts` — server-only utilities (`RPCError`, error formatting, `redirect`, glob file walking)
|
|
39
39
|
* `scanForServerFiles.ts` — file discovery
|
|
40
40
|
* **Adapters** — thin middleware wrappers
|
|
41
41
|
</details>
|
|
@@ -90,7 +90,7 @@ Scan `src/api/` for classic `server.ts|js|mjs|mts` files, or switch to glob mode
|
|
|
90
90
|
<details>
|
|
91
91
|
<summary><b>Typed errors, safe by default</b></summary>
|
|
92
92
|
|
|
93
|
-
Server errors return a generic `Internal Server Error`
|
|
93
|
+
Server errors return a generic `Internal Server Error` — no messages, codes, or stacks leak to clients, in any environment. Only `RPCError` payloads (developer-authored `message`/`code`/`data`) reach the client, and only in development, so you can debug instantly. `multipart/form-data` content type is supported for file uploads via your framework's multipart parser, and json/text/urlencoded requests are validated against the function's declared content type (`415 Unsupported Media Type` on mismatch; form encodings are interchangeable for nojs form fallbacks).
|
|
94
94
|
</details>
|
|
95
95
|
|
|
96
96
|
<details>
|
|
@@ -99,29 +99,29 @@ Server errors return a generic `Internal Server Error` in production — no mess
|
|
|
99
99
|
Generic type inference flows from your server function's arguments and return type all the way to the client stub. You get autocomplete for function names, argument types, and return types without writing a single type annotation on the client side.
|
|
100
100
|
</details>
|
|
101
101
|
|
|
102
|
-
##
|
|
103
|
-
|
|
104
|
-
| Example | Source Code | Try online |
|
|
105
|
-
| -----------------| --------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------|
|
|
106
|
-
| SPA - node:http | [examples/spa](https://github.com/thednp/rpc/tree/master/examples/spa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/spa) |
|
|
107
|
-
| SSR - node:http | [examples/ssr](https://github.com/thednp/rpc/tree/master/examples/ssr) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/ssr) |
|
|
108
|
-
| Express | [examples/express](https://github.com/thednp/rpc/tree/master/examples/express) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/express) |
|
|
109
|
-
| Fastify | [examples/fastify](https://github.com/thednp/rpc/tree/master/examples/fastify) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/fastify) |
|
|
110
|
-
| Hono | [examples/hono](https://github.com/thednp/rpc/tree/master/examples/hono) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/hono) |
|
|
111
|
-
| Koa | [examples/koa](https://github.com/thednp/rpc/tree/master/examples/koa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/koa) |
|
|
102
|
+
## Examples
|
|
112
103
|
|
|
113
|
-
|
|
104
|
+
| Source | Demo | Clone |
|
|
105
|
+
| ----------------------------------------------------------------------------------------| ----------------------------------------------------------------------------------------------| ---------------------------------------------------------|
|
|
106
|
+
| [examples/spa](https://github.com/thednp/rpc/tree/master/examples/spa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/spa) | `pmpm dlx degit thednp/rpc/examples/spa my-app` |
|
|
107
|
+
| [examples/ssr](https://github.com/thednp/rpc/tree/master/examples/ssr) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/ssr) | `pnpm dlx degit thednp/rpc/examples/ssr my-app` |
|
|
108
|
+
| [examples/express](https://github.com/thednp/rpc/tree/master/examples/express) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/express) | `pnpm dlx degit thednp/rpc/examples/express my-app` |
|
|
109
|
+
| [examples/fastify](https://github.com/thednp/rpc/tree/master/examples/fastify) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/fastify) | `pnpm dlx degit thednp/rpc/examples/fastify my-app` |
|
|
110
|
+
| [examples/h3](https://github.com/thednp/rpc/tree/master/examples/h3) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/h3) | `pnpm dlx degit thednp/rpc/examples/h3 my-app` |
|
|
111
|
+
| [examples/hono](https://github.com/thednp/rpc/tree/master/examples/hono) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/hono) | `pnpm dlx degit thednp/rpc/examples/hono my-app` |
|
|
112
|
+
| [examples/koa](https://github.com/thednp/rpc/tree/master/examples/koa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/koa) | `pnpm dlx degit thednp/rpc/examples/koa my-app` |
|
|
113
|
+
| [examples/react-query](https://github.com/thednp/rpc/tree/master/examples/react-query) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/react-query) | `pnpm dlx degit thednp/rpc/examples/react-query my-app` |
|
|
114
|
+
| [examples/solid-query](https://github.com/thednp/rpc/tree/master/examples/solid-query) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/solid-query) | `pnpm dlx degit thednp/rpc/examples/solid-query my-app` |
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
> **Clone an example**: `degit` scaffolds a fresh copy straight from the repo — no git history, ready to run:
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| ssr | Custom `node:http` (Express-compatible) | SSR | `pnpm dev:ssr` | Direct import (SSR) + client stubs |
|
|
118
|
+
```bash
|
|
119
|
+
# Scaffold the Express example
|
|
120
|
+
pnpm dlx degit thednp/rpc/examples/express my-rpc-app
|
|
121
|
+
cd my-rpc-app
|
|
122
|
+
pnpm install
|
|
123
|
+
pnpm dev
|
|
124
|
+
```
|
|
125
125
|
|
|
126
126
|
SSR examples demonstrate isomorphic usage: server functions are imported directly during server-side rendering (`entry-server.ts`) and also called from the client via auto-generated fetch stubs. The SPA example uses only the client-side stubs.
|
|
127
127
|
|
|
@@ -319,6 +319,12 @@ The `readBody` utility of each adapter doesn't cap raw request bodies by default
|
|
|
319
319
|
Server functions only support `GET` and `POST` (default `POST`). RPC dispatch is not REST — `PUT`/`PATCH`/`DELETE` carry resource semantics that don't apply to function calls, and `OPTIONS` must stay reserved for CORS preflight. Every accepted method is another dispatch path to validate; keeping the surface minimal (and defaulting to `POST`) reduces CSRF and parsing attack surface. See [Server Functions Guide](./wiki/server-functions.md) for details.
|
|
320
320
|
</details>
|
|
321
321
|
|
|
322
|
+
<details>
|
|
323
|
+
<summary><b>Content-type enforcement</b></summary>
|
|
324
|
+
|
|
325
|
+
Request bodies are validated against the function's declared `contentType` before parsing — mismatches get a `415 Unsupported Media Type`. JSON and text functions require an exact match (after stripping `charset`/`boundary` parameters); the two form encodings are interchangeable so native urlencoded `<form>` submissions keep working on multipart-declared endpoints (nojs progressive enhancement). Requests without a `Content-Type` header are exempt, so curl and `GET` keep working unchanged. See [Wire Protocol](./wiki/wire-protocol.md) for details.
|
|
326
|
+
</details>
|
|
327
|
+
|
|
322
328
|
---
|
|
323
329
|
The full threat model, including edge cases and configuration options for tightening things further, is documented in [Security](./wiki/security.md).
|
|
324
330
|
|
|
@@ -327,13 +333,13 @@ The full threat model, including edge cases and configuration options for tighte
|
|
|
327
333
|
|
|
328
334
|
- [Quick Start](./wiki/quickstart.md) — Rebuild the Express SSR example from `create-vite` in under a minute
|
|
329
335
|
- [Getting Started](./wiki/getting-started.md) — Installation, project structure, and your first function
|
|
330
|
-
- [Configuration](./wiki/configuration.md)
|
|
331
|
-
- [Server Functions](./wiki/server-functions.md)
|
|
332
|
-
- [Client Usage](./wiki/client-usage.md)
|
|
336
|
+
- [Configuration](./wiki/configuration.md) — Full configuration reference
|
|
337
|
+
- [Server Functions](./wiki/server-functions.md) — Creating server functions
|
|
338
|
+
- [Client Usage](./wiki/client-usage.md) — Client-side usage
|
|
333
339
|
- [Wire Protocol](./wiki/wire-protocol.md) — The HTTP contract behind the generated clients (curl debugging)
|
|
334
|
-
- [Adapters](./wiki/adapters.md)
|
|
335
|
-
- [Best Practices](./wiki/best-practices.md)
|
|
336
|
-
- [Security](./wiki/security.md)
|
|
340
|
+
- [Adapters](./wiki/adapters.md) — Framework adapters
|
|
341
|
+
- [Best Practices](./wiki/best-practices.md) — Tips and best practices
|
|
342
|
+
- [Security](./wiki/security.md) — Security hardening
|
|
337
343
|
|
|
338
344
|
## License
|
|
339
345
|
|
|
@@ -107,6 +107,18 @@ declare const isExpressRequest: (req: IncomingMessage | Request) => req is Reque
|
|
|
107
107
|
* @returns True if the response is an Express Response
|
|
108
108
|
*/
|
|
109
109
|
declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
|
|
110
|
+
/**
|
|
111
|
+
* Issues an HTTP redirect on an Express or raw Node ServerResponse.
|
|
112
|
+
* Uses Express's native `res.redirect(status, location)` when an Express
|
|
113
|
+
* Response is provided, otherwise writes the status code and `Location`
|
|
114
|
+
* header directly on the raw `ServerResponse` (safe for Connect-compatible
|
|
115
|
+
* middlewares and serverless adapters whose mock responses lack `.redirect`).
|
|
116
|
+
* Defaults to `303 See Other` for convention (Post/Redirect/Get).
|
|
117
|
+
* @param res - Express Response or raw Node ServerResponse
|
|
118
|
+
* @param location - The URL to redirect to
|
|
119
|
+
* @param status - HTTP status code, defaults to 303
|
|
120
|
+
*/
|
|
121
|
+
declare const redirect: (res: ServerResponse | Response, location: string, status?: number) => void;
|
|
110
122
|
/**
|
|
111
123
|
* Type guard that checks whether a request has a pre-parsed body (`body` property).
|
|
112
124
|
* Used to detect if a body-parser middleware already consumed the stream.
|
|
@@ -129,5 +141,5 @@ declare const getRequestDetails: (request: Request | IncomingMessage) => Request
|
|
|
129
141
|
*/
|
|
130
142
|
declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
|
|
131
143
|
//#endregion
|
|
132
|
-
export { type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, type RequestDetails, type ResponseDetails, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
|
|
144
|
+
export { type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, type RequestDetails, type ResponseDetails, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody, redirect };
|
|
133
145
|
//# sourceMappingURL=express.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ,eAAe;;;;;KAM1C;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ,eAAe;;;;;KAM1C;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;cC3BW,kBAAkB;;;;;;;;cAyElB,qBAAqB;;;;;;;;iBCvGZ,UAAU,KAAK,UAAO;;;;;;iBAc5B,WAAW,KAAK,SAAS,MAAM;;;;;;;;cAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;cAqFE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;cASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;;;;;;cAeG,WAAQ,KACd,iBAAiB,UAAe,kBACrB;;;;;;;cAkBL,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;cAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;cAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}
|
package/dist/express/express.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
1
|
+
import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
2
|
//#region src/options.ts
|
|
3
3
|
const defaultRPCOptions = {
|
|
4
4
|
rpcPrefix: "__rpc",
|
|
@@ -44,9 +44,10 @@ const readBody = (req) => {
|
|
|
44
44
|
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
45
45
|
const isJSON = contentType.includes("json");
|
|
46
46
|
const isMultipart = contentType.includes("multipart/form-data");
|
|
47
|
+
const isUrlEncoded = contentType.includes("urlencoded");
|
|
47
48
|
resolve({
|
|
48
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
49
|
-
data: isMultipart ? req.body : isJSON ? req.body : String(req.body)
|
|
49
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
50
|
+
data: isMultipart ? req.body : isJSON ? req.body : isUrlEncoded ? req.body : String(req.body)
|
|
50
51
|
});
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
@@ -65,10 +66,11 @@ const readBody = (req) => {
|
|
|
65
66
|
const incomingType = req.headers["content-type"]?.toLowerCase() || "";
|
|
66
67
|
const isJSON = incomingType.includes("json");
|
|
67
68
|
const isMultipart = incomingType.includes("multipart/form-data");
|
|
69
|
+
const isUrlEncoded = incomingType.includes("urlencoded");
|
|
68
70
|
try {
|
|
69
|
-
const data = isMultipart ? { raw: body } : JSON.parse(body);
|
|
71
|
+
const data = isMultipart ? { raw: body } : isUrlEncoded ? Object.fromEntries(new URLSearchParams(body)) : JSON.parse(body);
|
|
70
72
|
resolve({
|
|
71
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
73
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
72
74
|
data: isMultipart ? data : data
|
|
73
75
|
});
|
|
74
76
|
} catch (_e) {
|
|
@@ -102,6 +104,26 @@ const isExpressResponse = (res) => {
|
|
|
102
104
|
return "json" in res && "send" in res;
|
|
103
105
|
};
|
|
104
106
|
/**
|
|
107
|
+
* Issues an HTTP redirect on an Express or raw Node ServerResponse.
|
|
108
|
+
* Uses Express's native `res.redirect(status, location)` when an Express
|
|
109
|
+
* Response is provided, otherwise writes the status code and `Location`
|
|
110
|
+
* header directly on the raw `ServerResponse` (safe for Connect-compatible
|
|
111
|
+
* middlewares and serverless adapters whose mock responses lack `.redirect`).
|
|
112
|
+
* Defaults to `303 See Other` for convention (Post/Redirect/Get).
|
|
113
|
+
* @param res - Express Response or raw Node ServerResponse
|
|
114
|
+
* @param location - The URL to redirect to
|
|
115
|
+
* @param status - HTTP status code, defaults to 303
|
|
116
|
+
*/
|
|
117
|
+
const redirect = (res, location, status = 303) => {
|
|
118
|
+
if (isExpressResponse(res)) {
|
|
119
|
+
res.redirect(status, location);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
res.statusCode = status;
|
|
123
|
+
res.setHeader("Location", location);
|
|
124
|
+
res.end();
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
105
127
|
* Type guard that checks whether a request has a pre-parsed body (`body` property).
|
|
106
128
|
* Used to detect if a body-parser middleware already consumed the stream.
|
|
107
129
|
* @param req - A Node IncomingMessage or Express Request
|
|
@@ -158,66 +180,15 @@ const getResponseDetails = (response) => {
|
|
|
158
180
|
};
|
|
159
181
|
};
|
|
160
182
|
//#endregion
|
|
161
|
-
//#region src/tools.ts
|
|
162
|
-
/**
|
|
163
|
-
* Escapes special regex metacharacters in a string.
|
|
164
|
-
* Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
|
|
165
|
-
* preventing ReDoS and regex injection attacks.
|
|
166
|
-
* @param s - The raw string to escape
|
|
167
|
-
* @returns The escaped string safe for use in new RegExp()
|
|
168
|
-
*/
|
|
169
|
-
function escapeRegExp(s) {
|
|
170
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
171
|
-
}
|
|
172
|
-
//#endregion
|
|
173
183
|
//#region src/constants.ts
|
|
174
184
|
const FUNCTION_NOT_FOUND = "Function not found";
|
|
175
185
|
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
176
186
|
const REQUEST_FORBIDDEN = "Forbidden";
|
|
177
|
-
const
|
|
187
|
+
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
178
188
|
const CLIENT_DISCONNECTED = "client disconnected";
|
|
179
189
|
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
180
190
|
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
181
191
|
//#endregion
|
|
182
|
-
//#region src/server-helpers.ts
|
|
183
|
-
/**
|
|
184
|
-
* A typed error thrown from server functions.
|
|
185
|
-
* The middleware serializes the `message` and `code` in the response,
|
|
186
|
-
* allowing clients to recognise and handle specific error conditions.
|
|
187
|
-
*/
|
|
188
|
-
var RPCError = class extends Error {
|
|
189
|
-
/** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
|
|
190
|
-
code;
|
|
191
|
-
/** Optional diagnostic payload */
|
|
192
|
-
data;
|
|
193
|
-
constructor(message, code = "INTERNAL", data) {
|
|
194
|
-
super(message);
|
|
195
|
-
this.name = "RPCError";
|
|
196
|
-
this.code = code;
|
|
197
|
-
this.data = data;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
/**
|
|
201
|
-
* Formats an error for the RCP middleware response.
|
|
202
|
-
* In development the full message and stack are included so developers
|
|
203
|
-
* can quickly identify issues. In production only the generic
|
|
204
|
-
* "Internal Server Error" is sent, preventing information disclosure.
|
|
205
|
-
*/
|
|
206
|
-
const formatError = (err, isProduction) => {
|
|
207
|
-
if (!isProduction) {
|
|
208
|
-
if (err instanceof RPCError) {
|
|
209
|
-
const payload = {
|
|
210
|
-
error: err.message || "Internal Server Error",
|
|
211
|
-
code: err.code
|
|
212
|
-
};
|
|
213
|
-
if (err.data !== void 0) payload.data = err.data;
|
|
214
|
-
return payload;
|
|
215
|
-
}
|
|
216
|
-
return { error: (err instanceof Error ? err.message : String(err)) || "Internal Server Error" };
|
|
217
|
-
}
|
|
218
|
-
return { error: INTERNAL_SERVER_ERROR };
|
|
219
|
-
};
|
|
220
|
-
//#endregion
|
|
221
192
|
//#region src/express/createMiddleware.ts
|
|
222
193
|
let middlewareCount = 0;
|
|
223
194
|
const middlewareStack = /* @__PURE__ */ new Set();
|
|
@@ -295,15 +266,35 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
295
266
|
const raw = searchParams.get("args");
|
|
296
267
|
if (raw) args = JSON.parse(raw);
|
|
297
268
|
} else {
|
|
269
|
+
if (hasContentTypeMismatch(serverFunction.options?.contentType ?? "application/json", req.headers["content-type"])) {
|
|
270
|
+
sendResponse(415, { error: UNSUPPORTED_MEDIA_TYPE });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
298
273
|
const body = await readBody(req);
|
|
299
274
|
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
300
275
|
}
|
|
301
|
-
const
|
|
276
|
+
const requestEvent = {
|
|
277
|
+
request: req,
|
|
278
|
+
response: res,
|
|
279
|
+
nativeEvent: {
|
|
280
|
+
req,
|
|
281
|
+
res
|
|
282
|
+
},
|
|
283
|
+
locals: res.locals ?? {},
|
|
284
|
+
redirect: (location, status = 303) => {
|
|
285
|
+
requestEvent.redirected = {
|
|
286
|
+
location,
|
|
287
|
+
status
|
|
288
|
+
};
|
|
289
|
+
redirect(res, location, status);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const { data, cancel } = provideRequestContext(requestEvent, () => serverFunction.handler(...args));
|
|
302
293
|
const onClose = () => cancel(CLIENT_DISCONNECTED);
|
|
303
294
|
req.on("close", onClose);
|
|
304
295
|
const result = await data;
|
|
305
296
|
req.off("close", onClose);
|
|
306
|
-
if (!res.headersSent) sendResponse(200, { data: result });
|
|
297
|
+
if (!requestEvent.redirected && !res.headersSent) sendResponse(200, { data: result });
|
|
307
298
|
} catch (err) {
|
|
308
299
|
console.error(String(err));
|
|
309
300
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -313,6 +304,6 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
313
304
|
});
|
|
314
305
|
};
|
|
315
306
|
//#endregion
|
|
316
|
-
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
|
|
307
|
+
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody, redirect };
|
|
317
308
|
|
|
318
309
|
//# sourceMappingURL=express.mjs.map
|