starlight-server 0.0.1

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/demo/index.d.ts +1 -0
  4. package/dist/demo/index.js +27 -0
  5. package/dist/http/body/form-data.d.ts +35 -0
  6. package/dist/http/body/form-data.js +141 -0
  7. package/dist/http/body/index.d.ts +23 -0
  8. package/dist/http/body/index.js +47 -0
  9. package/dist/http/body/receive.d.ts +7 -0
  10. package/dist/http/body/receive.js +39 -0
  11. package/dist/http/http-status.d.ts +9 -0
  12. package/dist/http/http-status.js +64 -0
  13. package/dist/http/index.d.ts +9 -0
  14. package/dist/http/index.js +9 -0
  15. package/dist/http/mime-types.d.ts +14 -0
  16. package/dist/http/mime-types.js +764 -0
  17. package/dist/http/request.d.ts +25 -0
  18. package/dist/http/request.js +40 -0
  19. package/dist/http/response.d.ts +32 -0
  20. package/dist/http/response.js +66 -0
  21. package/dist/http/server.d.ts +31 -0
  22. package/dist/http/server.js +52 -0
  23. package/dist/http/types.d.ts +26 -0
  24. package/dist/http/types.js +12 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +4 -0
  27. package/dist/logging.d.ts +24 -0
  28. package/dist/logging.js +30 -0
  29. package/dist/router/cors.d.ts +24 -0
  30. package/dist/router/cors.js +35 -0
  31. package/dist/router/index.d.ts +38 -0
  32. package/dist/router/index.js +36 -0
  33. package/dist/router/match.d.ts +23 -0
  34. package/dist/router/match.js +172 -0
  35. package/dist/router/parameters.d.ts +51 -0
  36. package/dist/router/parameters.js +118 -0
  37. package/dist/router/router.d.ts +127 -0
  38. package/dist/router/router.js +97 -0
  39. package/dist/swagger/index.d.ts +1 -0
  40. package/dist/swagger/index.js +168 -0
  41. package/dist/swagger/openapi-spec.d.ts +261 -0
  42. package/dist/swagger/openapi-spec.js +5 -0
  43. package/dist/validators/array.d.ts +9 -0
  44. package/dist/validators/array.js +28 -0
  45. package/dist/validators/boolean.d.ts +4 -0
  46. package/dist/validators/boolean.js +28 -0
  47. package/dist/validators/common.d.ts +23 -0
  48. package/dist/validators/common.js +25 -0
  49. package/dist/validators/index.d.ts +20 -0
  50. package/dist/validators/index.js +38 -0
  51. package/dist/validators/number.d.ts +10 -0
  52. package/dist/validators/number.js +30 -0
  53. package/dist/validators/object.d.ts +13 -0
  54. package/dist/validators/object.js +36 -0
  55. package/dist/validators/string.d.ts +11 -0
  56. package/dist/validators/string.js +29 -0
  57. package/package.json +54 -0
  58. package/src/demo/index.ts +33 -0
  59. package/src/http/body/form-data.ts +164 -0
  60. package/src/http/body/index.ts +59 -0
  61. package/src/http/body/receive.ts +49 -0
  62. package/src/http/http-status.ts +65 -0
  63. package/src/http/index.ts +9 -0
  64. package/src/http/mime-types.ts +765 -0
  65. package/src/http/request.ts +44 -0
  66. package/src/http/response.ts +73 -0
  67. package/src/http/server.ts +67 -0
  68. package/src/http/types.ts +31 -0
  69. package/src/index.ts +4 -0
  70. package/src/logging.ts +57 -0
  71. package/src/router/cors.ts +54 -0
  72. package/src/router/index.ts +38 -0
  73. package/src/router/match.ts +194 -0
  74. package/src/router/parameters.ts +172 -0
  75. package/src/router/router.ts +233 -0
  76. package/src/swagger/index.ts +184 -0
  77. package/src/swagger/openapi-spec.ts +312 -0
  78. package/src/validators/array.ts +33 -0
  79. package/src/validators/boolean.ts +23 -0
  80. package/src/validators/common.ts +46 -0
  81. package/src/validators/index.ts +50 -0
  82. package/src/validators/number.ts +36 -0
  83. package/src/validators/object.ts +41 -0
  84. package/src/validators/string.ts +38 -0
@@ -0,0 +1,44 @@
1
+ import type http from 'node:http'
2
+ import { type Logger } from '@anjianshi/utils'
3
+ import { RequestBody, type BodyOptions } from './body/index.js'
4
+ import { HTTPError, type NodeRequest } from './types.js'
5
+
6
+ /**
7
+ * 对 Node.js 请求内容的二次封装
8
+ * - 提供经过整理的请求信息
9
+ * - 自带 body 解析(实现了 “body 大小限制” 和 “JSON、form-data 等格式的内容解析”)
10
+ *
11
+ * Secondary encapsulation of the Node.js Request object
12
+ * - Organized request information
13
+ * - Comes with body parsing (implements 'body size limit' and 'content parsing for formats such as JSON and form-data').
14
+ */
15
+ export class Request {
16
+ readonly method: string
17
+ readonly host: string
18
+ readonly path: string
19
+ readonly query: URLSearchParams
20
+ readonly headers: http.IncomingHttpHeaders
21
+ readonly body: RequestBody
22
+
23
+ constructor(
24
+ readonly nodeRequest: NodeRequest,
25
+ protected readonly logger: Logger,
26
+ bodyOptions: BodyOptions,
27
+ ) {
28
+ if (nodeRequest.method === undefined) throw new HTTPError(405)
29
+ this.method = nodeRequest.method
30
+
31
+ try {
32
+ const url = new URL(nodeRequest.url ?? '', `http://${nodeRequest.headers.host ?? ''}`)
33
+ this.host = url.host
34
+ this.path = url.pathname
35
+ this.query = url.searchParams
36
+ } catch (e) {
37
+ this.logger.warn('parse url failed', e)
38
+ throw new HTTPError(400, 'url invalid')
39
+ }
40
+
41
+ this.headers = nodeRequest.headers
42
+ this.body = new RequestBody(nodeRequest, bodyOptions)
43
+ }
44
+ }
@@ -0,0 +1,73 @@
1
+ import { type Logger } from '@anjianshi/utils'
2
+ import { path2MIMEType } from './mime-types.js'
3
+ import { HTTPError, type NodeResponse } from './types.js'
4
+
5
+ /**
6
+ * 封装响应内容输出函数
7
+ * Encapsulate functions for outputting response content.
8
+ */
9
+ export class ResponseUtils {
10
+ constructor(
11
+ readonly nodeResponse: NodeResponse,
12
+ readonly logger: Logger,
13
+ ) {}
14
+
15
+ header(name: string, value: string) {
16
+ this.nodeResponse.setHeader(name, value)
17
+ }
18
+
19
+ headers(...items: [string, string][]) {
20
+ items.forEach(([name, value]) => this.header(name, value))
21
+ }
22
+
23
+ /**
24
+ * Output CORS Headers
25
+ */
26
+ cors(allowOrigin = '*', allowHeaders = '*') {
27
+ this.headers(
28
+ ['Access-Control-Allow-Origin', allowOrigin],
29
+ ['Access-Control-Allow-Headers', allowHeaders],
30
+ )
31
+ }
32
+
33
+ text(content: string | Buffer) {
34
+ this.nodeResponse.end(content)
35
+ }
36
+
37
+ /**
38
+ * Output JSON
39
+ */
40
+ json(data: unknown) {
41
+ try {
42
+ const json = JSON.stringify(data)
43
+ this.header('Content-Type', 'application/json; charset=UTF-8')
44
+ this.nodeResponse.end(json)
45
+ } catch (e) {
46
+ throw new HTTPError(500, 'Invalid JSON Response')
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 原样输出文件内容,并带上文件类型对应的 MIME Type
52
+ * Output the file content and include the MIME Type corresponding to the file type.
53
+ */
54
+ file(content: Buffer | string, path: string) {
55
+ const mimeType = path2MIMEType(path)
56
+ if (mimeType !== null) this.header('Content-Type', mimeType)
57
+ this.nodeResponse.end(content)
58
+ }
59
+
60
+ /**
61
+ * Output HTTP Error
62
+ */
63
+ error(error: unknown) {
64
+ if (error instanceof HTTPError) {
65
+ this.nodeResponse.statusCode = error.status // eslint-disable-line require-atomic-updates
66
+ this.nodeResponse.end(error.message)
67
+ } else {
68
+ this.logger.error(error)
69
+ this.nodeResponse.statusCode = 500 // eslint-disable-line require-atomic-updates
70
+ this.nodeResponse.end(new HTTPError(500).message)
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,67 @@
1
+ import http from 'node:http'
2
+ import { type Logger, LogLevel } from '@anjianshi/utils'
3
+ import { type BodyOptions } from './body/index.js'
4
+ import { Request } from './request.js'
5
+ import { ResponseUtils } from './response.js'
6
+ import { type NodeRequest, type NodeResponse } from './types.js'
7
+
8
+ export type RequestHandler = (request: Request, response: ResponseUtils) => unknown
9
+
10
+ /**
11
+ * 二次封装 Node.js HTTP Server,实现了更完善的 request 和 response 处理
12
+ * Secondary encapsulation of the Node.js HTTP Server, with more complete request and response handling.
13
+ *
14
+ * 功能点:
15
+ * - 经过整理的 request 信息,且自带 body 解析
16
+ * - 快速输出指定格式的响应内容
17
+ * - 自带异常处理
18
+ * - 任意处抛出 HTTPError,可以指定 HTTP Status 结束请求
19
+ * - 抛出其他 Error,则以 Status 500 结束请求
20
+ *
21
+ * Features:
22
+ * - Organized request information, and built-in body parsing
23
+ * - Quickly output response content in a specified format
24
+ * - Built-in exception handling
25
+ * - Throw HTTPError anywhere, and end the request with the specified HTTP Status
26
+ * - If another Error is thrown, end the request with Status 500
27
+ *
28
+ * Usage:
29
+ * startHTTPServer(handler, options)
30
+ */
31
+ export function startHTTPServer(
32
+ options: BodyOptions & {
33
+ handler?: RequestHandler
34
+ logger: Logger
35
+ port: number
36
+ },
37
+ ) {
38
+ const { handler = placeholderHandler, logger, port, ...bodyOptions } = options
39
+
40
+ async function handleRequest(nodeRequest: NodeRequest, nodeResponse: NodeResponse) {
41
+ const logStart = Date.now()
42
+ const logMethod = nodeRequest.method ?? 'UNKNOWN'
43
+ const logUrl = nodeRequest.url ?? ''
44
+ logger.info('<--', logMethod, logUrl)
45
+
46
+ const response = new ResponseUtils(nodeResponse, logger)
47
+ try {
48
+ const request = new Request(nodeRequest, logger, bodyOptions)
49
+ await handler(request, response)
50
+ } catch (error) {
51
+ response.error(error)
52
+ }
53
+
54
+ const logUsage = Date.now() - logStart
55
+ const logStatus = nodeResponse.statusCode
56
+ const logLevel = logStatus >= 400 ? LogLevel.Warning : LogLevel.Info
57
+ logger.log(logLevel, ['-->', logMethod, logUrl, logStatus, `${logUsage}ms`])
58
+ }
59
+
60
+ const server = http.createServer(handleRequest)
61
+ logger.info('Listening', port)
62
+ server.listen(port)
63
+ }
64
+
65
+ function placeholderHandler(request: Request, response: ResponseUtils) {
66
+ response.json({ message: 'Hello World, from starlight.' })
67
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 重新定义 HTTP 请求相关类型
3
+ * Redefine HTTP request-related types.
4
+ */
5
+ import type http from 'node:http'
6
+ import { HTTPStatusMap } from './http-status.js'
7
+
8
+ /**
9
+ * Node.js 原生的请求对象
10
+ * Node.js native request object
11
+ */
12
+ export type NodeRequest = http.IncomingMessage
13
+
14
+ /**
15
+ * Node.js 原生的响应对象
16
+ * Node.js native response object
17
+ */
18
+ export type NodeResponse = http.ServerResponse & { req: http.IncomingMessage }
19
+
20
+ /**
21
+ * 带有 HTTP Status 的错误对象
22
+ * Error object with HTTP Status
23
+ */
24
+ export class HTTPError extends Error {
25
+ readonly status: number
26
+
27
+ constructor(status: number, message?: string) {
28
+ super(message ?? HTTPStatusMap.get(status) ?? status.toString())
29
+ this.status = status
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from '@/http/index.js'
2
+ export * from '@/router/index.js'
3
+ export * from '@/logging.js'
4
+ // export * from '@/swagger/index.js'
package/src/logging.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 实现日志记录
3
+ */
4
+ import { truthy, Logger, LogLevel, adaptDebugLib } from '@anjianshi/utils'
5
+ import {
6
+ ConsoleHandler,
7
+ FileHandler,
8
+ type FileHandlerOptions,
9
+ } from '@anjianshi/utils/env-node/logging.js'
10
+ import debug from 'debug'
11
+
12
+ export type { FileHandlerOptions }
13
+
14
+ export interface LoggingOptions {
15
+ /**
16
+ * 指定日志等级(debug info warn err)
17
+ * Specify log level.
18
+ */
19
+ level?: string
20
+
21
+ /**
22
+ * 文件日志参数
23
+ * File log options
24
+ */
25
+ file?: Partial<FileHandlerOptions>
26
+
27
+ /**
28
+ * 是否把 debug 库的日志引入进来;可通过字符串指定匹配规则
29
+ * Whether to import logs from 'debug' library; can specify matching rules through strings
30
+ */
31
+ debugLib?: boolean | string
32
+ }
33
+
34
+ const levelMap: Record<string, LogLevel> = {
35
+ debug: LogLevel.Debug,
36
+ info: LogLevel.Info,
37
+ warn: LogLevel.Warning,
38
+ warning: LogLevel.Warning,
39
+ err: LogLevel.Error,
40
+ error: LogLevel.Error,
41
+ }
42
+
43
+ export function getLogger(options: LoggingOptions = {}) {
44
+ const logger = new Logger()
45
+ logger.addHandler(new ConsoleHandler())
46
+ if (options.level !== undefined) {
47
+ const level = levelMap[options.level.toLowerCase()]
48
+ if (level !== undefined) logger.setLevel(level)
49
+ }
50
+ if (options.file) {
51
+ logger.addHandler(new FileHandler(options.file))
52
+ }
53
+ if (truthy(options.debugLib)) {
54
+ void adaptDebugLib(debug, options.debugLib === true ? '*' : options.debugLib, logger)
55
+ }
56
+ return logger
57
+ }
@@ -0,0 +1,54 @@
1
+ import { type Request, type ResponseUtils } from '@/http/index.js'
2
+
3
+ /**
4
+ * CORS 配置
5
+ *
6
+ * - false: 不处理跨域
7
+ * - true: 支持且不设限(等同于 { allowOrigin: '*', allowHeaders: '*', exposeHeaders: '*' })
8
+ * - { allowOrigin, allowHeaders, exposeHeaders }: 手动配置
9
+ *
10
+ * allowHeaders 仅对 Preflight 请求有效
11
+ * exposeHeaders 仅对非 Preflight 请求有效
12
+ */
13
+ export type CORSOptions =
14
+ | boolean
15
+ | { allowOrigin?: string; allowHeaders?: string; exposeHeaders?: string }
16
+
17
+ /**
18
+ * 若当前请求是 CORS Preflight 请求,返回实际请求的 method,否则返回 null
19
+ */
20
+ export function getMethodFromCORSPreflight(request: Request) {
21
+ if (
22
+ request.method === 'OPTIONS' &&
23
+ request.headers['Access-Control-Request-Methods'] !== undefined
24
+ ) {
25
+ const rawMethod = request.headers['Access-Control-Request-Methods']
26
+ return Array.isArray(rawMethod) ? rawMethod[0]! : rawMethod
27
+ }
28
+ return null
29
+ }
30
+
31
+ /**
32
+ * 输出 CORS 相关 headers
33
+ */
34
+ export function outputCORSHeaders(
35
+ response: ResponseUtils,
36
+ cors: CORSOptions,
37
+ isPreflight: boolean,
38
+ ) {
39
+ if (cors === true) {
40
+ response.headers(['Access-Control-Allow-Origin', '*'])
41
+ if (isPreflight) response.headers(['Access-Control-Allow-Headers', '*'])
42
+ else response.headers(['Access-Control-Expose-Headers', '*'])
43
+ } else if (cors !== false) {
44
+ if (cors.allowOrigin !== undefined)
45
+ response.header('Access-Control-Allow-Origin', cors.allowOrigin)
46
+ if (isPreflight && cors.allowHeaders !== undefined)
47
+ response.header('Access-Control-Allow-Headers', cors.allowHeaders)
48
+ if (!isPreflight && cors.exposeHeaders !== undefined)
49
+ response.header('Access-Control-Expose-Headers', cors.exposeHeaders)
50
+ } else {
51
+ // 若指定为“不处理跨域”,则不输出 headers,浏览器会默认服务端不允许跨域请求
52
+ // (后续 route handler 仍有机会自行设置 CORS headers)
53
+ }
54
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 实现路由注册
3
+ * Implement route registration
4
+ *
5
+ * Usage:
6
+ * const router = new DefaultRouter()
7
+ * router.register('GET', '/hello/:name', async ({ request, response }: { request: Request, response: ResponseUtils }, params: { name: string }) => {
8
+ * response.json({ hello: params.name })
9
+ * })
10
+ * startHTTPServer({
11
+ * handler: router.handle
12
+ * ...
13
+ * })
14
+ *
15
+ * Usage With Custom Context:
16
+ * interface MyContext extends BaseContext {
17
+ * foo: () => void
18
+ * }
19
+ * class MyRouter extends BaseRouter<MyContext> {
20
+ * executeWithContext(baseContext: BaseContext, route: Route<MyContext>, params: PathParamaters) {
21
+ * function foo() { console.log('bar') }
22
+ * const context = { ...baseContext, foo }
23
+ * route.handle(context)
24
+ * }
25
+ * }
26
+ * const router = new MyRouter()
27
+ * router.register('GET', '/foobar', async ({ request, response, foo }) => {
28
+ * foo()
29
+ * response.json({})
30
+ * })
31
+ * startHTTPServer({
32
+ * handler: router.handle
33
+ * ...
34
+ * })
35
+ */
36
+ export * from './router.js'
37
+ export { type PathParameters } from './match.js'
38
+ export type { BasicDataType, Parameter, ParameterDataType } from './parameters.js'
@@ -0,0 +1,194 @@
1
+ /**
2
+ * 匹配路由路径
3
+ *
4
+ * 支持的格式:
5
+ * 1. /abc/ abc/ /abc => 这几种形式等价
6
+ * 2. /abc/:foo/bar => 命名参数(foo)
7
+ * 3. /abc/:foo?/bar => 可选命名参数(foo)
8
+ * 4. /abc/* => 后续路径(任意长度,* 只能出现在路由最后面,匹配结果不含开头的“/”)
9
+ *
10
+ * 路由不区分大小写
11
+ */
12
+ import escapeRegExp from 'lodash/escapeRegExp.js'
13
+
14
+ /**
15
+ * 标准化路径
16
+ * 标准化后,相同的两个路径一定也是相同的字符串(例如 /Abc/Def 和 abc/def/ 都会变成 abc/def)。
17
+ *
18
+ * - 移除首尾和重复的 '/',完成后有 path 有这几种可能的格式: ''、'abc'、'abc/def'
19
+ * - 统一改为小写
20
+ */
21
+ export function normalizePath(path: string) {
22
+ if (path.startsWith('/')) path = path.slice(1)
23
+ if (path.endsWith('/')) path = path.slice(0, -1)
24
+ path = path.replace(/\/+/g, '/')
25
+ return path.toLowerCase()
26
+ }
27
+
28
+ /**
29
+ * 解析路由路径定义
30
+ * '/abc/:foo?/*' => [{ type: 'text', text: 'abc' }, { type: 'named', name: 'foo', optional: true }, { type: 'rest' }]
31
+ */
32
+ function parseRoutePath(routePath: string): ParsedPath {
33
+ const normalizedPath = normalizePath(routePath)
34
+
35
+ const nodes: PathNode[] = []
36
+ const pathParts = normalizedPath.split('/')
37
+ for (const [i, part] of pathParts.entries()) {
38
+ if (part.startsWith(':') && part !== ':' && part !== ':?') {
39
+ const optional = part.endsWith('?')
40
+ const name = optional ? part.slice(1, -1) : part.slice(1)
41
+ if (name === '*') throw new Error('route path parameter name cannot be "*"')
42
+ nodes.push({ type: PathNodeType.Named, name, optional })
43
+ } else if (part === '*' && i === pathParts.length - 1) {
44
+ nodes.push({ type: PathNodeType.Rest })
45
+ } else {
46
+ nodes.push({ type: PathNodeType.Text, text: part })
47
+ }
48
+ }
49
+
50
+ return nodes
51
+ }
52
+
53
+ enum PathNodeType {
54
+ /** 纯文本 */
55
+ Text = 'text',
56
+ /** 命名参数 */
57
+ Named = 'named',
58
+ /** 后续路径 */
59
+ Rest = 'rest',
60
+ }
61
+ type PathNode =
62
+ | { type: PathNodeType.Text; text: string }
63
+ | { type: PathNodeType.Named; name: string; optional: boolean }
64
+ | { type: PathNodeType.Rest }
65
+ type ParsedPath = PathNode[]
66
+
67
+ /**
68
+ * 把路由定义转换成正则表达式
69
+ *
70
+ * Nodes | Path | RegExp
71
+ * -------------------------- | ------------- | ----------------------------
72
+ * [] | | ^$
73
+ * ['abc'] | abc | ^abc$
74
+ * ['abc', named:foo, 'xyz'] | abc/:foo/xyz | ^abc/([^/]+?)/xyz$
75
+ * ['abc', named:foo?, 'xyz'] | abc/:foo?/xyz | ^abc(?:/([^/]+?))?/xyz$
76
+ * ['abc', named:foo?, rest] | abc/:foo?/* | ^abc(?:/([^/]+?))?(?:/(.+))?$
77
+ *
78
+ * ^(?:/(.+))?$
79
+ */
80
+ function parsedPathToRegExp(parsedPath: ParsedPath) {
81
+ const regexpParts: string[] = []
82
+ for (const node of parsedPath) {
83
+ const prefix = node === parsedPath[0] ? '' : '/'
84
+ if (node.type === PathNodeType.Text) {
85
+ regexpParts.push(prefix + escapeRegExp(node.text))
86
+ } else if (node.type === PathNodeType.Named) {
87
+ regexpParts.push(node.optional ? `(?:${prefix}([^/]+?))?` : `${prefix}([^/]+?)`)
88
+ } else {
89
+ regexpParts.push(`(?:${prefix}(.+))?`)
90
+ }
91
+ }
92
+ return new RegExp('^' + regexpParts.join('') + '$', 'i')
93
+ }
94
+
95
+ /**
96
+ * 比较两个路径的优先级(两个路由都能匹配路径时,优先级更高的生效)
97
+ *
98
+ * pathA 优先:-1
99
+ * pathB 优先:1
100
+ *
101
+ * 规则:
102
+ * 1. 挨个 node 比对
103
+ * 2. 先匹配各节点的类型,text > named > named optional > rest
104
+ * 3. 节点类型都一致,节点数量多的优先级更高
105
+ * 4. 节点类型、数量都一样,后出现的覆盖先出现的
106
+ */
107
+ function pathSorter(pathA: ParsedPath, pathB: ParsedPath) {
108
+ const typePriorities = [PathNodeType.Text, PathNodeType.Named, PathNodeType.Rest]
109
+
110
+ const length = Math.max(pathA.length, pathB.length)
111
+ for (let i = 0; i < length; i++) {
112
+ const a = pathA[i]
113
+ const b = pathB[i]
114
+
115
+ // 若 nodes 长度不一样,长的优先
116
+ if (a === undefined) return -1
117
+ else if (b === undefined) return 1
118
+
119
+ // node 类型不一样,按类型排序
120
+ const aTypePriority = typePriorities.indexOf(a.type)
121
+ const bTypePriority = typePriorities.indexOf(b.type)
122
+ if (aTypePriority !== bTypePriority) return aTypePriority - bTypePriority
123
+
124
+ // 若都是 named 类型但 optional 不同
125
+ if (
126
+ a.type === PathNodeType.Named &&
127
+ b.type === PathNodeType.Named &&
128
+ a.optional !== b.optional
129
+ ) {
130
+ return a.optional ? 1 : -1
131
+ }
132
+ }
133
+
134
+ // 长度、类型都一样,后出现的优先
135
+ return 1
136
+ }
137
+
138
+ /**
139
+ * 返回匹配的路由及匹配上的路径参数
140
+ */
141
+ export function matchRoutes<R extends BasicRoute>(requestPath: string, routes: R[]) {
142
+ if (!parsedCache.has(routes)) {
143
+ // 解析路由路径并按优先级排序
144
+ const parsedRoutes = routes
145
+ .map(route => {
146
+ const parsedPath = parseRoutePath(route.path)
147
+ const regexp = parsedPathToRegExp(parsedPath)
148
+ return { parsedPath, regexp, route }
149
+ })
150
+ .sort((a, b) => pathSorter(a.parsedPath, b.parsedPath))
151
+ parsedCache.set(routes, parsedRoutes)
152
+ }
153
+
154
+ requestPath = normalizePath(requestPath)
155
+ const parsedRoutes = parsedCache.get(routes)!
156
+ return parsedRoutes
157
+ .map(({ parsedPath, regexp, route }) => {
158
+ const match = regexp.exec(requestPath)
159
+ if (!match) return null
160
+
161
+ const pathParameters = matchPathParameters(parsedPath, [...match].slice(1))
162
+ return { route, pathParameters } as RouteMatch<R>
163
+ })
164
+ .filter((matched): matched is Exclude<typeof matched, null> => matched !== null)
165
+ }
166
+ const parsedCache = new WeakMap<
167
+ BasicRoute[], // eslint-disable-line @typescript-eslint/no-explicit-any
168
+ { parsedPath: ParsedPath; regexp: RegExp; route: BasicRoute }[] // eslint-disable-line @typescript-eslint/no-explicit-any
169
+ >()
170
+
171
+ export type BasicRoute = { path: string }
172
+
173
+ export interface RouteMatch<R> {
174
+ route: R
175
+ pathParameters: PathParameters
176
+ }
177
+
178
+ /**
179
+ * 把匹配到的路径参数映射成键值对
180
+ * 没匹配到的内容(optional named node / rest node)值为 undefined
181
+ */
182
+ function matchPathParameters(parsedPath: ParsedPath, parameterNames: (string | undefined)[]) {
183
+ const parameters: PathParameters = {}
184
+ for (const node of parsedPath) {
185
+ if (node.type === PathNodeType.Text) continue
186
+ else if (node.type === PathNodeType.Named) parameters[node.name] = parameterNames.shift()!
187
+ else parameters['*'] = parameterNames.shift()!
188
+ }
189
+ return parameters
190
+ }
191
+ export type PathParameters = {
192
+ [name: string]: string | undefined
193
+ '*'?: string
194
+ }