@sigitex/route 1.0.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 +7 -0
- package/README.md +7 -0
- package/package.json +47 -0
- package/src/Assets.ts +4 -0
- package/src/CSP.ts +18 -0
- package/src/HTTP.ts +83 -0
- package/src/Router.ts +100 -0
- package/src/RouterError.ts +31 -0
- package/src/bun/bun.ts +37 -0
- package/src/bun/index.ts +1 -0
- package/src/bun/tsconfig.json +8 -0
- package/src/cloudflare/cloudflare.ts +43 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/tsconfig.json +8 -0
- package/src/env.d.ts +1 -0
- package/src/global.d.ts +1 -0
- package/src/handler/app.ts +27 -0
- package/src/handler/assets.ts +10 -0
- package/src/handler/index.ts +7 -0
- package/src/handler/mount.ts +13 -0
- package/src/handler/noop.ts +3 -0
- package/src/handler/pattern.ts +71 -0
- package/src/handler/prefix.ts +32 -0
- package/src/handler/use.ts +36 -0
- package/src/index.ts +9 -0
- package/src/middleware/bodyLimit.ts +43 -0
- package/src/middleware/cache.ts +53 -0
- package/src/middleware/cookies.ts +76 -0
- package/src/middleware/cors.ts +113 -0
- package/src/middleware/csp.ts +109 -0
- package/src/middleware/csrf.ts +66 -0
- package/src/middleware/filter.ts +10 -0
- package/src/middleware/frameGuard.ts +15 -0
- package/src/middleware/hardened.ts +43 -0
- package/src/middleware/hsts.ts +24 -0
- package/src/middleware/https.ts +9 -0
- package/src/middleware/index.ts +17 -0
- package/src/middleware/noSniff.ts +11 -0
- package/src/middleware/rateLimit.ts +107 -0
- package/src/middleware/referrerPolicy.ts +27 -0
- package/src/middleware/requestId.ts +31 -0
- package/src/middleware/setHeader.ts +7 -0
- package/src/middleware/www.ts +12 -0
- package/src/route.ts +21 -0
- package/src/router.types.ts +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright © 2026 Sigitex
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sigitex/route",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Sigitex",
|
|
7
|
+
"url": "http://github.com/sigitex"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/sigitex/route.git"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.ts",
|
|
15
|
+
"./cloudflare": "./src/cloudflare/index.ts",
|
|
16
|
+
"./bun": "./src/bun/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@cloudflare/workers-types": "^4.20250425.0",
|
|
20
|
+
"@commitlint/cli": "^20.5.3",
|
|
21
|
+
"@commitlint/config-conventional": "^20.5.3",
|
|
22
|
+
"@markwylde/semantic-release-gitea": "^2.2.0",
|
|
23
|
+
"@semantic-release/exec": "^7.0.3",
|
|
24
|
+
"@sigitex/bind": "*",
|
|
25
|
+
"@sigitex/ssjs": "*",
|
|
26
|
+
"@types/bun": "1.3.11",
|
|
27
|
+
"@typescript/native-preview": "beta",
|
|
28
|
+
"husky": "^9.1.7",
|
|
29
|
+
"multi-semantic-release": "^3.1.0",
|
|
30
|
+
"oxfmt": "^0.47.0",
|
|
31
|
+
"oxlint": "^1.62.0",
|
|
32
|
+
"semantic-release": "^25.0.3"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"regexparam": "3.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"check": "tsgo --noEmit && find . -mindepth 2 -name tsconfig.json -not -path '*/node_modules/*' -exec tsgo --noEmit -p {} \\;",
|
|
39
|
+
"test": "bun test --pass-with-no-tests",
|
|
40
|
+
"prepare": "husky",
|
|
41
|
+
"lint": "oxlint"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src"
|
|
45
|
+
],
|
|
46
|
+
"version": "1.0.0"
|
|
47
|
+
}
|
package/src/Assets.ts
ADDED
package/src/CSP.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const nonce = Symbol("csp-nonce")
|
|
2
|
+
|
|
3
|
+
export const CSP = {
|
|
4
|
+
self: "'self'",
|
|
5
|
+
none: "'none'",
|
|
6
|
+
unsafeInline: "'unsafe-inline'",
|
|
7
|
+
unsafeEval: "'unsafe-eval'",
|
|
8
|
+
unsafeHashes: "'unsafe-hashes'",
|
|
9
|
+
strictDynamic: "'strict-dynamic'",
|
|
10
|
+
nonce,
|
|
11
|
+
wildcard: "*",
|
|
12
|
+
data: "data:",
|
|
13
|
+
blob: "blob:",
|
|
14
|
+
https: "https:",
|
|
15
|
+
wss: "wss:",
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
export type CspSource = string | typeof nonce
|
package/src/HTTP.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const HTTP = {
|
|
2
|
+
method: {
|
|
3
|
+
GET: "GET",
|
|
4
|
+
POST: "POST",
|
|
5
|
+
PUT: "PUT",
|
|
6
|
+
PATCH: "PATCH",
|
|
7
|
+
DELETE: "DELETE",
|
|
8
|
+
HEAD: "HEAD",
|
|
9
|
+
OPTIONS: "OPTIONS",
|
|
10
|
+
},
|
|
11
|
+
status: {
|
|
12
|
+
NoContent: 204,
|
|
13
|
+
MovedPermanently: 301,
|
|
14
|
+
Forbidden: 403,
|
|
15
|
+
PayloadTooLarge: 413,
|
|
16
|
+
UnsupportedMediaType: 415,
|
|
17
|
+
TooManyRequests: 429,
|
|
18
|
+
},
|
|
19
|
+
statusText: {
|
|
20
|
+
Forbidden: "Forbidden",
|
|
21
|
+
PayloadTooLarge: "Payload Too Large",
|
|
22
|
+
UnsupportedMediaType: "Unsupported Media Type",
|
|
23
|
+
TooManyRequests: "Too Many Requests",
|
|
24
|
+
},
|
|
25
|
+
header: {
|
|
26
|
+
Accept: "Accept",
|
|
27
|
+
Authorization: "Authorization",
|
|
28
|
+
CacheControl: "Cache-Control",
|
|
29
|
+
ContentLength: "Content-Length",
|
|
30
|
+
ContentType: "Content-Type",
|
|
31
|
+
Cookie: "Cookie",
|
|
32
|
+
Origin: "Origin",
|
|
33
|
+
Referrer: "Referer",
|
|
34
|
+
SetCookie: "Set-Cookie",
|
|
35
|
+
Vary: "Vary",
|
|
36
|
+
XContentTypeOptions: "X-Content-Type-Options",
|
|
37
|
+
XForwardedFor: "X-Forwarded-For",
|
|
38
|
+
XFrameOptions: "X-Frame-Options",
|
|
39
|
+
XRequestId: "X-Request-Id",
|
|
40
|
+
XCsrfToken: "X-CSRF-Token",
|
|
41
|
+
StrictTransportSecurity: "Strict-Transport-Security",
|
|
42
|
+
ReferrerPolicy: "Referrer-Policy",
|
|
43
|
+
AccessControlAllowOrigin: "Access-Control-Allow-Origin",
|
|
44
|
+
AccessControlAllowMethods: "Access-Control-Allow-Methods",
|
|
45
|
+
AccessControlAllowHeaders: "Access-Control-Allow-Headers",
|
|
46
|
+
AccessControlExposeHeaders: "Access-Control-Expose-Headers",
|
|
47
|
+
AccessControlAllowCredentials: "Access-Control-Allow-Credentials",
|
|
48
|
+
AccessControlMaxAge: "Access-Control-Max-Age",
|
|
49
|
+
AccessControlRequestMethod: "Access-Control-Request-Method",
|
|
50
|
+
AccessControlRequestHeaders: "Access-Control-Request-Headers",
|
|
51
|
+
ContentSecurityPolicy: "Content-Security-Policy",
|
|
52
|
+
ContentSecurityPolicyReportOnly: "Content-Security-Policy-Report-Only",
|
|
53
|
+
CFConnectingIP: "CF-Connecting-IP",
|
|
54
|
+
XRateLimitLimit: "X-RateLimit-Limit",
|
|
55
|
+
XRateLimitRemaining: "X-RateLimit-Remaining",
|
|
56
|
+
XRateLimitReset: "X-RateLimit-Reset",
|
|
57
|
+
},
|
|
58
|
+
contentType: {
|
|
59
|
+
json: "application/json",
|
|
60
|
+
urlencoded: "application/x-www-form-urlencoded",
|
|
61
|
+
formData: "multipart/form-data",
|
|
62
|
+
text: "text/plain",
|
|
63
|
+
html: "text/html",
|
|
64
|
+
octetStream: "application/octet-stream",
|
|
65
|
+
},
|
|
66
|
+
referrerPolicy: {
|
|
67
|
+
noReferrer: "no-referrer",
|
|
68
|
+
noReferrerWhenDowngrade: "no-referrer-when-downgrade",
|
|
69
|
+
origin: "origin",
|
|
70
|
+
originWhenCrossOrigin: "origin-when-cross-origin",
|
|
71
|
+
sameOrigin: "same-origin",
|
|
72
|
+
strictOrigin: "strict-origin",
|
|
73
|
+
strictOriginWhenCrossOrigin: "strict-origin-when-cross-origin",
|
|
74
|
+
unsafeUrl: "unsafe-url",
|
|
75
|
+
},
|
|
76
|
+
frameOption: {
|
|
77
|
+
deny: "DENY",
|
|
78
|
+
sameOrigin: "SAMEORIGIN",
|
|
79
|
+
},
|
|
80
|
+
contentTypeOptions: {
|
|
81
|
+
nosniff: "nosniff",
|
|
82
|
+
},
|
|
83
|
+
} as const
|
package/src/Router.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Container } from "@sigitex/bind"
|
|
2
|
+
import type {
|
|
3
|
+
RequestHandler,
|
|
4
|
+
RouterOptions,
|
|
5
|
+
RouteMiddleware,
|
|
6
|
+
RequestContext
|
|
7
|
+
} from "./router.types"
|
|
8
|
+
|
|
9
|
+
export class Router {
|
|
10
|
+
private readonly handlers: RequestHandler[]
|
|
11
|
+
private readonly container: Container | undefined
|
|
12
|
+
private readonly middlewares: RouteMiddleware[]
|
|
13
|
+
|
|
14
|
+
constructor(handlers: RequestHandler[], options: RouterOptions = {}) {
|
|
15
|
+
this.handlers = handlers
|
|
16
|
+
this.container = options.container
|
|
17
|
+
this.middlewares = options.middlewares ?? []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async route(
|
|
21
|
+
request: Request,
|
|
22
|
+
env: Env,
|
|
23
|
+
): Promise<Response> {
|
|
24
|
+
const url = new URL(request.url)
|
|
25
|
+
const context: RequestContext = {
|
|
26
|
+
request,
|
|
27
|
+
env,
|
|
28
|
+
url,
|
|
29
|
+
bind,
|
|
30
|
+
dispatch,
|
|
31
|
+
}
|
|
32
|
+
const container = this.container?.clone()
|
|
33
|
+
try {
|
|
34
|
+
if (container) {
|
|
35
|
+
container.bind(context)
|
|
36
|
+
}
|
|
37
|
+
for (const handler of this.handlers) {
|
|
38
|
+
const result = await dispatch(handler, this.middlewares)
|
|
39
|
+
if (result === undefined) continue
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
return fail(404, "Not found.")
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(error)
|
|
45
|
+
return fail(500, "Internal server error.")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
49
|
+
async function bind(bindings: { [key: string]: any }) {
|
|
50
|
+
if (container) {
|
|
51
|
+
container.bind(bindings)
|
|
52
|
+
} else {
|
|
53
|
+
Object.assign(context, bindings)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function dispatch(
|
|
58
|
+
handler: RequestHandler,
|
|
59
|
+
middlewares: RouteMiddleware[],
|
|
60
|
+
): Promise<Response | undefined> {
|
|
61
|
+
for (const { before } of middlewares) {
|
|
62
|
+
if (!before) continue
|
|
63
|
+
const interrupt = await Promise.resolve(invoke(before))
|
|
64
|
+
if (interrupt !== undefined) return respond(interrupt)
|
|
65
|
+
}
|
|
66
|
+
const result = await Promise.resolve(invoke(handler))
|
|
67
|
+
if (result === undefined) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const response = respond(result)
|
|
71
|
+
bind({ response })
|
|
72
|
+
for (const { after } of middlewares) {
|
|
73
|
+
if (!after) continue
|
|
74
|
+
const interrupt = await Promise.resolve(invoke(after))
|
|
75
|
+
if (interrupt !== undefined) return respond(interrupt)
|
|
76
|
+
}
|
|
77
|
+
return respond(result)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function invoke(handler: RequestHandler) {
|
|
81
|
+
if (container) {
|
|
82
|
+
return container.call(handler)
|
|
83
|
+
}
|
|
84
|
+
return handler(context)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fail(status: number, error: string) {
|
|
90
|
+
return new Response(JSON.stringify({ error }), {
|
|
91
|
+
status,
|
|
92
|
+
statusText: error,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function respond(result: unknown) {
|
|
97
|
+
return result instanceof Response
|
|
98
|
+
? result
|
|
99
|
+
: new Response(JSON.stringify(result))
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class RouterError extends Error {
|
|
2
|
+
readonly code: number
|
|
3
|
+
constructor(code: number, message: string) {
|
|
4
|
+
super(message)
|
|
5
|
+
this.code = code
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class NotFound extends RouterError {
|
|
10
|
+
constructor(message?: string) {
|
|
11
|
+
super(404, message ?? "Not found.")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ServerError extends Error {
|
|
16
|
+
constructor(message?: string) {
|
|
17
|
+
super(message ?? "Internal error.")
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MethodNotAllowed extends RouterError {
|
|
22
|
+
constructor(message?: string) {
|
|
23
|
+
super(405, message ?? "Method not allowed.")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class InvalidRequest extends RouterError {
|
|
28
|
+
constructor(message?: string) {
|
|
29
|
+
super(400, message ?? "Invalid request.")
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/bun/bun.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import type { Assets } from "../Assets"
|
|
3
|
+
import type { RequestHandler } from "../router.types"
|
|
4
|
+
|
|
5
|
+
type BunOptions = {
|
|
6
|
+
readonly assets?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function bun(options: BunOptions = {}): RequestHandler {
|
|
10
|
+
const assets = bunAssets(options.assets ?? "./assets")
|
|
11
|
+
return ({ bind }: { bind: (bindings: { assets: Assets }) => void }) => {
|
|
12
|
+
bind({ assets })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function bunAssets(dir: string): Assets {
|
|
17
|
+
const cache = new Map<string, Response>()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async static(path) {
|
|
21
|
+
const cached = cache.get(path)
|
|
22
|
+
if (cached) return cached.clone() as Response
|
|
23
|
+
const file = Bun.file(join(dir, path))
|
|
24
|
+
const response = new Response(await file.bytes(), {
|
|
25
|
+
headers: { "Content-Type": file.type },
|
|
26
|
+
})
|
|
27
|
+
cache.set(path, response.clone() as Response)
|
|
28
|
+
return response as Response
|
|
29
|
+
},
|
|
30
|
+
async file(request) {
|
|
31
|
+
const path = new URL(request.url).pathname
|
|
32
|
+
const file = Bun.file(join(dir, path))
|
|
33
|
+
if (!await file.exists()) return undefined
|
|
34
|
+
return new Response(file) as Response
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/bun/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./bun"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Assets } from "../Assets"
|
|
2
|
+
import type { RequestHandler } from "../router.types"
|
|
3
|
+
|
|
4
|
+
type CloudflareAssets = {
|
|
5
|
+
readonly fetch: (request: Request | URL | string) => Promise<Response>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type CloudflareOptions = {
|
|
9
|
+
readonly assets?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function cloudflare(options: CloudflareOptions = {}): RequestHandler {
|
|
13
|
+
const assetsKey = options.assets ?? "ASSETS"
|
|
14
|
+
return ({
|
|
15
|
+
env,
|
|
16
|
+
request,
|
|
17
|
+
bind,
|
|
18
|
+
}: {
|
|
19
|
+
env: Env
|
|
20
|
+
request: Request
|
|
21
|
+
bind: (bindings: { assets: Assets }) => void
|
|
22
|
+
}) => {
|
|
23
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
24
|
+
const binding = (env as any)[assetsKey] as CloudflareAssets
|
|
25
|
+
const { origin } = new URL(request.url)
|
|
26
|
+
bind({ assets: cloudflareAssets(binding, origin) })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cloudflareAssets(binding: CloudflareAssets, origin: string): Assets {
|
|
31
|
+
return {
|
|
32
|
+
async static(path) {
|
|
33
|
+
return binding.fetch(new Request(`${origin}${path}`))
|
|
34
|
+
},
|
|
35
|
+
async file(request) {
|
|
36
|
+
const response = await binding.fetch(request)
|
|
37
|
+
if (response.status === 404) {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
return response
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./cloudflare"
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
interface Env {}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@sigitex/ssjs"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as RegexParam from "regexparam"
|
|
2
|
+
import type { Assets } from "../Assets"
|
|
3
|
+
import type { RequestHandler, RouteTree } from "../router.types"
|
|
4
|
+
|
|
5
|
+
export function app(routes: RouteTree): RequestHandler {
|
|
6
|
+
const patterns: RegExp[] = getAppPatterns(routes)
|
|
7
|
+
return async ({ assets, url }: { assets: Assets, url: URL }) => {
|
|
8
|
+
for (const pattern of patterns) {
|
|
9
|
+
if (pattern.test(url.pathname)) {
|
|
10
|
+
return await assets.static("/index.html")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getAppPatterns(tree: RouteTree) {
|
|
18
|
+
const patterns: RegExp[] = []
|
|
19
|
+
for (const [_key, value] of Object.entries(tree)) {
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
patterns.push(RegexParam.parse(value).pattern)
|
|
22
|
+
} else {
|
|
23
|
+
patterns.push(...getAppPatterns(value))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return patterns
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Assets } from "../Assets"
|
|
2
|
+
import type { RequestHandler } from "../router.types"
|
|
3
|
+
|
|
4
|
+
export function assets(): RequestHandler {
|
|
5
|
+
return async ({ assets, request }: { assets: Assets, request: Request }) => {
|
|
6
|
+
const response = await assets.file(request)
|
|
7
|
+
if (!response || response.status === 404) return
|
|
8
|
+
return response
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
import type { RequestHandler } from "../router.types"
|
|
3
|
+
|
|
4
|
+
/** Wraps a standard fetch function as a router handler. */
|
|
5
|
+
export function mount(handler: (request: Request) => Promise<Response>): RequestHandler {
|
|
6
|
+
return ({ request, url, container }: any) => {
|
|
7
|
+
const mapped = new Request(url.href, request)
|
|
8
|
+
if ("__mount" in handler && container) {
|
|
9
|
+
return (handler as any).__mount(mapped, container)
|
|
10
|
+
}
|
|
11
|
+
return handler(mapped)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as RegexParam from "regexparam"
|
|
2
|
+
import { MethodNotAllowed } from "../RouterError"
|
|
3
|
+
import type { RequestContext, RequestHandler, RouteMiddleware } from "../router.types"
|
|
4
|
+
|
|
5
|
+
export function pattern(
|
|
6
|
+
method: string | null,
|
|
7
|
+
path: string,
|
|
8
|
+
handler: RequestHandler,
|
|
9
|
+
...middlewares: RouteMiddleware[]
|
|
10
|
+
): RequestHandler {
|
|
11
|
+
const { pattern, keys } = RegexParam.parse(path)
|
|
12
|
+
return ({ url, request, bind, dispatch }: RequestContext) => {
|
|
13
|
+
const match = pattern.exec(url.pathname)
|
|
14
|
+
if (!match) return
|
|
15
|
+
if (method !== null && request.method !== method) {
|
|
16
|
+
return new MethodNotAllowed(`Please use "${method}".`)
|
|
17
|
+
}
|
|
18
|
+
const params: { [key: string]: string } = {}
|
|
19
|
+
if (keys.length > 0) {
|
|
20
|
+
for (let k = 0; k < keys.length; k++) {
|
|
21
|
+
let key = keys[k]
|
|
22
|
+
if (key === "*") {
|
|
23
|
+
key = "path"
|
|
24
|
+
}
|
|
25
|
+
params[key] = match[k + 1]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
bind({ params })
|
|
29
|
+
return dispatch(handler, middlewares)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function get(
|
|
34
|
+
path: string,
|
|
35
|
+
handler: RequestHandler,
|
|
36
|
+
...middlewares: RouteMiddleware[]
|
|
37
|
+
): RequestHandler {
|
|
38
|
+
return pattern("GET", path, handler, ...middlewares)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function post(
|
|
42
|
+
path: string,
|
|
43
|
+
handler: RequestHandler,
|
|
44
|
+
...middlewares: RouteMiddleware[]
|
|
45
|
+
): RequestHandler {
|
|
46
|
+
return pattern("POST", path, handler, ...middlewares)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function put(
|
|
50
|
+
path: string,
|
|
51
|
+
handler: RequestHandler,
|
|
52
|
+
...middlewares: RouteMiddleware[]
|
|
53
|
+
): RequestHandler {
|
|
54
|
+
return pattern("PUT", path, handler, ...middlewares)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function del(
|
|
58
|
+
path: string,
|
|
59
|
+
handler: RequestHandler,
|
|
60
|
+
...middlewares: RouteMiddleware[]
|
|
61
|
+
): RequestHandler {
|
|
62
|
+
return pattern("DELETE", path, handler, ...middlewares)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function patch(
|
|
66
|
+
path: string,
|
|
67
|
+
handler: RequestHandler,
|
|
68
|
+
...middlewares: RouteMiddleware[]
|
|
69
|
+
): RequestHandler {
|
|
70
|
+
return pattern("PATCH", path, handler, ...middlewares)
|
|
71
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RequestContext, RequestHandler, RouteMiddleware } from "../router.types"
|
|
2
|
+
import { use } from "./use"
|
|
3
|
+
|
|
4
|
+
export function prefix(
|
|
5
|
+
prefix: string,
|
|
6
|
+
...handlers: RequestHandler[]
|
|
7
|
+
): RequestHandler
|
|
8
|
+
export function prefix(
|
|
9
|
+
prefix: string,
|
|
10
|
+
middlewares: RouteMiddleware[],
|
|
11
|
+
...handlers: RequestHandler[]
|
|
12
|
+
): RequestHandler
|
|
13
|
+
export function prefix(
|
|
14
|
+
prefix: string,
|
|
15
|
+
head: RouteMiddleware[] | RequestHandler,
|
|
16
|
+
...tail: RequestHandler[]
|
|
17
|
+
): RequestHandler {
|
|
18
|
+
const [middlewares, handlers] = Array.isArray(head)
|
|
19
|
+
? [head, tail]
|
|
20
|
+
: [[], [head, ...tail]]
|
|
21
|
+
const root = prefix.endsWith("/") ? prefix : prefix + "/"
|
|
22
|
+
const handle = use(middlewares, ...handlers)
|
|
23
|
+
return (context: RequestContext) => {
|
|
24
|
+
if (!context.url.pathname.startsWith(root)) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const url = new URL(context.url.href)
|
|
28
|
+
url.pathname = url.pathname.slice(root.length - 1) || "/"
|
|
29
|
+
context.bind({ url })
|
|
30
|
+
return handle(context)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RequestContext, RequestHandler, RouteMiddleware } from "../router.types"
|
|
2
|
+
import { noop } from "./noop"
|
|
3
|
+
|
|
4
|
+
export function use(
|
|
5
|
+
middlewares: RouteMiddleware[],
|
|
6
|
+
...handlers: RequestHandler[]
|
|
7
|
+
): RequestHandler {
|
|
8
|
+
const befores = middlewares.filter(({ before }) => before)
|
|
9
|
+
const afters = middlewares
|
|
10
|
+
.filter(({ after }) => after)
|
|
11
|
+
.map(({ after }) => ({ before: after }) as RouteMiddleware)
|
|
12
|
+
return async ({ dispatch, bind }: RequestContext) => {
|
|
13
|
+
if (befores.length) {
|
|
14
|
+
const interruptBefore = await dispatch(noop, befores)
|
|
15
|
+
if (interruptBefore !== undefined) {
|
|
16
|
+
return interruptBefore
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let response: Response | undefined
|
|
20
|
+
for (const handler of handlers) {
|
|
21
|
+
response = await dispatch(handler, [])
|
|
22
|
+
if (response !== undefined) break
|
|
23
|
+
}
|
|
24
|
+
if (response === undefined) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
bind({ response })
|
|
28
|
+
if (afters.length) {
|
|
29
|
+
const interruptAfter = await dispatch(noop, afters)
|
|
30
|
+
if (interruptAfter !== undefined) {
|
|
31
|
+
return interruptAfter
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return response
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED