astro-routify 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -91
- package/dist/core/RouteTrie.js +52 -24
- package/dist/core/RouterBuilder.js +42 -9
- package/dist/core/defineGroup.d.ts +1 -0
- package/dist/core/defineGroup.js +1 -0
- package/dist/core/defineHandler.d.ts +19 -11
- package/dist/core/defineHandler.js +2 -13
- package/dist/core/defineRoute.d.ts +6 -1
- package/dist/core/defineRoute.js +8 -3
- package/dist/core/defineRouter.d.ts +2 -2
- package/dist/core/defineRouter.js +18 -2
- package/dist/core/internal/createJsonStreamRoute.d.ts +1 -1
- package/dist/core/middlewares.d.ts +1 -1
- package/dist/core/middlewares.js +4 -4
- package/dist/core/openapi.js +32 -5
- package/dist/core/responseHelpers.d.ts +9 -5
- package/dist/core/responseHelpers.js +92 -17
- package/dist/core/stream.d.ts +3 -3
- package/dist/core/stream.js +3 -3
- package/dist/core/streamJsonArray.d.ts +1 -1
- package/dist/core/streamJsonArray.js +1 -1
- package/dist/index.d.ts +42 -24
- package/package.json +1 -1
package/dist/core/defineGroup.js
CHANGED
|
@@ -20,6 +20,7 @@ export class RouteGroup {
|
|
|
20
20
|
* @param basePath - The common prefix for all routes in the group (e.g. "/users")
|
|
21
21
|
*/
|
|
22
22
|
constructor(basePath) {
|
|
23
|
+
this._routifyType = 'group';
|
|
23
24
|
this.routes = [];
|
|
24
25
|
this.middlewares = [];
|
|
25
26
|
this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import type { APIContext, APIRoute } from 'astro';
|
|
2
|
-
import { type
|
|
2
|
+
import { type HandlerResult } from './responseHelpers';
|
|
3
3
|
/**
|
|
4
4
|
* Enhanced Astro context for Routify.
|
|
5
5
|
*/
|
|
6
|
-
export interface RoutifyContext extends APIContext {
|
|
6
|
+
export interface RoutifyContext<State = Record<string, any>> extends APIContext {
|
|
7
7
|
/**
|
|
8
8
|
* Parsed query parameters from the URL.
|
|
9
|
+
* Note: If multiple parameters have the same key, only the last one is included.
|
|
10
|
+
* Use `searchParams` for full multi-value support.
|
|
9
11
|
*/
|
|
10
|
-
query: Record<string, string>;
|
|
12
|
+
query: Record<string, string | string[]>;
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
14
|
+
* Full URLSearchParams object for the incoming request.
|
|
13
15
|
*/
|
|
14
|
-
|
|
16
|
+
searchParams: URLSearchParams;
|
|
17
|
+
/**
|
|
18
|
+
* Shared state container for middlewares and handlers (e.g., validation results).
|
|
19
|
+
* This is where middlewares can store information for subsequent handlers.
|
|
20
|
+
*/
|
|
21
|
+
state: State;
|
|
15
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Convenience type alias for RoutifyContext.
|
|
25
|
+
*/
|
|
26
|
+
export type Context<State = Record<string, any>> = RoutifyContext<State>;
|
|
16
27
|
/**
|
|
17
28
|
* A middleware function that can modify the context or short-circuit the response.
|
|
18
29
|
*/
|
|
19
|
-
export type Middleware = (ctx: RoutifyContext
|
|
30
|
+
export type Middleware<State = any> = (ctx: RoutifyContext<State>, next: () => Promise<Response>) => Promise<Response> | Response;
|
|
20
31
|
/**
|
|
21
|
-
* A flexible route handler that can return
|
|
22
|
-
* - a native `Response` object,
|
|
23
|
-
* - a structured `ResultResponse` object,
|
|
24
|
-
* - or a file stream (Blob, ArrayBuffer, or ReadableStream).
|
|
32
|
+
* A flexible route handler that can return various types.
|
|
25
33
|
*/
|
|
26
|
-
export type Handler = (ctx: RoutifyContext) => Promise<
|
|
34
|
+
export type Handler<State = any> = (ctx: RoutifyContext<State>) => Promise<HandlerResult> | HandlerResult;
|
|
27
35
|
/**
|
|
28
36
|
* Wraps a `Handler` function into an `APIRoute` that:
|
|
29
37
|
* - logs requests and responses,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { internalError, toAstroResponse
|
|
1
|
+
import { internalError, toAstroResponse } from './responseHelpers';
|
|
2
2
|
/**
|
|
3
3
|
* Logs the incoming request method and path to the console.
|
|
4
4
|
*/
|
|
@@ -33,18 +33,7 @@ export function defineHandler(handler) {
|
|
|
33
33
|
logResponse(result.status, start);
|
|
34
34
|
return result;
|
|
35
35
|
}
|
|
36
|
-
//
|
|
37
|
-
if (result?.body instanceof Blob ||
|
|
38
|
-
result?.body instanceof ArrayBuffer ||
|
|
39
|
-
isReadableStream(result?.body)) {
|
|
40
|
-
const res = new Response(result.body, {
|
|
41
|
-
status: result.status,
|
|
42
|
-
headers: result.headers,
|
|
43
|
-
});
|
|
44
|
-
logResponse(res.status, start);
|
|
45
|
-
return res;
|
|
46
|
-
}
|
|
47
|
-
// Structured ResultResponse → native Astro Response
|
|
36
|
+
// Structured ResultResponse or other HandlerResult → native Astro Response
|
|
48
37
|
const finalResponse = toAstroResponse(result);
|
|
49
38
|
logResponse(finalResponse.status, start);
|
|
50
39
|
return finalResponse;
|
|
@@ -21,9 +21,14 @@ export interface Route {
|
|
|
21
21
|
*/
|
|
22
22
|
middlewares?: Middleware[];
|
|
23
23
|
/**
|
|
24
|
-
* Optional metadata for the route (e.g
|
|
24
|
+
* Optional metadata for the route (e.g. for OpenAPI generation).
|
|
25
25
|
*/
|
|
26
26
|
metadata?: Record<string, any>;
|
|
27
|
+
/**
|
|
28
|
+
* Internal marker to identify the object type during module discovery.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
_routifyType?: 'route';
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Defines a route using a `Route` object.
|
package/dist/core/defineRoute.js
CHANGED
|
@@ -18,6 +18,7 @@ export function defineRoute(methodOrRoute, maybePathOrAutoRegister, maybeHandler
|
|
|
18
18
|
};
|
|
19
19
|
autoRegister = !!maybeAutoRegister;
|
|
20
20
|
}
|
|
21
|
+
route._routifyType = 'route';
|
|
21
22
|
validateRoute(route);
|
|
22
23
|
if (autoRegister) {
|
|
23
24
|
globalRegistry.register(route);
|
|
@@ -37,6 +38,9 @@ export function validateRoute({ method, path }) {
|
|
|
37
38
|
if (!ALLOWED_HTTP_METHODS.has(method)) {
|
|
38
39
|
throw new Error(`Unsupported HTTP method in route: ${method}`);
|
|
39
40
|
}
|
|
41
|
+
if (path.includes('**') && !path.endsWith('**')) {
|
|
42
|
+
throw new Error(`Catch-all '**' is only allowed at the end of a path: ${path}`);
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* Checks if an object implements the `Route` interface.
|
|
@@ -47,7 +51,8 @@ export function validateRoute({ method, path }) {
|
|
|
47
51
|
export function isRoute(obj) {
|
|
48
52
|
return (obj &&
|
|
49
53
|
typeof obj === 'object' &&
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
(obj._routifyType === 'route' ||
|
|
55
|
+
(typeof obj.method === 'string' &&
|
|
56
|
+
typeof obj.path === 'string' &&
|
|
57
|
+
typeof obj.handler === 'function')));
|
|
53
58
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { type RoutifyContext } from './defineHandler';
|
|
3
|
-
import {
|
|
3
|
+
import { notFound, type HandlerResult } from './responseHelpers';
|
|
4
4
|
import type { Route } from './defineRoute';
|
|
5
5
|
/**
|
|
6
6
|
* Optional configuration for the router instance.
|
|
@@ -23,7 +23,7 @@ export interface RouterOptions {
|
|
|
23
23
|
/**
|
|
24
24
|
* Custom error handler for the router.
|
|
25
25
|
*/
|
|
26
|
-
onError?: (error: unknown, ctx: RoutifyContext) =>
|
|
26
|
+
onError?: (error: unknown, ctx: RoutifyContext) => HandlerResult | Response;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Defines a router that dynamically matches registered routes based on method and path.
|
|
@@ -42,8 +42,24 @@ export function defineRouter(routes, options = {}) {
|
|
|
42
42
|
const handler = defineHandler(async (routifyCtx) => {
|
|
43
43
|
const url = new URL(routifyCtx.request.url);
|
|
44
44
|
const pathname = url.pathname;
|
|
45
|
-
routifyCtx.
|
|
46
|
-
|
|
45
|
+
routifyCtx.searchParams = url.searchParams;
|
|
46
|
+
const query = {};
|
|
47
|
+
url.searchParams.forEach((value, key) => {
|
|
48
|
+
const existing = query[key];
|
|
49
|
+
if (existing !== undefined) {
|
|
50
|
+
if (Array.isArray(existing)) {
|
|
51
|
+
existing.push(value);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
query[key] = [existing, value];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
query[key] = value;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
routifyCtx.query = query;
|
|
62
|
+
routifyCtx.state = {};
|
|
47
63
|
let path = pathname;
|
|
48
64
|
if (basePath !== '') {
|
|
49
65
|
if (!pathname.startsWith(basePath)) {
|
|
@@ -3,7 +3,7 @@ import { HttpMethod } from '../HttpMethod';
|
|
|
3
3
|
import { type Route } from '../defineRoute';
|
|
4
4
|
type JsonValue = any;
|
|
5
5
|
/**
|
|
6
|
-
* A writer interface for streaming JSON
|
|
6
|
+
* A writer interface for streaming JSON state to the response body.
|
|
7
7
|
* Supports both NDJSON and array formats.
|
|
8
8
|
*/
|
|
9
9
|
export interface JsonStreamWriter {
|
|
@@ -42,6 +42,6 @@ export interface ValidationSchema {
|
|
|
42
42
|
* Middleware for request validation.
|
|
43
43
|
* Supports any schema library with a `safeParse` method (like Zod).
|
|
44
44
|
*
|
|
45
|
-
* Validated
|
|
45
|
+
* Validated state is stored in `ctx.state.body`, `ctx.state.query`, etc.
|
|
46
46
|
*/
|
|
47
47
|
export declare function validate(schema: ValidationSchema): Middleware;
|
package/dist/core/middlewares.js
CHANGED
|
@@ -77,7 +77,7 @@ export function securityHeaders() {
|
|
|
77
77
|
* Middleware for request validation.
|
|
78
78
|
* Supports any schema library with a `safeParse` method (like Zod).
|
|
79
79
|
*
|
|
80
|
-
* Validated
|
|
80
|
+
* Validated state is stored in `ctx.state.body`, `ctx.state.query`, etc.
|
|
81
81
|
*/
|
|
82
82
|
export function validate(schema) {
|
|
83
83
|
return async (ctx, next) => {
|
|
@@ -85,13 +85,13 @@ export function validate(schema) {
|
|
|
85
85
|
const result = schema.params.safeParse(ctx.params);
|
|
86
86
|
if (!result.success)
|
|
87
87
|
return toAstroResponse(badRequest({ error: 'Invalid parameters', details: result.error }));
|
|
88
|
-
ctx.
|
|
88
|
+
ctx.state.params = result.data;
|
|
89
89
|
}
|
|
90
90
|
if (schema.query) {
|
|
91
91
|
const result = schema.query.safeParse(ctx.query);
|
|
92
92
|
if (!result.success)
|
|
93
93
|
return toAstroResponse(badRequest({ error: 'Invalid query string', details: result.error }));
|
|
94
|
-
ctx.
|
|
94
|
+
ctx.state.query = result.data;
|
|
95
95
|
}
|
|
96
96
|
if (schema.body) {
|
|
97
97
|
try {
|
|
@@ -99,7 +99,7 @@ export function validate(schema) {
|
|
|
99
99
|
const result = schema.body.safeParse(body);
|
|
100
100
|
if (!result.success)
|
|
101
101
|
return toAstroResponse(badRequest({ error: 'Invalid request body', details: result.error }));
|
|
102
|
-
ctx.
|
|
102
|
+
ctx.state.body = result.data;
|
|
103
103
|
}
|
|
104
104
|
catch (e) {
|
|
105
105
|
return toAstroResponse(badRequest({ error: 'Invalid JSON body' }));
|
package/dist/core/openapi.js
CHANGED
|
@@ -25,7 +25,11 @@ export function generateOpenAPI(router, options) {
|
|
|
25
25
|
const path = route.path;
|
|
26
26
|
// OpenAPI paths must start with / and should not include the basePath if it's in servers
|
|
27
27
|
// Convert :param or :param(regex) to {param}
|
|
28
|
-
|
|
28
|
+
// Convert ** to {rest} and * to {any}
|
|
29
|
+
let openApiPath = path
|
|
30
|
+
.replace(/:([a-zA-Z0-9_]+)(\(.*?\))?/g, '{$1}')
|
|
31
|
+
.replace(/\*\*/g, '{rest}')
|
|
32
|
+
.replace(/\*/g, '{any}');
|
|
29
33
|
if (!spec.paths[openApiPath])
|
|
30
34
|
spec.paths[openApiPath] = {};
|
|
31
35
|
const method = route.method.toLowerCase();
|
|
@@ -42,14 +46,37 @@ export function generateOpenAPI(router, options) {
|
|
|
42
46
|
}
|
|
43
47
|
};
|
|
44
48
|
// Extract path parameters
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const paramRegex = /:([a-zA-Z0-9_]+)(?:\((.*?)\))?/g;
|
|
50
|
+
let pMatch;
|
|
51
|
+
while ((pMatch = paramRegex.exec(path)) !== null) {
|
|
52
|
+
const name = pMatch[1];
|
|
53
|
+
const pattern = pMatch[2];
|
|
48
54
|
spec.paths[openApiPath][method].parameters.push({
|
|
49
55
|
name,
|
|
50
56
|
in: 'path',
|
|
51
57
|
required: true,
|
|
52
|
-
schema: {
|
|
58
|
+
schema: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
pattern: pattern ? pattern : undefined
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (path.includes('**')) {
|
|
65
|
+
spec.paths[openApiPath][method].parameters.push({
|
|
66
|
+
name: 'rest',
|
|
67
|
+
in: 'path',
|
|
68
|
+
required: true,
|
|
69
|
+
schema: { type: 'string' },
|
|
70
|
+
description: 'Catch-all path'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (path.includes('*')) {
|
|
74
|
+
spec.paths[openApiPath][method].parameters.push({
|
|
75
|
+
name: 'any',
|
|
76
|
+
in: 'path',
|
|
77
|
+
required: true,
|
|
78
|
+
schema: { type: 'string' },
|
|
79
|
+
description: 'Wildcard segment'
|
|
53
80
|
});
|
|
54
81
|
}
|
|
55
82
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { BodyInit, HeadersInit } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* Supported types that can be returned from a route handler.
|
|
4
|
+
*/
|
|
5
|
+
export type HandlerResult = Response | ResultResponse | string | number | boolean | object | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array> | Blob | FormData | URLSearchParams | null | undefined;
|
|
2
6
|
/**
|
|
3
7
|
* A standardized shape for internal route result objects.
|
|
4
8
|
* These are later converted into native `Response` instances.
|
|
@@ -70,7 +74,7 @@ export declare const internalError: (err: unknown, headers?: HeadersInit) => Res
|
|
|
70
74
|
/**
|
|
71
75
|
* Sends a binary or stream-based file response with optional content-disposition.
|
|
72
76
|
*
|
|
73
|
-
* @param content - The file
|
|
77
|
+
* @param content - The file state (Blob, ArrayBuffer, or stream)
|
|
74
78
|
* @param contentType - MIME type of the file
|
|
75
79
|
* @param fileName - Optional download filename
|
|
76
80
|
* @param headers - Optional extra headers
|
|
@@ -85,12 +89,12 @@ export declare const fileResponse: (content: Blob | ArrayBuffer | ReadableStream
|
|
|
85
89
|
*/
|
|
86
90
|
export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
|
|
87
91
|
/**
|
|
88
|
-
* Converts an internal `ResultResponse` into a native `Response` object
|
|
92
|
+
* Converts an internal `ResultResponse` or any `HandlerResult` into a native `Response` object
|
|
89
93
|
* for use inside Astro API routes.
|
|
90
94
|
*
|
|
91
|
-
* Automatically applies
|
|
95
|
+
* Automatically applies appropriate Content-Type headers.
|
|
92
96
|
*
|
|
93
|
-
* @param result - A ResultResponse returned from route handler
|
|
97
|
+
* @param result - A ResultResponse or other supported type returned from route handler
|
|
94
98
|
* @returns A native Response
|
|
95
99
|
*/
|
|
96
|
-
export declare function toAstroResponse(result:
|
|
100
|
+
export declare function toAstroResponse(result: HandlerResult): Response;
|
|
@@ -63,7 +63,7 @@ export const internalError = (err, headers) => {
|
|
|
63
63
|
/**
|
|
64
64
|
* Sends a binary or stream-based file response with optional content-disposition.
|
|
65
65
|
*
|
|
66
|
-
* @param content - The file
|
|
66
|
+
* @param content - The file state (Blob, ArrayBuffer, or stream)
|
|
67
67
|
* @param contentType - MIME type of the file
|
|
68
68
|
* @param fileName - Optional download filename
|
|
69
69
|
* @param headers - Optional extra headers
|
|
@@ -101,28 +101,103 @@ export function isReadableStream(value) {
|
|
|
101
101
|
typeof value.getReader === 'function');
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* Converts an internal `ResultResponse` into a native `Response` object
|
|
104
|
+
* Converts an internal `ResultResponse` or any `HandlerResult` into a native `Response` object
|
|
105
105
|
* for use inside Astro API routes.
|
|
106
106
|
*
|
|
107
|
-
* Automatically applies
|
|
107
|
+
* Automatically applies appropriate Content-Type headers.
|
|
108
108
|
*
|
|
109
|
-
* @param result - A ResultResponse returned from route handler
|
|
109
|
+
* @param result - A ResultResponse or other supported type returned from route handler
|
|
110
110
|
* @returns A native Response
|
|
111
111
|
*/
|
|
112
112
|
export function toAstroResponse(result) {
|
|
113
|
-
if (
|
|
113
|
+
if (result instanceof Response)
|
|
114
|
+
return result;
|
|
115
|
+
if (result === undefined) {
|
|
114
116
|
return new Response(null, { status: 204 });
|
|
115
|
-
const { status, body, headers } = result;
|
|
116
|
-
if (body === undefined || body === null) {
|
|
117
|
-
return new Response(null, { status, headers });
|
|
118
117
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
if (result === null) {
|
|
119
|
+
return new Response('null', {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// If it's a ResultResponse object (has status)
|
|
125
|
+
if (typeof result === 'object' &&
|
|
126
|
+
'status' in result &&
|
|
127
|
+
typeof result.status === 'number') {
|
|
128
|
+
const { status, body, headers } = result;
|
|
129
|
+
if (body === undefined) {
|
|
130
|
+
return new Response(null, { status, headers });
|
|
131
|
+
}
|
|
132
|
+
if (body === null) {
|
|
133
|
+
const finalHeaders = new Headers();
|
|
134
|
+
finalHeaders.set('Content-Type', 'application/json; charset=utf-8');
|
|
135
|
+
if (headers) {
|
|
136
|
+
const h = new Headers(headers);
|
|
137
|
+
h.forEach((value, key) => finalHeaders.set(key, value));
|
|
138
|
+
}
|
|
139
|
+
return new Response('null', { status, headers: finalHeaders });
|
|
140
|
+
}
|
|
141
|
+
if (body instanceof Response)
|
|
142
|
+
return body;
|
|
143
|
+
const isJson = typeof body === 'number' ||
|
|
144
|
+
typeof body === 'boolean' ||
|
|
145
|
+
isPlainObject(body) ||
|
|
146
|
+
Array.isArray(body);
|
|
147
|
+
const isBinary = body instanceof ArrayBuffer ||
|
|
148
|
+
body instanceof Uint8Array ||
|
|
149
|
+
body instanceof Blob ||
|
|
150
|
+
isReadableStream(body);
|
|
151
|
+
const finalHeaders = new Headers();
|
|
152
|
+
// 1. Apply inferred defaults
|
|
153
|
+
if (isJson) {
|
|
154
|
+
finalHeaders.set('Content-Type', 'application/json; charset=utf-8');
|
|
155
|
+
}
|
|
156
|
+
else if (isBinary) {
|
|
157
|
+
finalHeaders.set('Content-Type', 'application/octet-stream');
|
|
158
|
+
}
|
|
159
|
+
// 2. Explicit headers take precedence
|
|
160
|
+
if (headers) {
|
|
161
|
+
const h = new Headers(headers);
|
|
162
|
+
h.forEach((value, key) => {
|
|
163
|
+
finalHeaders.set(key, value);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return new Response(isJson ? JSON.stringify(body) : body, {
|
|
167
|
+
status,
|
|
168
|
+
headers: finalHeaders,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Direct values
|
|
172
|
+
if (typeof result === 'string') {
|
|
173
|
+
return new Response(result, {
|
|
174
|
+
status: 200,
|
|
175
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (typeof result === 'number' || typeof result === 'boolean') {
|
|
179
|
+
return new Response(JSON.stringify(result), {
|
|
180
|
+
status: 200,
|
|
181
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (result instanceof ArrayBuffer ||
|
|
185
|
+
result instanceof Uint8Array ||
|
|
186
|
+
result instanceof Blob ||
|
|
187
|
+
isReadableStream(result)) {
|
|
188
|
+
return new Response(result, {
|
|
189
|
+
status: 200,
|
|
190
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (result instanceof FormData || result instanceof URLSearchParams) {
|
|
194
|
+
return new Response(result, { status: 200 });
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(result) || isPlainObject(result)) {
|
|
197
|
+
return new Response(JSON.stringify(result), {
|
|
198
|
+
status: 200,
|
|
199
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return new Response(null, { status: 204 });
|
|
128
203
|
}
|
package/dist/core/stream.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Route } from './defineRoute';
|
|
|
3
3
|
import { HttpMethod } from './HttpMethod';
|
|
4
4
|
import { HeadersInit } from 'undici';
|
|
5
5
|
/**
|
|
6
|
-
* A writer for streaming raw
|
|
6
|
+
* A writer for streaming raw state to the response body.
|
|
7
7
|
*
|
|
8
8
|
* This is used inside the `stream()` route handler to emit bytes
|
|
9
9
|
* or strings directly to the client with backpressure awareness.
|
|
@@ -47,7 +47,7 @@ export interface StreamOptions {
|
|
|
47
47
|
keepAlive?: boolean;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Defines a generic streaming route that can write raw chunks of
|
|
50
|
+
* Defines a generic streaming route that can write raw chunks of state
|
|
51
51
|
* to the response in real time using a `ReadableStream`.
|
|
52
52
|
*
|
|
53
53
|
* Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
|
|
@@ -56,7 +56,7 @@ export interface StreamOptions {
|
|
|
56
56
|
* @example
|
|
57
57
|
* stream('/clock', async ({ response }) => {
|
|
58
58
|
* const timer = setInterval(() => {
|
|
59
|
-
* response.write(`
|
|
59
|
+
* response.write(`state: ${new Date().toISOString()}\n\n`);
|
|
60
60
|
* }, 1000);
|
|
61
61
|
*
|
|
62
62
|
* setTimeout(() => {
|
package/dist/core/stream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineRoute } from './defineRoute';
|
|
2
2
|
import { HttpMethod } from './HttpMethod';
|
|
3
3
|
/**
|
|
4
|
-
* Defines a generic streaming route that can write raw chunks of
|
|
4
|
+
* Defines a generic streaming route that can write raw chunks of state
|
|
5
5
|
* to the response in real time using a `ReadableStream`.
|
|
6
6
|
*
|
|
7
7
|
* Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
|
|
@@ -10,7 +10,7 @@ import { HttpMethod } from './HttpMethod';
|
|
|
10
10
|
* @example
|
|
11
11
|
* stream('/clock', async ({ response }) => {
|
|
12
12
|
* const timer = setInterval(() => {
|
|
13
|
-
* response.write(`
|
|
13
|
+
* response.write(`state: ${new Date().toISOString()}\n\n`);
|
|
14
14
|
* }, 1000);
|
|
15
15
|
*
|
|
16
16
|
* setTimeout(() => {
|
|
@@ -36,7 +36,7 @@ export function stream(path, handler, options) {
|
|
|
36
36
|
if (closed || !controllerRef)
|
|
37
37
|
return;
|
|
38
38
|
const bytes = typeof chunk === 'string'
|
|
39
|
-
? (contentType === 'text/event-stream' ? encoder.encode(`
|
|
39
|
+
? (contentType === 'text/event-stream' ? encoder.encode(`state: ${chunk}\n\n`) : encoder.encode(chunk))
|
|
40
40
|
: chunk;
|
|
41
41
|
controllerRef.enqueue(bytes);
|
|
42
42
|
},
|
|
@@ -6,7 +6,7 @@ import { JsonStreamWriter } from './internal/createJsonStreamRoute';
|
|
|
6
6
|
* Defines a JSON streaming route that emits a valid JSON array.
|
|
7
7
|
*
|
|
8
8
|
* This helper returns a valid `application/json` response containing
|
|
9
|
-
* a streamable array of JSON values. Useful for large
|
|
9
|
+
* a streamable array of JSON values. Useful for large state exports
|
|
10
10
|
* or APIs where the full array can be streamed as it's generated.
|
|
11
11
|
*
|
|
12
12
|
* Unlike `streamJsonND()`, this wraps all values in `[` and `]`
|
|
@@ -4,7 +4,7 @@ import { createJsonStreamRoute } from './internal/createJsonStreamRoute';
|
|
|
4
4
|
* Defines a JSON streaming route that emits a valid JSON array.
|
|
5
5
|
*
|
|
6
6
|
* This helper returns a valid `application/json` response containing
|
|
7
|
-
* a streamable array of JSON values. Useful for large
|
|
7
|
+
* a streamable array of JSON values. Useful for large state exports
|
|
8
8
|
* or APIs where the full array can be streamed as it's generated.
|
|
9
9
|
*
|
|
10
10
|
* Unlike `streamJsonND()`, this wraps all values in `[` and `]`
|