astro-routify 1.0.0 → 1.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/README.md +160 -36
- package/dist/core/RouterBuilder.d.ts +114 -3
- package/dist/core/RouterBuilder.js +133 -6
- package/dist/core/defineGroup.d.ts +87 -0
- package/dist/core/defineGroup.js +111 -0
- package/dist/core/defineHandler.d.ts +23 -0
- package/dist/core/defineHandler.js +40 -0
- package/dist/core/defineRoute.d.ts +32 -0
- package/dist/core/defineRoute.js +14 -1
- package/dist/core/defineRouter.d.ts +33 -1
- package/dist/core/defineRouter.js +32 -3
- package/dist/core/internal/createJsonStreamRoute.d.ts +64 -0
- package/dist/core/internal/createJsonStreamRoute.js +92 -0
- package/dist/core/responseHelpers.d.ts +63 -1
- package/dist/core/responseHelpers.js +68 -0
- package/dist/core/stream.d.ts +75 -0
- package/dist/core/stream.js +91 -0
- package/dist/core/streamJsonArray.d.ts +31 -0
- package/dist/core/streamJsonArray.js +30 -0
- package/dist/core/streamJsonND.d.ts +34 -0
- package/dist/core/streamJsonND.js +33 -0
- package/dist/index.d.ts +512 -6
- package/dist/index.js +4 -0
- package/package.json +13 -7
|
@@ -1,16 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper to build a typed `ResultResponse`.
|
|
3
|
+
* @param status - HTTP status code
|
|
4
|
+
* @param body - Optional response body
|
|
5
|
+
* @param headers - Optional headers
|
|
6
|
+
*/
|
|
1
7
|
function createResponse(status, body, headers) {
|
|
2
8
|
return { status, body, headers };
|
|
3
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* 200 OK
|
|
12
|
+
*/
|
|
4
13
|
export const ok = (body, headers) => createResponse(200, body, headers);
|
|
14
|
+
/**
|
|
15
|
+
* 201 Created
|
|
16
|
+
*/
|
|
5
17
|
export const created = (body, headers) => createResponse(201, body, headers);
|
|
18
|
+
/**
|
|
19
|
+
* 204 No Content
|
|
20
|
+
*/
|
|
6
21
|
export const noContent = (headers) => createResponse(204, undefined, headers);
|
|
22
|
+
/**
|
|
23
|
+
* 304 Not Modified
|
|
24
|
+
*/
|
|
7
25
|
export const notModified = (headers) => createResponse(304, undefined, headers);
|
|
26
|
+
/**
|
|
27
|
+
* 400 Bad Request
|
|
28
|
+
*/
|
|
8
29
|
export const badRequest = (body = 'Bad Request', headers) => createResponse(400, body, headers);
|
|
30
|
+
/**
|
|
31
|
+
* 401 Unauthorized
|
|
32
|
+
*/
|
|
9
33
|
export const unauthorized = (body = 'Unauthorized', headers) => createResponse(401, body, headers);
|
|
34
|
+
/**
|
|
35
|
+
* 403 Forbidden
|
|
36
|
+
*/
|
|
10
37
|
export const forbidden = (body = 'Forbidden', headers) => createResponse(403, body, headers);
|
|
38
|
+
/**
|
|
39
|
+
* 404 Not Found
|
|
40
|
+
*/
|
|
11
41
|
export const notFound = (body = 'Not Found', headers) => createResponse(404, body, headers);
|
|
42
|
+
/**
|
|
43
|
+
* 405 Method Not Allowed
|
|
44
|
+
*/
|
|
12
45
|
export const methodNotAllowed = (body = 'Method Not Allowed', headers) => createResponse(405, body, headers);
|
|
46
|
+
/**
|
|
47
|
+
* 500 Internal Server Error
|
|
48
|
+
*/
|
|
13
49
|
export const internalError = (err, headers) => createResponse(500, err instanceof Error ? err.message : String(err), headers);
|
|
50
|
+
/**
|
|
51
|
+
* Sends a binary or stream-based file response with optional content-disposition.
|
|
52
|
+
*
|
|
53
|
+
* @param content - The file data (Blob, ArrayBuffer, or stream)
|
|
54
|
+
* @param contentType - MIME type of the file
|
|
55
|
+
* @param fileName - Optional download filename
|
|
56
|
+
* @param headers - Optional extra headers
|
|
57
|
+
* @returns A ResultResponse for download-ready content
|
|
58
|
+
*/
|
|
59
|
+
export const fileResponse = (content, contentType, fileName, headers) => {
|
|
60
|
+
const disposition = fileName
|
|
61
|
+
? { 'Content-Disposition': `attachment; filename="${fileName}"` }
|
|
62
|
+
: {};
|
|
63
|
+
return {
|
|
64
|
+
status: 200,
|
|
65
|
+
body: content,
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': contentType,
|
|
68
|
+
...disposition,
|
|
69
|
+
...headers,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Converts an internal `ResultResponse` into a native `Response` object
|
|
75
|
+
* for use inside Astro API routes.
|
|
76
|
+
*
|
|
77
|
+
* Automatically applies `Content-Type: application/json` for object bodies.
|
|
78
|
+
*
|
|
79
|
+
* @param result - A ResultResponse returned from route handler
|
|
80
|
+
* @returns A native Response
|
|
81
|
+
*/
|
|
14
82
|
export function toAstroResponse(result) {
|
|
15
83
|
if (!result)
|
|
16
84
|
return new Response(null, { status: 204 });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { type Route } from './defineRoute';
|
|
3
|
+
import { HttpMethod } from './HttpMethod';
|
|
4
|
+
import { HeadersInit } from 'undici';
|
|
5
|
+
/**
|
|
6
|
+
* A writer for streaming raw data to the response body.
|
|
7
|
+
*
|
|
8
|
+
* This is used inside the `stream()` route handler to emit bytes
|
|
9
|
+
* or strings directly to the client with backpressure awareness.
|
|
10
|
+
*/
|
|
11
|
+
export interface StreamWriter {
|
|
12
|
+
/**
|
|
13
|
+
* Write a string or Uint8Array chunk to the response stream.
|
|
14
|
+
* @param chunk - Text or byte chunk to emit
|
|
15
|
+
*/
|
|
16
|
+
write: (chunk: string | Uint8Array) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Close the stream and signal end-of-response to the client.
|
|
19
|
+
*/
|
|
20
|
+
close: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Dynamically change the Content-Type header.
|
|
23
|
+
* Should be called before the first `write()` to take effect.
|
|
24
|
+
* @param type - MIME type (e.g., `text/html`, `application/json`)
|
|
25
|
+
*/
|
|
26
|
+
setContentType: (type: string) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configuration options for the `stream()` route helper.
|
|
30
|
+
*/
|
|
31
|
+
export interface StreamOptions {
|
|
32
|
+
/**
|
|
33
|
+
* HTTP method (defaults to GET).
|
|
34
|
+
*/
|
|
35
|
+
method?: HttpMethod;
|
|
36
|
+
/**
|
|
37
|
+
* Content-Type header (defaults to `text/event-stream`).
|
|
38
|
+
*/
|
|
39
|
+
contentType?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Additional custom headers.
|
|
42
|
+
*/
|
|
43
|
+
headers?: HeadersInit;
|
|
44
|
+
/**
|
|
45
|
+
* Enable SSE keep-alive headers (defaults to true for SSE).
|
|
46
|
+
*/
|
|
47
|
+
keepAlive?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Defines a generic streaming route that can write raw chunks of data
|
|
51
|
+
* to the response in real time using a `ReadableStream`.
|
|
52
|
+
*
|
|
53
|
+
* Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
|
|
54
|
+
* logs, LLM output, or NDJSON responses.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* stream('/clock', async ({ response }) => {
|
|
58
|
+
* const timer = setInterval(() => {
|
|
59
|
+
* response.write(`data: ${new Date().toISOString()}\n\n`);
|
|
60
|
+
* }, 1000);
|
|
61
|
+
*
|
|
62
|
+
* setTimeout(() => {
|
|
63
|
+
* clearInterval(timer);
|
|
64
|
+
* response.close();
|
|
65
|
+
* }, 5000);
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* @param path - The route path (e.g. `/clock`)
|
|
69
|
+
* @param handler - Handler that receives the API context and response writer
|
|
70
|
+
* @param options - Optional stream configuration
|
|
71
|
+
* @returns A Route object ready to be used in `defineRouter()`
|
|
72
|
+
*/
|
|
73
|
+
export declare function stream(path: string, handler: (ctx: APIContext & {
|
|
74
|
+
response: StreamWriter;
|
|
75
|
+
}) => void | Promise<void>, options?: StreamOptions): Route;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineRoute } from './defineRoute';
|
|
2
|
+
import { HttpMethod } from './HttpMethod';
|
|
3
|
+
/**
|
|
4
|
+
* Defines a generic streaming route that can write raw chunks of data
|
|
5
|
+
* to the response in real time using a `ReadableStream`.
|
|
6
|
+
*
|
|
7
|
+
* Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
|
|
8
|
+
* logs, LLM output, or NDJSON responses.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* stream('/clock', async ({ response }) => {
|
|
12
|
+
* const timer = setInterval(() => {
|
|
13
|
+
* response.write(`data: ${new Date().toISOString()}\n\n`);
|
|
14
|
+
* }, 1000);
|
|
15
|
+
*
|
|
16
|
+
* setTimeout(() => {
|
|
17
|
+
* clearInterval(timer);
|
|
18
|
+
* response.close();
|
|
19
|
+
* }, 5000);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* @param path - The route path (e.g. `/clock`)
|
|
23
|
+
* @param handler - Handler that receives the API context and response writer
|
|
24
|
+
* @param options - Optional stream configuration
|
|
25
|
+
* @returns A Route object ready to be used in `defineRouter()`
|
|
26
|
+
*/
|
|
27
|
+
export function stream(path, handler, options) {
|
|
28
|
+
const method = options?.method ?? HttpMethod.GET;
|
|
29
|
+
return defineRoute(method, path, async (ctx) => {
|
|
30
|
+
let contentType = options?.contentType ?? 'text/event-stream';
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
let controllerRef = null;
|
|
33
|
+
let closed = false;
|
|
34
|
+
const writer = {
|
|
35
|
+
write: (chunk) => {
|
|
36
|
+
if (closed || !controllerRef)
|
|
37
|
+
return;
|
|
38
|
+
const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : chunk;
|
|
39
|
+
controllerRef.enqueue(bytes);
|
|
40
|
+
},
|
|
41
|
+
close: () => {
|
|
42
|
+
if (closed)
|
|
43
|
+
return;
|
|
44
|
+
closed = true;
|
|
45
|
+
try {
|
|
46
|
+
controllerRef?.close();
|
|
47
|
+
}
|
|
48
|
+
catch { /* noop */ }
|
|
49
|
+
},
|
|
50
|
+
setContentType: (type) => {
|
|
51
|
+
contentType = type;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const body = new ReadableStream({
|
|
55
|
+
start(controller) {
|
|
56
|
+
controllerRef = controller;
|
|
57
|
+
Promise.resolve(handler({ ...ctx, response: writer }))
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
try {
|
|
60
|
+
controller.error(err);
|
|
61
|
+
}
|
|
62
|
+
catch { /* noop */ }
|
|
63
|
+
});
|
|
64
|
+
ctx.request.signal.addEventListener('abort', () => {
|
|
65
|
+
closed = true;
|
|
66
|
+
controller.close();
|
|
67
|
+
console.debug('Request aborted — streaming stopped.');
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
cancel() {
|
|
71
|
+
closed = true;
|
|
72
|
+
console.debug('Stream cancelled explicitly.');
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const defaultSseHeaders = contentType === 'text/event-stream' && options?.keepAlive !== false
|
|
76
|
+
? {
|
|
77
|
+
'Cache-Control': 'no-cache',
|
|
78
|
+
'Connection': 'keep-alive',
|
|
79
|
+
'X-Accel-Buffering': 'no',
|
|
80
|
+
}
|
|
81
|
+
: {};
|
|
82
|
+
return new Response(body, {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': contentType,
|
|
86
|
+
...defaultSseHeaders,
|
|
87
|
+
...(options?.headers ?? {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { HttpMethod } from './HttpMethod';
|
|
3
|
+
import { Route } from './defineRoute';
|
|
4
|
+
import { JsonStreamWriter } from './internal/createJsonStreamRoute';
|
|
5
|
+
/**
|
|
6
|
+
* Defines a JSON streaming route that emits a valid JSON array.
|
|
7
|
+
*
|
|
8
|
+
* This helper returns a valid `application/json` response containing
|
|
9
|
+
* a streamable array of JSON values. Useful for large data exports
|
|
10
|
+
* or APIs where the full array can be streamed as it's generated.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `streamJsonND()`, this wraps all values in `[` and `]`
|
|
13
|
+
* and separates them with commas.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* streamJsonArray('/users', async ({ response }) => {
|
|
17
|
+
* response.send({ id: 1 });
|
|
18
|
+
* response.send({ id: 2 });
|
|
19
|
+
* response.close();
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* @param path - The route path (e.g. `/users`)
|
|
23
|
+
* @param handler - A function that receives `ctx` and a JSON stream writer
|
|
24
|
+
* @param options - Optional HTTP method override (default is GET)
|
|
25
|
+
* @returns A Route object ready to be registered with `defineRouter`
|
|
26
|
+
*/
|
|
27
|
+
export declare function streamJsonArray(path: string, handler: (ctx: APIContext & {
|
|
28
|
+
response: JsonStreamWriter;
|
|
29
|
+
}) => void | Promise<void>, options?: {
|
|
30
|
+
method?: HttpMethod;
|
|
31
|
+
}): Route;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HttpMethod } from './HttpMethod';
|
|
2
|
+
import { createJsonStreamRoute } from './internal/createJsonStreamRoute';
|
|
3
|
+
/**
|
|
4
|
+
* Defines a JSON streaming route that emits a valid JSON array.
|
|
5
|
+
*
|
|
6
|
+
* This helper returns a valid `application/json` response containing
|
|
7
|
+
* a streamable array of JSON values. Useful for large data exports
|
|
8
|
+
* or APIs where the full array can be streamed as it's generated.
|
|
9
|
+
*
|
|
10
|
+
* Unlike `streamJsonND()`, this wraps all values in `[` and `]`
|
|
11
|
+
* and separates them with commas.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* streamJsonArray('/users', async ({ response }) => {
|
|
15
|
+
* response.send({ id: 1 });
|
|
16
|
+
* response.send({ id: 2 });
|
|
17
|
+
* response.close();
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* @param path - The route path (e.g. `/users`)
|
|
21
|
+
* @param handler - A function that receives `ctx` and a JSON stream writer
|
|
22
|
+
* @param options - Optional HTTP method override (default is GET)
|
|
23
|
+
* @returns A Route object ready to be registered with `defineRouter`
|
|
24
|
+
*/
|
|
25
|
+
export function streamJsonArray(path, handler, options) {
|
|
26
|
+
return createJsonStreamRoute(path, handler, {
|
|
27
|
+
mode: 'array',
|
|
28
|
+
method: options?.method ?? HttpMethod.GET,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { HttpMethod } from './HttpMethod';
|
|
3
|
+
import { Route } from './defineRoute';
|
|
4
|
+
import { JsonStreamWriter } from './internal/createJsonStreamRoute';
|
|
5
|
+
/**
|
|
6
|
+
* Defines a JSON streaming route using NDJSON (Newline Delimited JSON) format.
|
|
7
|
+
*
|
|
8
|
+
* This helper streams individual JSON objects separated by newlines (`\n`)
|
|
9
|
+
* instead of a single JSON array. It sets the `Content-Type` to
|
|
10
|
+
* `application/x-ndjson` and disables buffering to allow low-latency
|
|
11
|
+
* delivery of each item.
|
|
12
|
+
*
|
|
13
|
+
* Ideal for real-time updates, event logs, progressive loading, or
|
|
14
|
+
* integrations with LLMs or dashboards where each object can be processed
|
|
15
|
+
* as soon as it's received.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* streamJsonND('/events', async ({ response }) => {
|
|
19
|
+
* response.send({ type: 'start' });
|
|
20
|
+
* await delay(100);
|
|
21
|
+
* response.send({ type: 'progress', percent: 25 });
|
|
22
|
+
* response.close();
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* @param path - The route path (e.g. `/events`)
|
|
26
|
+
* @param handler - A function that receives `ctx` and a JSON stream writer
|
|
27
|
+
* @param options - Optional HTTP method override (default is GET)
|
|
28
|
+
* @returns A Route object ready to be registered with `defineRouter`
|
|
29
|
+
*/
|
|
30
|
+
export declare function streamJsonND(path: string, handler: (ctx: APIContext & {
|
|
31
|
+
response: JsonStreamWriter;
|
|
32
|
+
}) => void | Promise<void>, options?: {
|
|
33
|
+
method?: HttpMethod;
|
|
34
|
+
}): Route;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { HttpMethod } from './HttpMethod';
|
|
2
|
+
import { createJsonStreamRoute } from './internal/createJsonStreamRoute';
|
|
3
|
+
/**
|
|
4
|
+
* Defines a JSON streaming route using NDJSON (Newline Delimited JSON) format.
|
|
5
|
+
*
|
|
6
|
+
* This helper streams individual JSON objects separated by newlines (`\n`)
|
|
7
|
+
* instead of a single JSON array. It sets the `Content-Type` to
|
|
8
|
+
* `application/x-ndjson` and disables buffering to allow low-latency
|
|
9
|
+
* delivery of each item.
|
|
10
|
+
*
|
|
11
|
+
* Ideal for real-time updates, event logs, progressive loading, or
|
|
12
|
+
* integrations with LLMs or dashboards where each object can be processed
|
|
13
|
+
* as soon as it's received.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* streamJsonND('/events', async ({ response }) => {
|
|
17
|
+
* response.send({ type: 'start' });
|
|
18
|
+
* await delay(100);
|
|
19
|
+
* response.send({ type: 'progress', percent: 25 });
|
|
20
|
+
* response.close();
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @param path - The route path (e.g. `/events`)
|
|
24
|
+
* @param handler - A function that receives `ctx` and a JSON stream writer
|
|
25
|
+
* @param options - Optional HTTP method override (default is GET)
|
|
26
|
+
* @returns A Route object ready to be registered with `defineRouter`
|
|
27
|
+
*/
|
|
28
|
+
export function streamJsonND(path, handler, options) {
|
|
29
|
+
return createJsonStreamRoute(path, handler, {
|
|
30
|
+
mode: 'ndjson',
|
|
31
|
+
method: options?.method ?? HttpMethod.GET,
|
|
32
|
+
});
|
|
33
|
+
}
|