@nextrush/adapter-edge 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/adapter.test.ts +89 -0
- package/src/__tests__/body-source.test.ts +85 -0
- package/src/__tests__/context-response-microtrims.test.ts +52 -0
- package/src/__tests__/context.test.ts +536 -0
- package/src/__tests__/default-timeout.test.ts +82 -0
- package/src/__tests__/per-request-work-trim.test.ts +171 -0
- package/src/__tests__/public-surface.test.ts +85 -0
- package/src/__tests__/utils.test.ts +140 -0
- package/src/adapter.ts +333 -0
- package/src/body-source.ts +19 -0
- package/src/context.ts +122 -0
- package/src/index.ts +64 -0
- package/src/utils.ts +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Application } from '@nextrush/core';
|
|
2
|
+
import { WebContextBase } from '@nextrush/runtime';
|
|
3
|
+
export { BodyConsumedError, BodyTooLargeError, EdgeRuntimeInfo, EmptyBodySource, WebBodySource, createEmptyBodySource, createWebBodySource, detectEdgeRuntime, parseQueryString } from '@nextrush/runtime';
|
|
4
|
+
export { HttpError } from '@nextrush/errors';
|
|
5
|
+
export { BodySource, Context, HttpMethod, Middleware, Runtime, RuntimeCapabilities } from '@nextrush/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @nextrush/adapter-edge - Context Implementation
|
|
9
|
+
*
|
|
10
|
+
* Edge-specific Context implementation for Cloudflare Workers, Vercel Edge, etc.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Extends the shared {@link WebContextBase} (F-08, ADR-0010), which owns the
|
|
14
|
+
* response-building logic (json/send/html/redirect/set/getResponse and body
|
|
15
|
+
* suppression, composed from {@link WebResponseBuilder}), the lazy `raw`/
|
|
16
|
+
* `signal`/`triggerTimeout`, the streaming methods, and
|
|
17
|
+
* `get`/`next`/`throw`/`assert` — defined once across the Web adapters rather
|
|
18
|
+
* than copy-pasted per runtime. This file supplies only what is genuinely
|
|
19
|
+
* Edge-specific: `cf-connecting-ip`-aware IP resolution, platform bindings
|
|
20
|
+
* (`env`), and `waitUntil`/`executionContext`.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Edge execution context interface
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Provides access to edge-specific features like `waitUntil` and `passThroughOnException`.
|
|
30
|
+
*/
|
|
31
|
+
interface EdgeExecutionContext {
|
|
32
|
+
/** Extend request lifetime for async operations */
|
|
33
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
34
|
+
/** Pass through to origin on exception */
|
|
35
|
+
passThroughOnException?(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Edge Context implementation
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* Works with any edge runtime that implements the Web Fetch API:
|
|
42
|
+
* - Cloudflare Workers
|
|
43
|
+
* - Vercel Edge Functions
|
|
44
|
+
* - Netlify Edge Functions
|
|
45
|
+
*
|
|
46
|
+
* The response is built internally (via the inherited {@link WebResponseBuilder}
|
|
47
|
+
* composition) and returned via `getResponse()`.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const ctx = new EdgeContext(request);
|
|
52
|
+
* ctx.json({ message: 'Hello from Edge!' });
|
|
53
|
+
* const response = ctx.getResponse();
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
declare class EdgeContext<Env = unknown> extends WebContextBase {
|
|
57
|
+
/** Edge execution context (for waitUntil, etc.) */
|
|
58
|
+
readonly executionContext?: EdgeExecutionContext;
|
|
59
|
+
/**
|
|
60
|
+
* Platform bindings passed by the runtime (audit F-03).
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* On Cloudflare Workers this is the `env` argument of
|
|
64
|
+
* `fetch(request, env, ctx)` — KV, D1, R2, Durable Objects, Queues, secrets.
|
|
65
|
+
* `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).
|
|
66
|
+
*/
|
|
67
|
+
readonly env?: Env;
|
|
68
|
+
constructor(request: Request, executionContext?: EdgeExecutionContext, trustProxy?: boolean, env?: Env);
|
|
69
|
+
/**
|
|
70
|
+
* Extend request lifetime for async operations
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Use this for fire-and-forget operations that should complete
|
|
74
|
+
* after the response is sent (logging, analytics, etc.)
|
|
75
|
+
*/
|
|
76
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a new EdgeContext
|
|
80
|
+
*/
|
|
81
|
+
declare function createEdgeContext<Env = unknown>(request: Request, executionContext?: EdgeExecutionContext, trustProxy?: boolean, env?: Env): EdgeContext<Env>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @nextrush/adapter-edge - Edge Runtime Adapter
|
|
85
|
+
*
|
|
86
|
+
* Connects NextRush Application to Edge runtimes via fetch handlers.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* **Size optimization**: Edge runtimes have strict bundle-size constraints
|
|
90
|
+
* (e.g. Cloudflare Workers 1 MB limit). Import only the packages you need:
|
|
91
|
+
*
|
|
92
|
+
* - Import `@nextrush/core` and `@nextrush/adapter-edge` only.
|
|
93
|
+
* - Avoid `@nextrush/di` unless you need DI (adds `reflect-metadata`).
|
|
94
|
+
* - Tree-shake unused middleware — each middleware is a separate package.
|
|
95
|
+
* - Use `@nextrush/router` only if you have dynamic routes; for simple
|
|
96
|
+
* handlers, use the application directly.
|
|
97
|
+
*
|
|
98
|
+
* @packageDocumentation
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Default request timeout applied when the caller specifies none (F-07,
|
|
103
|
+
* ADR-0010), converging Edge's default contract with Node/Bun/Deno (which all
|
|
104
|
+
* default to a bounded timeout rather than none).
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* 25 000 ms — below the tightest common edge-platform wall limit (Vercel Edge
|
|
108
|
+
* Functions: 25 s), comfortably above typical handler durations, so the
|
|
109
|
+
* framework's clean `504` fires before the platform terminates the isolate.
|
|
110
|
+
* Pass `timeout: 0` to disable the framework timeout entirely (platform limit
|
|
111
|
+
* alone applies) or a positive value to override the default.
|
|
112
|
+
*/
|
|
113
|
+
declare const DEFAULT_EDGE_TIMEOUT_MS = 25000;
|
|
114
|
+
/**
|
|
115
|
+
* Options for the fetch handler
|
|
116
|
+
*/
|
|
117
|
+
interface FetchHandlerOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Custom error handler
|
|
120
|
+
*/
|
|
121
|
+
onError?: (error: Error, ctx: EdgeContext) => Response | Promise<Response>;
|
|
122
|
+
/**
|
|
123
|
+
* Request timeout in milliseconds. The handler races the application logic
|
|
124
|
+
* against a timer and returns a 504 Gateway Timeout if the timer fires
|
|
125
|
+
* first, cancelling the still-running handler via `ctx.signal`.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* Defaults to {@link DEFAULT_EDGE_TIMEOUT_MS} (25 000 ms) when omitted
|
|
129
|
+
* (F-07/ADR-0010) — below the tightest common edge-platform wall limit
|
|
130
|
+
* (Vercel Edge: 25 s), so the framework's clean `504` fires before the
|
|
131
|
+
* platform terminates the isolate. Pass `0` to disable the framework
|
|
132
|
+
* timeout entirely (the platform's own limit still applies).
|
|
133
|
+
*
|
|
134
|
+
* Per-platform CPU/wall limits for context when overriding:
|
|
135
|
+
* - Cloudflare Workers: 30 000 (30 s CPU limit)
|
|
136
|
+
* - Vercel Edge: 25 000 (25 s wall limit)
|
|
137
|
+
*
|
|
138
|
+
* @default DEFAULT_EDGE_TIMEOUT_MS (25000)
|
|
139
|
+
*/
|
|
140
|
+
timeout?: number;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Standard edge fetch handler type
|
|
144
|
+
*/
|
|
145
|
+
type FetchHandler = (request: Request, ctx?: EdgeExecutionContext) => Response | Promise<Response>;
|
|
146
|
+
/**
|
|
147
|
+
* Create a fetch handler for Edge runtimes
|
|
148
|
+
*
|
|
149
|
+
* @param app - NextRush Application instance
|
|
150
|
+
* @param options - Handler options
|
|
151
|
+
* @returns Fetch handler function
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // Cloudflare Workers
|
|
156
|
+
* import { createApp } from '@nextrush/core';
|
|
157
|
+
* import { createFetchHandler } from '@nextrush/adapter-edge';
|
|
158
|
+
*
|
|
159
|
+
* const app = createApp();
|
|
160
|
+
* const handler = createFetchHandler(app);
|
|
161
|
+
*
|
|
162
|
+
* export default {
|
|
163
|
+
* fetch: handler
|
|
164
|
+
* };
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // Vercel Edge Functions
|
|
170
|
+
* import { createApp } from '@nextrush/core';
|
|
171
|
+
* import { createFetchHandler } from '@nextrush/adapter-edge';
|
|
172
|
+
*
|
|
173
|
+
* const app = createApp();
|
|
174
|
+
* export const config = { runtime: 'edge' };
|
|
175
|
+
* export default createFetchHandler(app);
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
declare function createFetchHandler(app: Application, options?: FetchHandlerOptions): FetchHandler;
|
|
179
|
+
/**
|
|
180
|
+
* Cloudflare Workers fetch handler type
|
|
181
|
+
*
|
|
182
|
+
* Cloudflare's module format passes `(request, env, ctx)` where:
|
|
183
|
+
* - `env` contains bindings (KV, D1, R2, secrets, etc.)
|
|
184
|
+
* - `ctx` provides `waitUntil()` and `passThroughOnException()`
|
|
185
|
+
*
|
|
186
|
+
* @typeParam Env - The shape of the Worker's bindings.
|
|
187
|
+
*/
|
|
188
|
+
type CloudflareFetchHandler<Env = Record<string, unknown>> = (request: Request, env: Env, ctx: EdgeExecutionContext) => Response | Promise<Response>;
|
|
189
|
+
/**
|
|
190
|
+
* Create Cloudflare Workers module export
|
|
191
|
+
*
|
|
192
|
+
* @param app - NextRush Application instance
|
|
193
|
+
* @param options - Handler options
|
|
194
|
+
* @returns Cloudflare Workers module export object with correct `(request, env, ctx)` signature
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* The Cloudflare `env` argument (KV, D1, R2, Durable Objects, Queues, secrets)
|
|
198
|
+
* is threaded onto the context as `ctx.env` (audit F-03). Pass a binding type
|
|
199
|
+
* as the generic to make `ctx.env` fully typed inside handlers.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* interface Env { MY_KV: KVNamespace }
|
|
204
|
+
* export default createCloudflareHandler<Env>(app);
|
|
205
|
+
* // inside a handler: ctx.env.MY_KV.get('key')
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare function createCloudflareHandler<Env = Record<string, unknown>>(app: Application, options?: FetchHandlerOptions): {
|
|
209
|
+
fetch: CloudflareFetchHandler<Env>;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Create Vercel Edge Function handler
|
|
213
|
+
*
|
|
214
|
+
* @param app - NextRush Application instance
|
|
215
|
+
* @param options - Handler options
|
|
216
|
+
* @returns Vercel Edge Function handler
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* // api/hello.ts
|
|
221
|
+
* import { createApp } from '@nextrush/core';
|
|
222
|
+
* import { createVercelHandler } from '@nextrush/adapter-edge';
|
|
223
|
+
*
|
|
224
|
+
* const app = createApp();
|
|
225
|
+
*
|
|
226
|
+
* app.use(async (ctx) => {
|
|
227
|
+
* ctx.json({
|
|
228
|
+
* message: 'Hello from Vercel Edge!',
|
|
229
|
+
* region: process.env.VERCEL_REGION
|
|
230
|
+
* });
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* export const config = { runtime: 'edge' };
|
|
234
|
+
* export default createVercelHandler(app);
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
declare function createVercelHandler(app: Application, options?: FetchHandlerOptions): FetchHandler;
|
|
238
|
+
/**
|
|
239
|
+
* Create Netlify Edge Function handler
|
|
240
|
+
*
|
|
241
|
+
* @param app - NextRush Application instance
|
|
242
|
+
* @param options - Handler options
|
|
243
|
+
* @returns Netlify Edge Function handler
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* // netlify/edge-functions/api.ts
|
|
248
|
+
* import { createApp } from '@nextrush/core';
|
|
249
|
+
* import { createNetlifyHandler } from '@nextrush/adapter-edge';
|
|
250
|
+
*
|
|
251
|
+
* const app = createApp();
|
|
252
|
+
*
|
|
253
|
+
* app.use(async (ctx) => {
|
|
254
|
+
* ctx.json({ message: 'Hello from Netlify Edge!' });
|
|
255
|
+
* });
|
|
256
|
+
*
|
|
257
|
+
* export default createNetlifyHandler(app);
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
declare function createNetlifyHandler(app: Application, options?: FetchHandlerOptions): FetchHandler;
|
|
261
|
+
declare const createHandler: typeof createFetchHandler;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @nextrush/adapter-edge - Utility Functions
|
|
265
|
+
*
|
|
266
|
+
* @packageDocumentation
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get Content-Type header value
|
|
271
|
+
*
|
|
272
|
+
* @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s
|
|
273
|
+
* own content-type parsing). Kept for backward compatibility per the public-API
|
|
274
|
+
* contract (deprecate-before-remove); will be removed in a future major.
|
|
275
|
+
*/
|
|
276
|
+
declare function getContentType(headers: Headers): string | undefined;
|
|
277
|
+
/**
|
|
278
|
+
* Get Content-Length header as number
|
|
279
|
+
*
|
|
280
|
+
* @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s
|
|
281
|
+
* own content-length handling). Kept for backward compatibility per the
|
|
282
|
+
* public-API contract (deprecate-before-remove); will be removed in a future
|
|
283
|
+
* major.
|
|
284
|
+
*/
|
|
285
|
+
declare function getContentLength(headers: Headers): number | undefined;
|
|
286
|
+
|
|
287
|
+
export { type CloudflareFetchHandler, DEFAULT_EDGE_TIMEOUT_MS, EdgeContext, type EdgeExecutionContext, type FetchHandler, type FetchHandlerOptions, createCloudflareHandler, createEdgeContext, createFetchHandler, createHandler, createNetlifyHandler, createVercelHandler, getContentLength, getContentType };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/adapter.ts
|
|
5
|
+
import { jsonErrorResponse } from "@nextrush/runtime";
|
|
6
|
+
|
|
7
|
+
// src/context.ts
|
|
8
|
+
import { detectEdgeRuntime, getEdgeClientIp, WebContextBase } from "@nextrush/runtime";
|
|
9
|
+
import { runNDJSONStream, runSSEStream, runTextStream } from "@nextrush/stream";
|
|
10
|
+
var EdgeContext = class extends WebContextBase {
|
|
11
|
+
static {
|
|
12
|
+
__name(this, "EdgeContext");
|
|
13
|
+
}
|
|
14
|
+
/** Edge execution context (for waitUntil, etc.) */
|
|
15
|
+
executionContext;
|
|
16
|
+
/**
|
|
17
|
+
* Platform bindings passed by the runtime (audit F-03).
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* On Cloudflare Workers this is the `env` argument of
|
|
21
|
+
* `fetch(request, env, ctx)` — KV, D1, R2, Durable Objects, Queues, secrets.
|
|
22
|
+
* `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).
|
|
23
|
+
*/
|
|
24
|
+
env;
|
|
25
|
+
constructor(request, executionContext, trustProxy = false, env) {
|
|
26
|
+
const ip = trustProxy ? getEdgeClientIp(request, true) : "";
|
|
27
|
+
const runtime = detectEdgeRuntime().runtime;
|
|
28
|
+
super(request, ip, runtime, {
|
|
29
|
+
runTextStream,
|
|
30
|
+
runSSEStream,
|
|
31
|
+
runNDJSONStream
|
|
32
|
+
});
|
|
33
|
+
this.executionContext = executionContext;
|
|
34
|
+
this.env = env;
|
|
35
|
+
}
|
|
36
|
+
// ===========================================================================
|
|
37
|
+
// Edge-Specific Methods
|
|
38
|
+
// ===========================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Extend request lifetime for async operations
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Use this for fire-and-forget operations that should complete
|
|
44
|
+
* after the response is sent (logging, analytics, etc.)
|
|
45
|
+
*/
|
|
46
|
+
waitUntil(promise) {
|
|
47
|
+
if (this.executionContext?.waitUntil) {
|
|
48
|
+
this.executionContext.waitUntil(promise);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function createEdgeContext(request, executionContext, trustProxy = false, env) {
|
|
53
|
+
return new EdgeContext(request, executionContext, trustProxy, env);
|
|
54
|
+
}
|
|
55
|
+
__name(createEdgeContext, "createEdgeContext");
|
|
56
|
+
|
|
57
|
+
// src/adapter.ts
|
|
58
|
+
var DEFAULT_EDGE_TIMEOUT_MS = 25e3;
|
|
59
|
+
function createFetchHandler(app, options = {}) {
|
|
60
|
+
const run = createRequestRunner(app, options);
|
|
61
|
+
return (request, executionContext) => run(request, executionContext);
|
|
62
|
+
}
|
|
63
|
+
__name(createFetchHandler, "createFetchHandler");
|
|
64
|
+
function createRequestRunner(app, options) {
|
|
65
|
+
const timeout = options.timeout ?? DEFAULT_EDGE_TIMEOUT_MS;
|
|
66
|
+
const trustProxy = app.options.proxy ?? false;
|
|
67
|
+
const TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("timeout");
|
|
68
|
+
let bootPromise = null;
|
|
69
|
+
const ensureBooted = /* @__PURE__ */ __name(() => {
|
|
70
|
+
bootPromise ??= app.ready().then(() => {
|
|
71
|
+
const handler = app.callback();
|
|
72
|
+
app.start();
|
|
73
|
+
return handler;
|
|
74
|
+
});
|
|
75
|
+
return bootPromise;
|
|
76
|
+
}, "ensureBooted");
|
|
77
|
+
return async (request, executionContext, env) => {
|
|
78
|
+
const handler = await ensureBooted();
|
|
79
|
+
const ctx = createEdgeContext(request, executionContext, trustProxy, env);
|
|
80
|
+
try {
|
|
81
|
+
if (timeout > 0) {
|
|
82
|
+
let timerId;
|
|
83
|
+
try {
|
|
84
|
+
const result = await Promise.race([
|
|
85
|
+
handler(ctx).then(() => void 0),
|
|
86
|
+
new Promise((resolve) => {
|
|
87
|
+
timerId = setTimeout(() => {
|
|
88
|
+
resolve(TIMEOUT_SENTINEL);
|
|
89
|
+
}, timeout);
|
|
90
|
+
})
|
|
91
|
+
]);
|
|
92
|
+
if (result === TIMEOUT_SENTINEL) {
|
|
93
|
+
ctx.triggerTimeout();
|
|
94
|
+
return jsonErrorResponse(504, "Gateway Timeout");
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
if (timerId !== void 0) clearTimeout(timerId);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
await handler(ctx);
|
|
101
|
+
}
|
|
102
|
+
if (!ctx.responded && ctx.status === 404) {
|
|
103
|
+
ctx.json({
|
|
104
|
+
error: "Not Found"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return ctx.getResponse();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (options.onError) {
|
|
110
|
+
return options.onError(error, ctx);
|
|
111
|
+
}
|
|
112
|
+
app.logger.error("Request error:", error);
|
|
113
|
+
return jsonErrorResponse(500, "Internal Server Error");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
__name(createRequestRunner, "createRequestRunner");
|
|
118
|
+
function createCloudflareHandler(app, options = {}) {
|
|
119
|
+
const run = createRequestRunner(app, options);
|
|
120
|
+
return {
|
|
121
|
+
fetch: /* @__PURE__ */ __name((request, env, ctx) => run(request, ctx, env), "fetch")
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
__name(createCloudflareHandler, "createCloudflareHandler");
|
|
125
|
+
function createVercelHandler(app, options = {}) {
|
|
126
|
+
return createFetchHandler(app, options);
|
|
127
|
+
}
|
|
128
|
+
__name(createVercelHandler, "createVercelHandler");
|
|
129
|
+
function createNetlifyHandler(app, options = {}) {
|
|
130
|
+
return createFetchHandler(app, options);
|
|
131
|
+
}
|
|
132
|
+
__name(createNetlifyHandler, "createNetlifyHandler");
|
|
133
|
+
var createHandler = createFetchHandler;
|
|
134
|
+
|
|
135
|
+
// src/index.ts
|
|
136
|
+
import { HttpError } from "@nextrush/errors";
|
|
137
|
+
|
|
138
|
+
// src/body-source.ts
|
|
139
|
+
import { createEmptyBodySource, createWebBodySource, EmptyBodySource, WebBodySource } from "@nextrush/runtime";
|
|
140
|
+
|
|
141
|
+
// src/index.ts
|
|
142
|
+
import { BodyConsumedError, BodyTooLargeError } from "@nextrush/runtime";
|
|
143
|
+
|
|
144
|
+
// src/utils.ts
|
|
145
|
+
import { detectEdgeRuntime as detectEdgeRuntime2, parseQueryString } from "@nextrush/runtime";
|
|
146
|
+
function getContentType(headers) {
|
|
147
|
+
return headers.get("content-type") ?? void 0;
|
|
148
|
+
}
|
|
149
|
+
__name(getContentType, "getContentType");
|
|
150
|
+
function getContentLength(headers) {
|
|
151
|
+
const value = headers.get("content-length");
|
|
152
|
+
if (value === null) return void 0;
|
|
153
|
+
const num = parseInt(value, 10);
|
|
154
|
+
return Number.isNaN(num) ? void 0 : num;
|
|
155
|
+
}
|
|
156
|
+
__name(getContentLength, "getContentLength");
|
|
157
|
+
export {
|
|
158
|
+
BodyConsumedError,
|
|
159
|
+
BodyTooLargeError,
|
|
160
|
+
DEFAULT_EDGE_TIMEOUT_MS,
|
|
161
|
+
EdgeContext,
|
|
162
|
+
EmptyBodySource,
|
|
163
|
+
HttpError,
|
|
164
|
+
WebBodySource,
|
|
165
|
+
createCloudflareHandler,
|
|
166
|
+
createEdgeContext,
|
|
167
|
+
createEmptyBodySource,
|
|
168
|
+
createFetchHandler,
|
|
169
|
+
createHandler,
|
|
170
|
+
createNetlifyHandler,
|
|
171
|
+
createVercelHandler,
|
|
172
|
+
createWebBodySource,
|
|
173
|
+
detectEdgeRuntime2 as detectEdgeRuntime,
|
|
174
|
+
getContentLength,
|
|
175
|
+
getContentType,
|
|
176
|
+
parseQueryString
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/context.ts","../src/index.ts","../src/body-source.ts","../src/utils.ts"],"sourcesContent":["/**\n * @nextrush/adapter-edge - Edge Runtime Adapter\n *\n * Connects NextRush Application to Edge runtimes via fetch handlers.\n *\n * @remarks\n * **Size optimization**: Edge runtimes have strict bundle-size constraints\n * (e.g. Cloudflare Workers 1 MB limit). Import only the packages you need:\n *\n * - Import `@nextrush/core` and `@nextrush/adapter-edge` only.\n * - Avoid `@nextrush/di` unless you need DI (adds `reflect-metadata`).\n * - Tree-shake unused middleware — each middleware is a separate package.\n * - Use `@nextrush/router` only if you have dynamic routes; for simple\n * handlers, use the application directly.\n *\n * @packageDocumentation\n */\n\nimport type { Application } from '@nextrush/core';\nimport { jsonErrorResponse } from '@nextrush/runtime';\nimport type { AdapterContextFactory, FetchAdapter } from '@nextrush/types';\nimport { createEdgeContext, EdgeContext, type EdgeExecutionContext } from './context';\n\n/**\n * Default request timeout applied when the caller specifies none (F-07,\n * ADR-0010), converging Edge's default contract with Node/Bun/Deno (which all\n * default to a bounded timeout rather than none).\n *\n * @remarks\n * 25 000 ms — below the tightest common edge-platform wall limit (Vercel Edge\n * Functions: 25 s), comfortably above typical handler durations, so the\n * framework's clean `504` fires before the platform terminates the isolate.\n * Pass `timeout: 0` to disable the framework timeout entirely (platform limit\n * alone applies) or a positive value to override the default.\n */\nexport const DEFAULT_EDGE_TIMEOUT_MS = 25_000;\n\n/**\n * Options for the fetch handler\n */\nexport interface FetchHandlerOptions {\n /**\n * Custom error handler\n */\n onError?: (error: Error, ctx: EdgeContext) => Response | Promise<Response>;\n\n /**\n * Request timeout in milliseconds. The handler races the application logic\n * against a timer and returns a 504 Gateway Timeout if the timer fires\n * first, cancelling the still-running handler via `ctx.signal`.\n *\n * @remarks\n * Defaults to {@link DEFAULT_EDGE_TIMEOUT_MS} (25 000 ms) when omitted\n * (F-07/ADR-0010) — below the tightest common edge-platform wall limit\n * (Vercel Edge: 25 s), so the framework's clean `504` fires before the\n * platform terminates the isolate. Pass `0` to disable the framework\n * timeout entirely (the platform's own limit still applies).\n *\n * Per-platform CPU/wall limits for context when overriding:\n * - Cloudflare Workers: 30 000 (30 s CPU limit)\n * - Vercel Edge: 25 000 (25 s wall limit)\n *\n * @default DEFAULT_EDGE_TIMEOUT_MS (25000)\n */\n timeout?: number;\n}\n\n/**\n * Standard edge fetch handler type\n */\nexport type FetchHandler = (\n request: Request,\n ctx?: EdgeExecutionContext\n) => Response | Promise<Response>;\n\n/**\n * Create a fetch handler for Edge runtimes\n *\n * @param app - NextRush Application instance\n * @param options - Handler options\n * @returns Fetch handler function\n *\n * @example\n * ```typescript\n * // Cloudflare Workers\n * import { createApp } from '@nextrush/core';\n * import { createFetchHandler } from '@nextrush/adapter-edge';\n *\n * const app = createApp();\n * const handler = createFetchHandler(app);\n *\n * export default {\n * fetch: handler\n * };\n * ```\n *\n * @example\n * ```typescript\n * // Vercel Edge Functions\n * import { createApp } from '@nextrush/core';\n * import { createFetchHandler } from '@nextrush/adapter-edge';\n *\n * const app = createApp();\n * export const config = { runtime: 'edge' };\n * export default createFetchHandler(app);\n * ```\n */\nexport function createFetchHandler(\n app: Application,\n options: FetchHandlerOptions = {}\n): FetchHandler {\n const run = createRequestRunner(app, options);\n return (request: Request, executionContext?: EdgeExecutionContext): Promise<Response> =>\n run(request, executionContext);\n}\n\n/**\n * Internal request runner shared by every edge entry point.\n *\n * @remarks\n * Centralizes booting, context creation (threading Cloudflare `env` — F-03),\n * timeout racing with cooperative cancellation (F-08), the header-preserving\n * finalize path (F-02), and error handling — so Cloudflare/Vercel/Netlify\n * handlers cannot drift.\n */\nfunction createRequestRunner(\n app: Application,\n options: FetchHandlerOptions\n): (request: Request, executionContext?: EdgeExecutionContext, env?: unknown) => Promise<Response> {\n // F-07/ADR-0010: default to DEFAULT_EDGE_TIMEOUT_MS when the caller specifies\n // none (`undefined`); `0` remains an explicit opt-out (no framework timeout).\n const timeout = options.timeout ?? DEFAULT_EDGE_TIMEOUT_MS;\n const trustProxy = app.options.proxy ?? false;\n\n /** Sentinel value returned by the timeout racer */\n const TIMEOUT_SENTINEL = Symbol('timeout');\n\n // Edge has no serve()/start() phase, so the deferred boot barrier runs lazily\n // on the first request (idempotent). The boot promise caches the snapshotted\n // request handler, so every request awaits and reuses the same one.\n let bootPromise: Promise<ReturnType<Application['callback']>> | null = null;\n const ensureBooted = (): Promise<ReturnType<Application['callback']>> => {\n bootPromise ??= app.ready().then(() => {\n const handler = app.callback();\n // F-14: mark the app running so `app.isRunning` is consistent with the\n // server adapters. Edge has no server lifetime, so close()/destroy() are\n // intentionally never called — that no-teardown contract is documented\n // on createFetchHandler.\n app.start();\n return handler;\n });\n return bootPromise;\n };\n\n return async (\n request: Request,\n executionContext?: EdgeExecutionContext,\n env?: unknown\n ): Promise<Response> => {\n const handler = await ensureBooted();\n const ctx = createEdgeContext(request, executionContext, trustProxy, env);\n\n try {\n if (timeout > 0) {\n let timerId: ReturnType<typeof setTimeout> | undefined;\n try {\n const result = await Promise.race([\n handler(ctx).then(() => undefined),\n new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {\n timerId = setTimeout(() => {\n resolve(TIMEOUT_SENTINEL);\n }, timeout);\n }),\n ]);\n\n if (result === TIMEOUT_SENTINEL) {\n // F-08: cancel the still-running handler cooperatively via ctx.signal.\n ctx.triggerTimeout();\n return jsonErrorResponse(504, 'Gateway Timeout');\n }\n } finally {\n // F-08: always clear the timer, on both handler-wins and timeout paths.\n if (timerId !== undefined) clearTimeout(timerId);\n }\n } else {\n await handler(ctx);\n }\n\n // F-02: finalize through the context so headers set via ctx.set() survive\n // an implicit/empty response. The 404 default body is written through the\n // same builder, preserving those headers too.\n if (!ctx.responded && ctx.status === 404) {\n ctx.json({ error: 'Not Found' });\n }\n return ctx.getResponse();\n } catch (error) {\n // Custom error handler\n if (options.onError) {\n return options.onError(error as Error, ctx);\n }\n\n // Default error handling\n app.logger.error('Request error:', error);\n\n return jsonErrorResponse(500, 'Internal Server Error');\n }\n };\n}\n\n/**\n * Cloudflare Workers fetch handler type\n *\n * Cloudflare's module format passes `(request, env, ctx)` where:\n * - `env` contains bindings (KV, D1, R2, secrets, etc.)\n * - `ctx` provides `waitUntil()` and `passThroughOnException()`\n *\n * @typeParam Env - The shape of the Worker's bindings.\n */\nexport type CloudflareFetchHandler<Env = Record<string, unknown>> = (\n request: Request,\n env: Env,\n ctx: EdgeExecutionContext\n) => Response | Promise<Response>;\n\n/**\n * Create Cloudflare Workers module export\n *\n * @param app - NextRush Application instance\n * @param options - Handler options\n * @returns Cloudflare Workers module export object with correct `(request, env, ctx)` signature\n *\n * @remarks\n * The Cloudflare `env` argument (KV, D1, R2, Durable Objects, Queues, secrets)\n * is threaded onto the context as `ctx.env` (audit F-03). Pass a binding type\n * as the generic to make `ctx.env` fully typed inside handlers.\n *\n * @example\n * ```typescript\n * interface Env { MY_KV: KVNamespace }\n * export default createCloudflareHandler<Env>(app);\n * // inside a handler: ctx.env.MY_KV.get('key')\n * ```\n */\nexport function createCloudflareHandler<Env = Record<string, unknown>>(\n app: Application,\n options: FetchHandlerOptions = {}\n): { fetch: CloudflareFetchHandler<Env> } {\n const run = createRequestRunner(app, options);\n\n return {\n fetch: (request: Request, env: Env, ctx: EdgeExecutionContext): Promise<Response> =>\n run(request, ctx, env),\n };\n}\n\n/**\n * Create Vercel Edge Function handler\n *\n * @param app - NextRush Application instance\n * @param options - Handler options\n * @returns Vercel Edge Function handler\n *\n * @example\n * ```typescript\n * // api/hello.ts\n * import { createApp } from '@nextrush/core';\n * import { createVercelHandler } from '@nextrush/adapter-edge';\n *\n * const app = createApp();\n *\n * app.use(async (ctx) => {\n * ctx.json({\n * message: 'Hello from Vercel Edge!',\n * region: process.env.VERCEL_REGION\n * });\n * });\n *\n * export const config = { runtime: 'edge' };\n * export default createVercelHandler(app);\n * ```\n */\nexport function createVercelHandler(\n app: Application,\n options: FetchHandlerOptions = {}\n): FetchHandler {\n return createFetchHandler(app, options);\n}\n\n/**\n * Create Netlify Edge Function handler\n *\n * @param app - NextRush Application instance\n * @param options - Handler options\n * @returns Netlify Edge Function handler\n *\n * @example\n * ```typescript\n * // netlify/edge-functions/api.ts\n * import { createApp } from '@nextrush/core';\n * import { createNetlifyHandler } from '@nextrush/adapter-edge';\n *\n * const app = createApp();\n *\n * app.use(async (ctx) => {\n * ctx.json({ message: 'Hello from Netlify Edge!' });\n * });\n *\n * export default createNetlifyHandler(app);\n * ```\n */\nexport function createNetlifyHandler(\n app: Application,\n options: FetchHandlerOptions = {}\n): FetchHandler {\n return createFetchHandler(app, options);\n}\n\n// Alias for backwards compatibility and consistency\nexport const createHandler = createFetchHandler;\n\n// F-01: compile-time conformance guard. If the exported fetch-adapter shape\n// drifts from the shared `FetchAdapter` contract, this stops compiling.\nconst _edgeConformance: FetchAdapter<Application, EdgeExecutionContext> = { createFetchHandler };\nvoid _edgeConformance;\n\n// RFC-NEXTRUSH-ADAPTER-CONTRACT: prove the context factory produces an\n// AdapterContext over the shared Context contract. A drift in createEdgeContext's\n// return type stops compiling here.\nconst _edgeContextFactory: AdapterContextFactory<\n [Request, EdgeExecutionContext?, boolean?, unknown?],\n EdgeContext\n> = createEdgeContext;\nvoid _edgeContextFactory;\n","/**\n * @nextrush/adapter-edge - Context Implementation\n *\n * Edge-specific Context implementation for Cloudflare Workers, Vercel Edge, etc.\n *\n * @remarks\n * Extends the shared {@link WebContextBase} (F-08, ADR-0010), which owns the\n * response-building logic (json/send/html/redirect/set/getResponse and body\n * suppression, composed from {@link WebResponseBuilder}), the lazy `raw`/\n * `signal`/`triggerTimeout`, the streaming methods, and\n * `get`/`next`/`throw`/`assert` — defined once across the Web adapters rather\n * than copy-pasted per runtime. This file supplies only what is genuinely\n * Edge-specific: `cf-connecting-ip`-aware IP resolution, platform bindings\n * (`env`), and `waitUntil`/`executionContext`.\n *\n * @packageDocumentation\n */\n\nimport { detectEdgeRuntime, getEdgeClientIp, WebContextBase } from '@nextrush/runtime';\nimport { runNDJSONStream, runSSEStream, runTextStream } from '@nextrush/stream';\n\n/**\n * Edge execution context interface\n *\n * @remarks\n * Provides access to edge-specific features like `waitUntil` and `passThroughOnException`.\n */\nexport interface EdgeExecutionContext {\n /** Extend request lifetime for async operations */\n waitUntil(promise: Promise<unknown>): void;\n\n /** Pass through to origin on exception */\n passThroughOnException?(): void;\n}\n\n/**\n * Edge Context implementation\n *\n * @remarks\n * Works with any edge runtime that implements the Web Fetch API:\n * - Cloudflare Workers\n * - Vercel Edge Functions\n * - Netlify Edge Functions\n *\n * The response is built internally (via the inherited {@link WebResponseBuilder}\n * composition) and returned via `getResponse()`.\n *\n * @example\n * ```typescript\n * const ctx = new EdgeContext(request);\n * ctx.json({ message: 'Hello from Edge!' });\n * const response = ctx.getResponse();\n * ```\n */\nexport class EdgeContext<Env = unknown> extends WebContextBase {\n /** Edge execution context (for waitUntil, etc.) */\n readonly executionContext?: EdgeExecutionContext;\n\n /**\n * Platform bindings passed by the runtime (audit F-03).\n *\n * @remarks\n * On Cloudflare Workers this is the `env` argument of\n * `fetch(request, env, ctx)` — KV, D1, R2, Durable Objects, Queues, secrets.\n * `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).\n */\n readonly env?: Env;\n\n constructor(\n request: Request,\n executionContext?: EdgeExecutionContext,\n trustProxy = false,\n env?: Env\n ) {\n // Get client IP from CF headers or standard headers.\n //\n // HP-1 trim: Edge has no socket, so when `trustProxy` is false (default) the\n // client IP is `''` — returned directly, with no per-request header-lookup\n // closure and no `getEdgeClientIp` policy call (byte-identical to\n // `getEdgeClientIp(request, false)`, whose `directIp` is `''`). When true,\n // resolution still goes through `getEdgeClientIp`, preserving the Cloudflare\n // `cf-connecting-ip` → `x-forwarded-for` → `x-real-ip` precedence.\n const ip = trustProxy ? getEdgeClientIp(request, true) : '';\n\n // Detect specific edge runtime\n const runtime = detectEdgeRuntime().runtime;\n\n super(request, ip, runtime, { runTextStream, runSSEStream, runNDJSONStream });\n\n this.executionContext = executionContext;\n this.env = env;\n }\n\n // ===========================================================================\n // Edge-Specific Methods\n // ===========================================================================\n\n /**\n * Extend request lifetime for async operations\n *\n * @remarks\n * Use this for fire-and-forget operations that should complete\n * after the response is sent (logging, analytics, etc.)\n */\n waitUntil(promise: Promise<unknown>): void {\n if (this.executionContext?.waitUntil) {\n this.executionContext.waitUntil(promise);\n }\n }\n}\n\n/**\n * Create a new EdgeContext\n */\nexport function createEdgeContext<Env = unknown>(\n request: Request,\n executionContext?: EdgeExecutionContext,\n trustProxy = false,\n env?: Env\n): EdgeContext<Env> {\n return new EdgeContext<Env>(request, executionContext, trustProxy, env);\n}\n","/**\n * @nextrush/adapter-edge - Edge Runtime Adapter for NextRush\n *\n * Provides universal Edge runtime support for:\n * - Cloudflare Workers\n * - Vercel Edge Functions\n * - Netlify Edge Functions\n * - Any runtime supporting the Fetch API\n *\n * @packageDocumentation\n * @module @nextrush/adapter-edge\n */\n\n// Main adapter functions\nexport {\n createCloudflareHandler,\n createFetchHandler,\n createHandler,\n createNetlifyHandler,\n createVercelHandler,\n DEFAULT_EDGE_TIMEOUT_MS,\n type CloudflareFetchHandler, // Alias\n type FetchHandler,\n type FetchHandlerOptions,\n} from './adapter';\n\n// Context exports\nexport { EdgeContext, createEdgeContext, type EdgeExecutionContext } from './context';\n\n// HttpError re-export (uniform across all adapters — audit F-10)\nexport { HttpError } from '@nextrush/errors';\n\n// Body source exports (F-10: previously only `EdgeBodySource` was exported;\n// the rest were dead. Now the full shared surface is wired up.)\nexport {\n createEmptyBodySource,\n createWebBodySource,\n EmptyBodySource,\n WebBodySource,\n} from './body-source';\n\n// Shared error classes (parity with node/bun/deno — audit F-10)\nexport { BodyConsumedError, BodyTooLargeError } from '@nextrush/runtime';\n\n// Utility exports\n/* eslint-disable @typescript-eslint/no-deprecated -- F-09: intentional compat re-export, not a usage site */\nexport {\n detectEdgeRuntime,\n getContentLength,\n getContentType,\n parseQueryString,\n type EdgeRuntimeInfo,\n} from './utils';\n/* eslint-enable @typescript-eslint/no-deprecated */\n\n// Re-export types for convenience (parity with node/bun/deno — audit F-10)\nexport type {\n BodySource,\n Context,\n HttpMethod,\n Middleware,\n Runtime,\n RuntimeCapabilities,\n} from '@nextrush/types';\n","/**\n * @nextrush/adapter-edge - Body Source\n *\n * The bespoke `EdgeBodySource`/`EmptyBodySource`/`concatUint8Arrays` have been\n * collapsed onto the shared cross-runtime {@link WebBodySource} in\n * `@nextrush/runtime` (audit F-04a). Body-reading behavior is now identical\n * across the Web adapters (Bun/Deno/Edge) and fixed in one place.\n *\n * @packageDocumentation\n */\n\nimport {\n createEmptyBodySource,\n createWebBodySource,\n EmptyBodySource,\n WebBodySource,\n} from '@nextrush/runtime';\n\nexport { createEmptyBodySource, createWebBodySource, EmptyBodySource, WebBodySource };\n","/**\n * @nextrush/adapter-edge - Utility Functions\n *\n * @packageDocumentation\n */\n\nexport { detectEdgeRuntime, parseQueryString } from '@nextrush/runtime';\nexport type { EdgeRuntimeInfo } from '@nextrush/runtime';\n\n/**\n * Get Content-Type header value\n *\n * @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s\n * own content-type parsing). Kept for backward compatibility per the public-API\n * contract (deprecate-before-remove); will be removed in a future major.\n */\nexport function getContentType(headers: Headers): string | undefined {\n return headers.get('content-type') ?? undefined;\n}\n\n/**\n * Get Content-Length header as number\n *\n * @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s\n * own content-length handling). Kept for backward compatibility per the\n * public-API contract (deprecate-before-remove); will be removed in a future\n * major.\n */\nexport function getContentLength(headers: Headers): number | undefined {\n const value = headers.get('content-length');\n if (value === null) return undefined;\n const num = parseInt(value, 10);\n return Number.isNaN(num) ? undefined : num;\n}\n"],"mappings":"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","names":["jsonErrorResponse","detectEdgeRuntime","getEdgeClientIp","WebContextBase","runNDJSONStream","runSSEStream","runTextStream","EdgeContext","WebContextBase","executionContext","env","request","trustProxy","ip","getEdgeClientIp","runtime","detectEdgeRuntime","runTextStream","runSSEStream","runNDJSONStream","waitUntil","promise","createEdgeContext","DEFAULT_EDGE_TIMEOUT_MS","createFetchHandler","app","options","run","createRequestRunner","request","executionContext","timeout","trustProxy","proxy","TIMEOUT_SENTINEL","Symbol","bootPromise","ensureBooted","ready","then","handler","callback","start","env","ctx","createEdgeContext","timerId","result","Promise","race","undefined","resolve","setTimeout","triggerTimeout","jsonErrorResponse","clearTimeout","responded","status","json","error","getResponse","onError","logger","createCloudflareHandler","fetch","createVercelHandler","createNetlifyHandler","createHandler","HttpError","createEmptyBodySource","createWebBodySource","EmptyBodySource","WebBodySource","BodyConsumedError","BodyTooLargeError","detectEdgeRuntime","parseQueryString","getContentType","headers","get","undefined","getContentLength","value","num","parseInt","Number","isNaN"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextrush/adapter-edge",
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
|
+
"description": "Edge runtime adapter for NextRush (Cloudflare Workers, Vercel Edge, etc.)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@nextrush/core": "^4.0.0-beta.0",
|
|
21
|
+
"@nextrush/errors": "^4.0.0-beta.0",
|
|
22
|
+
"@nextrush/runtime": "^4.0.0-beta.0",
|
|
23
|
+
"@nextrush/stream": "^1.0.0-beta.0",
|
|
24
|
+
"@nextrush/types": "^4.0.0-beta.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.5.1",
|
|
28
|
+
"typescript": "^6.0.3",
|
|
29
|
+
"vitest": "^4.1.10"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"nextrush",
|
|
33
|
+
"adapter",
|
|
34
|
+
"edge",
|
|
35
|
+
"cloudflare",
|
|
36
|
+
"workers",
|
|
37
|
+
"vercel",
|
|
38
|
+
"http",
|
|
39
|
+
"framework"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/0xTanzim/nextRush.git",
|
|
45
|
+
"directory": "packages/adapters/edge"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/0xTanzim/nextRush/tree/main/packages/adapters/edge#readme",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.0.0"
|
|
53
|
+
},
|
|
54
|
+
"author": {
|
|
55
|
+
"name": "Tanzim Hossain",
|
|
56
|
+
"email": "tanzimhossain2@gmail.com",
|
|
57
|
+
"url": "https://github.com/0xTanzim"
|
|
58
|
+
},
|
|
59
|
+
"sideEffects": false,
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"lint": "eslint src --ignore-pattern '**/__tests__/**'",
|
|
67
|
+
"clean": "rm -rf dist"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Adapter behavior tests
|
|
3
|
+
*
|
|
4
|
+
* Regression guards for:
|
|
5
|
+
* - F-02: headers set via ctx.set() survive an implicit/empty (and 404) response.
|
|
6
|
+
* - F-03: Cloudflare `env` bindings are threaded onto ctx.env.
|
|
7
|
+
* - F-14: app.isRunning becomes true after the first request boots the app.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createApp } from '@nextrush/core';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { createCloudflareHandler, createFetchHandler } from '../adapter';
|
|
13
|
+
|
|
14
|
+
describe('edge adapter — F-02 header preservation', () => {
|
|
15
|
+
it('keeps headers set via ctx.set() on a header-only (no body method) response', async () => {
|
|
16
|
+
const app = createApp();
|
|
17
|
+
app.use(async (ctx) => {
|
|
18
|
+
ctx.set('X-Custom', 'kept');
|
|
19
|
+
ctx.set('Access-Control-Allow-Origin', '*');
|
|
20
|
+
ctx.status = 204;
|
|
21
|
+
});
|
|
22
|
+
const handler = createFetchHandler(app);
|
|
23
|
+
|
|
24
|
+
const res = await handler(new Request('http://localhost/'));
|
|
25
|
+
expect(res.status).toBe(204);
|
|
26
|
+
expect(res.headers.get('X-Custom')).toBe('kept');
|
|
27
|
+
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('keeps headers set via ctx.set() on the default 404 response', async () => {
|
|
31
|
+
const app = createApp();
|
|
32
|
+
app.use(async (ctx) => {
|
|
33
|
+
ctx.set('X-Trace', 'abc');
|
|
34
|
+
ctx.status = 404;
|
|
35
|
+
});
|
|
36
|
+
const handler = createFetchHandler(app);
|
|
37
|
+
|
|
38
|
+
const res = await handler(new Request('http://localhost/missing'));
|
|
39
|
+
expect(res.status).toBe(404);
|
|
40
|
+
expect(res.headers.get('X-Trace')).toBe('abc');
|
|
41
|
+
expect(await res.json()).toEqual({ error: 'Not Found' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('accumulates multiple Set-Cookie headers on an empty response', async () => {
|
|
45
|
+
const app = createApp();
|
|
46
|
+
app.use(async (ctx) => {
|
|
47
|
+
ctx.set('Set-Cookie', 'a=1; Path=/');
|
|
48
|
+
ctx.set('Set-Cookie', 'b=2; Path=/');
|
|
49
|
+
ctx.status = 204;
|
|
50
|
+
});
|
|
51
|
+
const handler = createFetchHandler(app);
|
|
52
|
+
|
|
53
|
+
const res = await handler(new Request('http://localhost/'));
|
|
54
|
+
expect(res.headers.getSetCookie()).toHaveLength(2);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('edge adapter — F-03 Cloudflare env', () => {
|
|
59
|
+
it('threads env onto ctx.env for the Cloudflare handler', async () => {
|
|
60
|
+
interface Env {
|
|
61
|
+
MY_SECRET: string;
|
|
62
|
+
}
|
|
63
|
+
const app = createApp();
|
|
64
|
+
app.use(async (ctx) => {
|
|
65
|
+
const env = ctx.env as Env | undefined;
|
|
66
|
+
ctx.json({ secret: env?.MY_SECRET ?? null });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const mod = createCloudflareHandler<Env>(app);
|
|
70
|
+
const res = await mod.fetch(
|
|
71
|
+
new Request('http://localhost/'),
|
|
72
|
+
{ MY_SECRET: 'shhh' },
|
|
73
|
+
{ waitUntil: () => undefined }
|
|
74
|
+
);
|
|
75
|
+
expect(await res.json()).toEqual({ secret: 'shhh' });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('edge adapter — F-14 lifecycle', () => {
|
|
80
|
+
it('marks the app running after the first request boots it', async () => {
|
|
81
|
+
const app = createApp();
|
|
82
|
+
app.use(async (ctx) => ctx.json({ ok: true }));
|
|
83
|
+
expect(app.isRunning).toBe(false);
|
|
84
|
+
|
|
85
|
+
const handler = createFetchHandler(app);
|
|
86
|
+
await handler(new Request('http://localhost/'));
|
|
87
|
+
expect(app.isRunning).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|