astro-routify 1.2.2 → 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 +367 -28
- package/dist/core/RouteTrie.d.ts +4 -3
- package/dist/core/RouteTrie.js +132 -35
- package/dist/core/RouterBuilder.d.ts +96 -13
- package/dist/core/RouterBuilder.js +243 -21
- package/dist/core/decorators.d.ts +29 -0
- package/dist/core/decorators.js +49 -0
- package/dist/core/defineGroup.d.ts +33 -17
- package/dist/core/defineGroup.js +66 -23
- package/dist/core/defineHandler.d.ts +31 -13
- package/dist/core/defineHandler.js +1 -23
- package/dist/core/defineRoute.d.ts +32 -3
- package/dist/core/defineRoute.js +36 -9
- package/dist/core/defineRouter.d.ts +11 -1
- package/dist/core/defineRouter.js +79 -16
- package/dist/core/internal/createJsonStreamRoute.d.ts +1 -1
- package/dist/core/internal/createJsonStreamRoute.js +12 -4
- package/dist/core/middlewares.d.ts +47 -0
- package/dist/core/middlewares.js +110 -0
- package/dist/core/openapi.d.ts +17 -0
- package/dist/core/openapi.js +84 -0
- package/dist/core/registry.d.ts +15 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/responseHelpers.d.ts +22 -5
- package/dist/core/responseHelpers.js +121 -20
- package/dist/core/stream.d.ts +3 -3
- package/dist/core/stream.js +16 -9
- package/dist/core/streamJsonArray.d.ts +1 -1
- package/dist/core/streamJsonArray.js +1 -1
- package/dist/index.d.ts +338 -57
- package/dist/index.js +5 -1
- package/package.json +5 -3
|
@@ -47,22 +47,32 @@ export const methodNotAllowed = (body = 'Method Not Allowed', headers) => create
|
|
|
47
47
|
* 429 Too Many Requests
|
|
48
48
|
*/
|
|
49
49
|
export const tooManyRequests = (body = 'Too Many Requests', headers) => createResponse(429, body, headers);
|
|
50
|
+
/**
|
|
51
|
+
* Returns a response with JSON body and specified status.
|
|
52
|
+
*/
|
|
53
|
+
export const json = (body, status = 200, headers) => createResponse(status, body, headers);
|
|
50
54
|
/**
|
|
51
55
|
* 500 Internal Server Error
|
|
56
|
+
*
|
|
57
|
+
* In production, you might want to avoid leaking error details.
|
|
52
58
|
*/
|
|
53
|
-
export const internalError = (err, headers) =>
|
|
59
|
+
export const internalError = (err, headers) => {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return createResponse(500, message, headers);
|
|
62
|
+
};
|
|
54
63
|
/**
|
|
55
64
|
* Sends a binary or stream-based file response with optional content-disposition.
|
|
56
65
|
*
|
|
57
|
-
* @param content - The file
|
|
66
|
+
* @param content - The file state (Blob, ArrayBuffer, or stream)
|
|
58
67
|
* @param contentType - MIME type of the file
|
|
59
68
|
* @param fileName - Optional download filename
|
|
60
69
|
* @param headers - Optional extra headers
|
|
61
70
|
* @returns A ResultResponse for download-ready content
|
|
62
71
|
*/
|
|
63
72
|
export const fileResponse = (content, contentType, fileName, headers) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
73
|
+
const sanitizedFileName = fileName?.replace(/"/g, '\\"');
|
|
74
|
+
const disposition = sanitizedFileName
|
|
75
|
+
? { 'Content-Disposition': `attachment; filename="${sanitizedFileName}"` }
|
|
66
76
|
: {};
|
|
67
77
|
return {
|
|
68
78
|
status: 200,
|
|
@@ -74,29 +84,120 @@ export const fileResponse = (content, contentType, fileName, headers) => {
|
|
|
74
84
|
},
|
|
75
85
|
};
|
|
76
86
|
};
|
|
87
|
+
function isPlainObject(value) {
|
|
88
|
+
return (typeof value === 'object' &&
|
|
89
|
+
value !== null &&
|
|
90
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Type guard to detect ReadableStreams, used for streamed/binary responses.
|
|
94
|
+
*
|
|
95
|
+
* @param value - Any value to test
|
|
96
|
+
* @returns True if it looks like a ReadableStream
|
|
97
|
+
*/
|
|
98
|
+
export function isReadableStream(value) {
|
|
99
|
+
return (typeof value === 'object' &&
|
|
100
|
+
value !== null &&
|
|
101
|
+
typeof value.getReader === 'function');
|
|
102
|
+
}
|
|
77
103
|
/**
|
|
78
|
-
* Converts an internal `ResultResponse` into a native `Response` object
|
|
104
|
+
* Converts an internal `ResultResponse` or any `HandlerResult` into a native `Response` object
|
|
79
105
|
* for use inside Astro API routes.
|
|
80
106
|
*
|
|
81
|
-
* Automatically applies
|
|
107
|
+
* Automatically applies appropriate Content-Type headers.
|
|
82
108
|
*
|
|
83
|
-
* @param result - A ResultResponse returned from route handler
|
|
109
|
+
* @param result - A ResultResponse or other supported type returned from route handler
|
|
84
110
|
* @returns A native Response
|
|
85
111
|
*/
|
|
86
112
|
export function toAstroResponse(result) {
|
|
87
|
-
if (
|
|
113
|
+
if (result instanceof Response)
|
|
114
|
+
return result;
|
|
115
|
+
if (result === undefined) {
|
|
88
116
|
return new Response(null, { status: 204 });
|
|
89
|
-
const { status, body, headers } = result;
|
|
90
|
-
if (body === undefined || body === null) {
|
|
91
|
-
return new Response(null, { status, headers });
|
|
92
117
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 });
|
|
102
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(() => {
|
|
@@ -35,8 +35,8 @@ export function stream(path, handler, options) {
|
|
|
35
35
|
write: (chunk) => {
|
|
36
36
|
if (closed || !controllerRef)
|
|
37
37
|
return;
|
|
38
|
-
const bytes = typeof chunk === 'string'
|
|
39
|
-
encoder.encode(`
|
|
38
|
+
const bytes = typeof chunk === 'string'
|
|
39
|
+
? (contentType === 'text/event-stream' ? encoder.encode(`state: ${chunk}\n\n`) : encoder.encode(chunk))
|
|
40
40
|
: chunk;
|
|
41
41
|
controllerRef.enqueue(bytes);
|
|
42
42
|
},
|
|
@@ -56,17 +56,24 @@ export function stream(path, handler, options) {
|
|
|
56
56
|
const body = new ReadableStream({
|
|
57
57
|
start(controller) {
|
|
58
58
|
controllerRef = controller;
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
closed = true;
|
|
61
|
+
try {
|
|
62
|
+
controller.close();
|
|
63
|
+
}
|
|
64
|
+
catch { /* noop */ }
|
|
65
|
+
console.debug('Request aborted — streaming stopped.');
|
|
66
|
+
};
|
|
67
|
+
ctx.request.signal.addEventListener('abort', onAbort, { once: true });
|
|
59
68
|
Promise.resolve(handler({ ...ctx, response: writer }))
|
|
60
69
|
.catch((err) => {
|
|
61
70
|
try {
|
|
62
71
|
controller.error(err);
|
|
63
72
|
}
|
|
64
73
|
catch { /* noop */ }
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
controller.close();
|
|
69
|
-
console.debug('Request aborted — streaming stopped.');
|
|
74
|
+
})
|
|
75
|
+
.finally(() => {
|
|
76
|
+
ctx.request.signal.removeEventListener('abort', onAbort);
|
|
70
77
|
});
|
|
71
78
|
},
|
|
72
79
|
cancel() {
|
|
@@ -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 `]`
|