@stacksjs/bun-router 0.0.13 → 0.0.15
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/dist/{chunk-1mvtv6jw.js → chunk-j0e7z7hd.js} +7549 -2508
- package/dist/cli.js +1023 -858
- package/dist/index.js +60 -25
- package/dist/response/macros.d.ts +19 -8
- package/dist/router/router.d.ts +39 -16
- package/dist/types.d.ts +12 -0
- package/package.json +3 -6
- package/src/middleware/auth.ts +2 -1
- package/src/middleware/file_upload.ts +11 -0
- package/src/response/macros.ts +65 -12
- package/src/router/file-based-routing.ts +20 -0
- package/src/router/file-streaming.ts +15 -4
- package/src/router/route-building.ts +29 -20
- package/src/router/router.ts +39 -16
- package/src/types.ts +12 -0
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
signRequest,
|
|
67
67
|
singleFileUpload,
|
|
68
68
|
verifySignature
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-j0e7z7hd.js";
|
|
70
70
|
|
|
71
71
|
// src/auth.ts
|
|
72
72
|
function base64UrlEncode(str) {
|
|
@@ -3915,12 +3915,19 @@ function registerFileBasedRouting(RouterClass) {
|
|
|
3915
3915
|
async value() {
|
|
3916
3916
|
if (this._fileRoutesInitialized)
|
|
3917
3917
|
return;
|
|
3918
|
+
if (!Object.prototype.hasOwnProperty.call(this, "_fileBasedRoutes")) {
|
|
3919
|
+
this._fileBasedRoutes = [];
|
|
3920
|
+
}
|
|
3918
3921
|
const viewsDir = this._viewsDir || detectViewsDirectory();
|
|
3919
3922
|
if (!viewsDir) {
|
|
3920
3923
|
this._fileRoutesInitialized = true;
|
|
3921
3924
|
return;
|
|
3922
3925
|
}
|
|
3923
3926
|
const config2 = this._fileRoutingConfig || {};
|
|
3927
|
+
if (config2.enabled === false) {
|
|
3928
|
+
this._fileRoutesInitialized = true;
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3924
3931
|
const extensions = config2.extensions || DEFAULT_EXTENSIONS;
|
|
3925
3932
|
const exclude = config2.exclude || DEFAULT_EXCLUDES;
|
|
3926
3933
|
if (!hasRoutableFiles(viewsDir, extensions, exclude)) {
|
|
@@ -4291,6 +4298,11 @@ class FileStreamHandler {
|
|
|
4291
4298
|
}
|
|
4292
4299
|
|
|
4293
4300
|
// src/router/file-streaming.ts
|
|
4301
|
+
function closeReadableStreamAfterError(controller, error) {
|
|
4302
|
+
try {
|
|
4303
|
+
controller.close();
|
|
4304
|
+
} catch {}
|
|
4305
|
+
}
|
|
4294
4306
|
function registerFileStreaming(RouterClass) {
|
|
4295
4307
|
Object.defineProperties(RouterClass.prototype, {
|
|
4296
4308
|
streamFile: {
|
|
@@ -4390,7 +4402,7 @@ function registerFileStreaming(RouterClass) {
|
|
|
4390
4402
|
}
|
|
4391
4403
|
controller.close();
|
|
4392
4404
|
} catch (error) {
|
|
4393
|
-
controller
|
|
4405
|
+
closeReadableStreamAfterError(controller, error);
|
|
4394
4406
|
}
|
|
4395
4407
|
}
|
|
4396
4408
|
});
|
|
@@ -4434,7 +4446,7 @@ function registerFileStreaming(RouterClass) {
|
|
|
4434
4446
|
controller.enqueue(encoder.encode("}"));
|
|
4435
4447
|
controller.close();
|
|
4436
4448
|
} catch (error) {
|
|
4437
|
-
controller
|
|
4449
|
+
closeReadableStreamAfterError(controller, error);
|
|
4438
4450
|
}
|
|
4439
4451
|
}
|
|
4440
4452
|
});
|
|
@@ -4482,7 +4494,7 @@ function registerFileStreaming(RouterClass) {
|
|
|
4482
4494
|
}
|
|
4483
4495
|
controller.close();
|
|
4484
4496
|
} catch (error) {
|
|
4485
|
-
controller
|
|
4497
|
+
closeReadableStreamAfterError(controller, error);
|
|
4486
4498
|
}
|
|
4487
4499
|
}
|
|
4488
4500
|
});
|
|
@@ -4514,7 +4526,7 @@ function registerFileStreaming(RouterClass) {
|
|
|
4514
4526
|
}
|
|
4515
4527
|
controller.close();
|
|
4516
4528
|
} catch (error) {
|
|
4517
|
-
controller
|
|
4529
|
+
closeReadableStreamAfterError(controller, error);
|
|
4518
4530
|
}
|
|
4519
4531
|
}
|
|
4520
4532
|
});
|
|
@@ -8161,12 +8173,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
8161
8173
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
8162
8174
|
matchPath(path, url2.pathname, params, constraintsRecord);
|
|
8163
8175
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
8164
|
-
const
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
}
|
|
8168
|
-
const response2 = await this.resolveHandler(route.handler, enhancedReq);
|
|
8169
|
-
return this.applyModifiedCookies(response2, enhancedReq);
|
|
8176
|
+
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
8177
|
+
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
8178
|
+
const response2 = await this.runMiddleware(enhancedReq, middlewareStack);
|
|
8179
|
+
return this.applyModifiedCookies(response2 ?? new Response("No response from middleware chain", { status: 500 }), enhancedReq);
|
|
8170
8180
|
};
|
|
8171
8181
|
} else {
|
|
8172
8182
|
const methodHandlers = {};
|
|
@@ -8178,12 +8188,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
8178
8188
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
8179
8189
|
matchPath(path, url2.pathname, params, constraintsRecord);
|
|
8180
8190
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
8181
|
-
const
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
}
|
|
8185
|
-
const response2 = await this.resolveHandler(route.handler, enhancedReq);
|
|
8186
|
-
return this.applyModifiedCookies(response2, enhancedReq);
|
|
8191
|
+
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
8192
|
+
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
8193
|
+
const response2 = await this.runMiddleware(enhancedReq, middlewareStack);
|
|
8194
|
+
return this.applyModifiedCookies(response2 ?? new Response("No response from middleware chain", { status: 500 }), enhancedReq);
|
|
8187
8195
|
};
|
|
8188
8196
|
}
|
|
8189
8197
|
bunRoutes[path] = methodHandlers;
|
|
@@ -10419,6 +10427,29 @@ class ResponseWithMacros extends Response {
|
|
|
10419
10427
|
return responseMacroRegistry.has(name);
|
|
10420
10428
|
}
|
|
10421
10429
|
}
|
|
10430
|
+
function resolveResponseInit(statusOrInit, headers) {
|
|
10431
|
+
if (typeof statusOrInit === "number")
|
|
10432
|
+
return { status: statusOrInit, mergedHeaders: headers };
|
|
10433
|
+
if (statusOrInit && typeof statusOrInit === "object") {
|
|
10434
|
+
const status = typeof statusOrInit.status === "number" ? statusOrInit.status : 200;
|
|
10435
|
+
let mergedHeaders = headers;
|
|
10436
|
+
if (statusOrInit.headers) {
|
|
10437
|
+
mergedHeaders = {};
|
|
10438
|
+
if (statusOrInit.headers instanceof Headers) {
|
|
10439
|
+
statusOrInit.headers.forEach((value, key) => {
|
|
10440
|
+
mergedHeaders[key] = value;
|
|
10441
|
+
});
|
|
10442
|
+
} else if (Array.isArray(statusOrInit.headers)) {
|
|
10443
|
+
for (const [key, value] of statusOrInit.headers)
|
|
10444
|
+
mergedHeaders[key] = value;
|
|
10445
|
+
} else {
|
|
10446
|
+
Object.assign(mergedHeaders, statusOrInit.headers);
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
return { status, mergedHeaders };
|
|
10450
|
+
}
|
|
10451
|
+
return { status: 200, mergedHeaders: headers };
|
|
10452
|
+
}
|
|
10422
10453
|
var BuiltInResponseMacros = {
|
|
10423
10454
|
success: (data, message, status = 200) => {
|
|
10424
10455
|
const body = {};
|
|
@@ -10496,39 +10527,43 @@ var BuiltInResponseMacros = {
|
|
|
10496
10527
|
headers: { "Content-Type": "application/json" }
|
|
10497
10528
|
});
|
|
10498
10529
|
},
|
|
10499
|
-
json: (data,
|
|
10530
|
+
json: (data, statusOrInit = 200, headers = {}) => {
|
|
10531
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers);
|
|
10500
10532
|
return new Response(JSON.stringify(data), {
|
|
10501
10533
|
status,
|
|
10502
10534
|
headers: {
|
|
10503
10535
|
"Content-Type": "application/json",
|
|
10504
|
-
...
|
|
10536
|
+
...mergedHeaders
|
|
10505
10537
|
}
|
|
10506
10538
|
});
|
|
10507
10539
|
},
|
|
10508
|
-
html: (content,
|
|
10540
|
+
html: (content, statusOrInit = 200, headers = {}) => {
|
|
10541
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers);
|
|
10509
10542
|
return new Response(content, {
|
|
10510
10543
|
status,
|
|
10511
10544
|
headers: {
|
|
10512
10545
|
"Content-Type": "text/html",
|
|
10513
|
-
...
|
|
10546
|
+
...mergedHeaders
|
|
10514
10547
|
}
|
|
10515
10548
|
});
|
|
10516
10549
|
},
|
|
10517
|
-
text: (content,
|
|
10550
|
+
text: (content, statusOrInit = 200, headers = {}) => {
|
|
10551
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers);
|
|
10518
10552
|
return new Response(content, {
|
|
10519
10553
|
status,
|
|
10520
10554
|
headers: {
|
|
10521
10555
|
"Content-Type": "text/plain",
|
|
10522
|
-
...
|
|
10556
|
+
...mergedHeaders
|
|
10523
10557
|
}
|
|
10524
10558
|
});
|
|
10525
10559
|
},
|
|
10526
|
-
xml: (content,
|
|
10560
|
+
xml: (content, statusOrInit = 200, headers = {}) => {
|
|
10561
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers);
|
|
10527
10562
|
return new Response(content, {
|
|
10528
10563
|
status,
|
|
10529
10564
|
headers: {
|
|
10530
10565
|
"Content-Type": "application/xml",
|
|
10531
|
-
...
|
|
10566
|
+
...mergedHeaders
|
|
10532
10567
|
}
|
|
10533
10568
|
});
|
|
10534
10569
|
},
|
|
@@ -144,21 +144,32 @@ export declare const BuiltInResponseMacros: {
|
|
|
144
144
|
path: string;
|
|
145
145
|
}, message?: string) => Response;
|
|
146
146
|
/**
|
|
147
|
-
* JSON response with custom headers
|
|
147
|
+
* JSON response with custom headers. Accepts BOTH the native
|
|
148
|
+
* `Response.json(data, init?)` shape (where `init` is a
|
|
149
|
+
* `ResponseInit` object) AND the legacy positional
|
|
150
|
+
* `Response.json(data, status, headers)` shape. Required for
|
|
151
|
+
* backwards-compatibility with framework + third-party code that
|
|
152
|
+
* calls `Response.json(data, { status: 422 })` — the standard WHATWG
|
|
153
|
+
* signature that Bun's native `Response.json` ships with.
|
|
154
|
+
*
|
|
155
|
+
* See stacksjs/stacks#1857.
|
|
148
156
|
*/
|
|
149
|
-
json: (data: any,
|
|
157
|
+
json: (data: any, statusOrInit?: number | ResponseInit, headers?: Record<string, string>) => Response;
|
|
150
158
|
/**
|
|
151
|
-
* HTML response
|
|
159
|
+
* HTML response. Dual-shape: native `(content, init?)` or positional
|
|
160
|
+
* `(content, status, headers)`.
|
|
152
161
|
*/
|
|
153
|
-
html: (content: string,
|
|
162
|
+
html: (content: string, statusOrInit?: number | ResponseInit, headers?: Record<string, string>) => Response;
|
|
154
163
|
/**
|
|
155
|
-
* Plain text response
|
|
164
|
+
* Plain text response. Dual-shape: native `(content, init?)` or
|
|
165
|
+
* positional `(content, status, headers)`.
|
|
156
166
|
*/
|
|
157
|
-
text: (content: string,
|
|
167
|
+
text: (content: string, statusOrInit?: number | ResponseInit, headers?: Record<string, string>) => Response;
|
|
158
168
|
/**
|
|
159
|
-
* XML response
|
|
169
|
+
* XML response. Dual-shape: native `(content, init?)` or positional
|
|
170
|
+
* `(content, status, headers)`.
|
|
160
171
|
*/
|
|
161
|
-
xml: (content: string,
|
|
172
|
+
xml: (content: string, statusOrInit?: number | ResponseInit, headers?: Record<string, string>) => Response;
|
|
162
173
|
/**
|
|
163
174
|
* Redirect response
|
|
164
175
|
*/
|
package/dist/router/router.d.ts
CHANGED
|
@@ -99,37 +99,60 @@ export declare class Router {
|
|
|
99
99
|
*/
|
|
100
100
|
resolveMiddleware(middleware: string | MiddlewareHandler): MiddlewareHandler | null;
|
|
101
101
|
/**
|
|
102
|
-
* HTTP GET method
|
|
102
|
+
* HTTP GET method.
|
|
103
|
+
*
|
|
104
|
+
* `TPath` is inferred from the path literal at the call site, so an
|
|
105
|
+
* inline handler's `request.params` narrows to the extracted keyset:
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* router.get('/api/users/{id}', (req) => {
|
|
109
|
+
* req.params.id // typed `string`, no `as any`
|
|
110
|
+
* // @ts-expect-error wrong key
|
|
111
|
+
* req.params.bogus
|
|
112
|
+
* })
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* The string-action / class-handler forms (`'Actions/Foo/BarAction'`)
|
|
116
|
+
* keep working — `ActionHandler<TPath>` is a union that accepts them.
|
|
117
|
+
* See stacksjs/stacks#1851 for the broader typed-request work.
|
|
103
118
|
*/
|
|
104
|
-
get(path:
|
|
119
|
+
get<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
105
120
|
/**
|
|
106
|
-
* HTTP POST method
|
|
121
|
+
* HTTP POST method. See {@link get} for `TPath`-driven param
|
|
122
|
+
* narrowing on inline handlers.
|
|
107
123
|
*/
|
|
108
|
-
post(path:
|
|
124
|
+
post<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
109
125
|
/**
|
|
110
|
-
* HTTP PUT method
|
|
126
|
+
* HTTP PUT method. See {@link get} for `TPath`-driven param
|
|
127
|
+
* narrowing on inline handlers.
|
|
111
128
|
*/
|
|
112
|
-
put(path:
|
|
129
|
+
put<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
113
130
|
/**
|
|
114
|
-
* HTTP PATCH method
|
|
131
|
+
* HTTP PATCH method. See {@link get} for `TPath`-driven param
|
|
132
|
+
* narrowing on inline handlers.
|
|
115
133
|
*/
|
|
116
|
-
patch(path:
|
|
134
|
+
patch<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
117
135
|
/**
|
|
118
|
-
* HTTP DELETE method
|
|
136
|
+
* HTTP DELETE method. See {@link get} for `TPath`-driven param
|
|
137
|
+
* narrowing on inline handlers.
|
|
119
138
|
*/
|
|
120
|
-
delete(path:
|
|
139
|
+
delete<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
121
140
|
/**
|
|
122
|
-
* HTTP OPTIONS method
|
|
141
|
+
* HTTP OPTIONS method. See {@link get} for `TPath`-driven param
|
|
142
|
+
* narrowing on inline handlers.
|
|
123
143
|
*/
|
|
124
|
-
options(path:
|
|
144
|
+
options<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
125
145
|
/**
|
|
126
|
-
* Register
|
|
146
|
+
* Register the same handler against multiple HTTP methods. The
|
|
147
|
+
* handler is type-narrowed via {@link ActionHandler}'s `TPath`
|
|
148
|
+
* generic, same as the single-method overloads.
|
|
127
149
|
*/
|
|
128
|
-
match(methods: string[], path:
|
|
150
|
+
match<TPath extends string>(methods: string[], path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
129
151
|
/**
|
|
130
|
-
* Register
|
|
152
|
+
* Register the handler against every HTTP method. See {@link get}
|
|
153
|
+
* for `TPath`-driven param narrowing on inline handlers.
|
|
131
154
|
*/
|
|
132
|
-
any(path:
|
|
155
|
+
any<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
|
|
133
156
|
/**
|
|
134
157
|
* Set fallback handler for unmatched routes
|
|
135
158
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -1716,6 +1716,18 @@ export interface RequestMacroMethods {
|
|
|
1716
1716
|
param: <T = InputValue>(key: string, defaultValue?: T) => T;
|
|
1717
1717
|
cookie: (name: string, defaultValue?: string) => string | null;
|
|
1718
1718
|
cookies: () => Record<string, string>;
|
|
1719
|
+
/**
|
|
1720
|
+
* Lookup a single uploaded file by form field name. Returns the
|
|
1721
|
+
* matching `FileInfo` (structurally an `UploadedFile`) or `null` when
|
|
1722
|
+
* no field with that name was uploaded. Populated by the multipart
|
|
1723
|
+
* file-upload middleware alongside `files: UploadedFile[]`. Mirrors
|
|
1724
|
+
* Laravel's `$request->file('avatar')`:
|
|
1725
|
+
*
|
|
1726
|
+
* ```ts
|
|
1727
|
+
* const avatar = req.file('avatar')
|
|
1728
|
+
* if (!avatar) return new Response('Missing avatar', { status: 400 })
|
|
1729
|
+
* ```
|
|
1730
|
+
*/
|
|
1719
1731
|
file: (name: string) => FileInfo | null;
|
|
1720
1732
|
hasFile: (name: string) => boolean;
|
|
1721
1733
|
path: () => string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/bun-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15",
|
|
5
5
|
"description": "A fast, type-safe router for Bun.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"zip:windows-x64": "zip -j bin/router-windows-x64.zip bin/router-windows-x64.exe",
|
|
74
74
|
"zip:darwin-x64": "zip -j bin/router-darwin-x64.zip bin/router-darwin-x64",
|
|
75
75
|
"zip:darwin-arm64": "zip -j bin/router-darwin-arm64.zip bin/router-darwin-arm64",
|
|
76
|
-
"lint": "bunx --bun
|
|
77
|
-
"lint:fix": "bunx --bun
|
|
76
|
+
"lint": "bunx --bun pickier .",
|
|
77
|
+
"lint:fix": "bunx --bun pickier . --fix",
|
|
78
78
|
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
|
|
79
79
|
"changelog": "bunx changelogen --output CHANGELOG.md",
|
|
80
80
|
"prepublishOnly": "bun --bun run build",
|
|
@@ -86,8 +86,5 @@
|
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@stacksjs/clapp": "^0.2.0",
|
|
88
88
|
"ts-rate-limiter": "^0.4.2"
|
|
89
|
-
},
|
|
90
|
-
"lint-staged": {
|
|
91
|
-
"*.{js,ts}": "bunx --bun pickier . --fix"
|
|
92
89
|
}
|
|
93
90
|
}
|
package/src/middleware/auth.ts
CHANGED
|
@@ -162,6 +162,17 @@ export default class FileUpload {
|
|
|
162
162
|
configurable: true,
|
|
163
163
|
})
|
|
164
164
|
|
|
165
|
+
// Typed lookup by form field name. Mirrors Laravel's
|
|
166
|
+
// `$request->file('avatar')` ergonomics: returns the matching
|
|
167
|
+
// UploadedFile or null. See stacksjs/stacks#1856.
|
|
168
|
+
Object.defineProperty(req, 'file', {
|
|
169
|
+
value: (fieldName: string): UploadedFile | null =>
|
|
170
|
+
uploadedFiles.find(f => f.fieldName === fieldName) ?? null,
|
|
171
|
+
writable: true,
|
|
172
|
+
enumerable: false,
|
|
173
|
+
configurable: true,
|
|
174
|
+
})
|
|
175
|
+
|
|
165
176
|
Object.defineProperty(req, 'formBody', {
|
|
166
177
|
value: fields,
|
|
167
178
|
writable: true,
|
package/src/response/macros.ts
CHANGED
|
@@ -124,6 +124,44 @@ export class ResponseWithMacros extends Response {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Reconcile the second argument of `Response.<method>(body, statusOrInit, headers?)`.
|
|
129
|
+
* Accepts either a positional status number (legacy bun-router shape) or a
|
|
130
|
+
* `ResponseInit` object (native WHATWG shape `{ status?, headers?, statusText? }`).
|
|
131
|
+
* When a `ResponseInit` provides `headers`, those replace the positional `headers`
|
|
132
|
+
* arg entirely — matching the principle that the more-explicit form wins.
|
|
133
|
+
*
|
|
134
|
+
* Headers normalize to a plain string map so downstream spread works uniformly.
|
|
135
|
+
*/
|
|
136
|
+
function resolveResponseInit(
|
|
137
|
+
statusOrInit: number | ResponseInit,
|
|
138
|
+
headers: Record<string, string>,
|
|
139
|
+
): { status: number, mergedHeaders: Record<string, string> } {
|
|
140
|
+
if (typeof statusOrInit === 'number')
|
|
141
|
+
return { status: statusOrInit, mergedHeaders: headers }
|
|
142
|
+
|
|
143
|
+
if (statusOrInit && typeof statusOrInit === 'object') {
|
|
144
|
+
const status = typeof statusOrInit.status === 'number' ? statusOrInit.status : 200
|
|
145
|
+
let mergedHeaders = headers
|
|
146
|
+
if (statusOrInit.headers) {
|
|
147
|
+
mergedHeaders = {}
|
|
148
|
+
if (statusOrInit.headers instanceof Headers) {
|
|
149
|
+
statusOrInit.headers.forEach((value, key) => { mergedHeaders[key] = value })
|
|
150
|
+
}
|
|
151
|
+
else if (Array.isArray(statusOrInit.headers)) {
|
|
152
|
+
for (const [key, value] of statusOrInit.headers)
|
|
153
|
+
mergedHeaders[key] = value
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
Object.assign(mergedHeaders, statusOrInit.headers as Record<string, string>)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { status, mergedHeaders }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { status: 200, mergedHeaders: headers }
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
/**
|
|
128
166
|
* Built-in response macros
|
|
129
167
|
*/
|
|
@@ -274,53 +312,68 @@ export const BuiltInResponseMacros = {
|
|
|
274
312
|
},
|
|
275
313
|
|
|
276
314
|
/**
|
|
277
|
-
* JSON response with custom headers
|
|
315
|
+
* JSON response with custom headers. Accepts BOTH the native
|
|
316
|
+
* `Response.json(data, init?)` shape (where `init` is a
|
|
317
|
+
* `ResponseInit` object) AND the legacy positional
|
|
318
|
+
* `Response.json(data, status, headers)` shape. Required for
|
|
319
|
+
* backwards-compatibility with framework + third-party code that
|
|
320
|
+
* calls `Response.json(data, { status: 422 })` — the standard WHATWG
|
|
321
|
+
* signature that Bun's native `Response.json` ships with.
|
|
322
|
+
*
|
|
323
|
+
* See stacksjs/stacks#1857.
|
|
278
324
|
*/
|
|
279
|
-
json: (data: any,
|
|
325
|
+
json: (data: any, statusOrInit: number | ResponseInit = 200, headers: Record<string, string> = {}): Response => {
|
|
326
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers)
|
|
280
327
|
return new Response(JSON.stringify(data), {
|
|
281
328
|
status,
|
|
282
329
|
headers: {
|
|
283
330
|
'Content-Type': 'application/json',
|
|
284
|
-
...
|
|
331
|
+
...mergedHeaders,
|
|
285
332
|
},
|
|
286
333
|
})
|
|
287
334
|
},
|
|
288
335
|
|
|
289
336
|
/**
|
|
290
|
-
* HTML response
|
|
337
|
+
* HTML response. Dual-shape: native `(content, init?)` or positional
|
|
338
|
+
* `(content, status, headers)`.
|
|
291
339
|
*/
|
|
292
|
-
html: (content: string,
|
|
340
|
+
html: (content: string, statusOrInit: number | ResponseInit = 200, headers: Record<string, string> = {}): Response => {
|
|
341
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers)
|
|
293
342
|
return new Response(content, {
|
|
294
343
|
status,
|
|
295
344
|
headers: {
|
|
296
345
|
'Content-Type': 'text/html',
|
|
297
|
-
...
|
|
346
|
+
...mergedHeaders,
|
|
298
347
|
},
|
|
299
348
|
})
|
|
300
349
|
},
|
|
301
350
|
|
|
302
351
|
/**
|
|
303
|
-
* Plain text response
|
|
352
|
+
* Plain text response. Dual-shape: native `(content, init?)` or
|
|
353
|
+
* positional `(content, status, headers)`.
|
|
304
354
|
*/
|
|
305
|
-
text: (content: string,
|
|
355
|
+
text: (content: string, statusOrInit: number | ResponseInit = 200, headers: Record<string, string> = {}): Response => {
|
|
356
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers)
|
|
306
357
|
return new Response(content, {
|
|
307
358
|
status,
|
|
308
359
|
headers: {
|
|
309
360
|
'Content-Type': 'text/plain',
|
|
310
|
-
...
|
|
361
|
+
...mergedHeaders,
|
|
311
362
|
},
|
|
312
363
|
})
|
|
313
364
|
},
|
|
314
365
|
|
|
315
366
|
/**
|
|
316
|
-
* XML response
|
|
367
|
+
* XML response. Dual-shape: native `(content, init?)` or positional
|
|
368
|
+
* `(content, status, headers)`.
|
|
317
369
|
*/
|
|
318
|
-
xml: (content: string,
|
|
370
|
+
xml: (content: string, statusOrInit: number | ResponseInit = 200, headers: Record<string, string> = {}): Response => {
|
|
371
|
+
const { status, mergedHeaders } = resolveResponseInit(statusOrInit, headers)
|
|
319
372
|
return new Response(content, {
|
|
320
373
|
status,
|
|
321
374
|
headers: {
|
|
322
375
|
'Content-Type': 'application/xml',
|
|
323
|
-
...
|
|
376
|
+
...mergedHeaders,
|
|
324
377
|
},
|
|
325
378
|
})
|
|
326
379
|
},
|
|
@@ -488,6 +488,16 @@ export function registerFileBasedRouting(RouterClass: typeof Router): void {
|
|
|
488
488
|
async value(): Promise<void> {
|
|
489
489
|
if (this._fileRoutesInitialized) return
|
|
490
490
|
|
|
491
|
+
// Ensure each Router instance gets its OWN routes array. The
|
|
492
|
+
// `_fileBasedRoutes: { value: [] }` declaration on the prototype
|
|
493
|
+
// means every instance shares a single array reference until
|
|
494
|
+
// someone reassigns. Without this lazy-init, calls like
|
|
495
|
+
// `instanceA.getFileRoutes()` leak routes registered by
|
|
496
|
+
// `instanceB._initFileRoutes()` — cross-instance contamination.
|
|
497
|
+
if (!Object.prototype.hasOwnProperty.call(this, '_fileBasedRoutes')) {
|
|
498
|
+
this._fileBasedRoutes = []
|
|
499
|
+
}
|
|
500
|
+
|
|
491
501
|
// Auto-detect views directory if not configured
|
|
492
502
|
const viewsDir = this._viewsDir || detectViewsDirectory()
|
|
493
503
|
if (!viewsDir) {
|
|
@@ -496,6 +506,16 @@ export function registerFileBasedRouting(RouterClass: typeof Router): void {
|
|
|
496
506
|
}
|
|
497
507
|
|
|
498
508
|
const config = this._fileRoutingConfig || {}
|
|
509
|
+
|
|
510
|
+
// Honour the opt-out flag set by `router.disableFileRouting()` and by
|
|
511
|
+
// anyone passing `{ enabled: false }` to `router.views(...)`. Before
|
|
512
|
+
// this check the flag was stored but never read, so the public
|
|
513
|
+
// `disableFileRouting()` API silently did nothing.
|
|
514
|
+
if (config.enabled === false) {
|
|
515
|
+
this._fileRoutesInitialized = true
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
499
519
|
const extensions = config.extensions || DEFAULT_EXTENSIONS
|
|
500
520
|
const exclude = config.exclude || DEFAULT_EXCLUDES
|
|
501
521
|
|
|
@@ -5,6 +5,17 @@ import type {
|
|
|
5
5
|
import type { Router } from './router'
|
|
6
6
|
import { FileStreamHandler, StreamHandler } from '../streaming/stream-handler'
|
|
7
7
|
|
|
8
|
+
function closeReadableStreamAfterError(controller: ReadableStreamDefaultController, error: unknown): void {
|
|
9
|
+
void error
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
controller.close()
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// The stream may already be closed or errored by the runtime.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* File streaming extension for Router class
|
|
10
21
|
*/
|
|
@@ -169,7 +180,7 @@ export function registerFileStreaming(RouterClass: typeof Router): void {
|
|
|
169
180
|
controller.close()
|
|
170
181
|
}
|
|
171
182
|
catch (error) {
|
|
172
|
-
controller
|
|
183
|
+
closeReadableStreamAfterError(controller, error)
|
|
173
184
|
}
|
|
174
185
|
},
|
|
175
186
|
})
|
|
@@ -233,7 +244,7 @@ export function registerFileStreaming(RouterClass: typeof Router): void {
|
|
|
233
244
|
controller.close()
|
|
234
245
|
}
|
|
235
246
|
catch (error) {
|
|
236
|
-
controller
|
|
247
|
+
closeReadableStreamAfterError(controller, error)
|
|
237
248
|
}
|
|
238
249
|
},
|
|
239
250
|
})
|
|
@@ -294,7 +305,7 @@ export function registerFileStreaming(RouterClass: typeof Router): void {
|
|
|
294
305
|
controller.close()
|
|
295
306
|
}
|
|
296
307
|
catch (error) {
|
|
297
|
-
controller
|
|
308
|
+
closeReadableStreamAfterError(controller, error)
|
|
298
309
|
}
|
|
299
310
|
},
|
|
300
311
|
})
|
|
@@ -340,7 +351,7 @@ export function registerFileStreaming(RouterClass: typeof Router): void {
|
|
|
340
351
|
controller.close()
|
|
341
352
|
}
|
|
342
353
|
catch (error) {
|
|
343
|
-
controller
|
|
354
|
+
closeReadableStreamAfterError(controller, error)
|
|
344
355
|
}
|
|
345
356
|
},
|
|
346
357
|
})
|