Comparing version 1.0.1 to 2.0.0
@@ -5,23 +5,25 @@ // Generated by dts-bundle-generator v9.5.1 | ||
import { LRUCache } from 'lru-cache'; | ||
import { match } from 'path-to-regexp'; | ||
import { RequireAtLeastOne } from 'type-fest'; | ||
export type Registration<T> = { | ||
matcher: ReturnType<typeof match<Record<string, string>>>; | ||
matcher: (subject: string) => null | Record<string, string>; | ||
pattern: string; | ||
regex: RegExp; | ||
methodFilter: null | ((subject: string) => boolean); | ||
target: T; | ||
}; | ||
declare class PathMatcher<Target extends any> { | ||
export type Result<T> = Array<[ | ||
T, | ||
Record<string, string> | ||
]>; | ||
declare class RouteMatcher<Target extends any> { | ||
registered: Registration<Target>[]; | ||
add(path: string | RegExp, target: Target): void; | ||
match(path: string, filter?: (target: Target) => boolean, fallbacks?: Function[]): { | ||
target: any; | ||
params: Record<string, string>; | ||
}[]; | ||
match(method: string, subject: string, fallbacks?: Target[]): Result<Target>; | ||
add(method: string, pattern: string | RegExp, target: Target): this; | ||
detectPotentialDos(detector: any, config?: any): void; | ||
} | ||
declare class MatcherWithCache<Target = any> { | ||
matcher: PathMatcher<Target>; | ||
declare class MatcherWithCache<Target = any> extends RouteMatcher<Target> { | ||
cache: LRUCache<string, any>; | ||
constructor(matcher: PathMatcher<Target>, size?: number); | ||
add(path: string | RegExp, target: Target): void; | ||
match(path: string, filter?: (target: Target) => boolean, fallbacks?: Function[]): any; | ||
constructor(size?: number); | ||
match(method: string, subject: string, fallbacks?: Target[]): any; | ||
} | ||
@@ -93,3 +95,3 @@ declare class SocketContext<UpgradeShape = any, ParamsShape = Record<string, any>> { | ||
httpRouter: HttpRouter; | ||
pathMatcher: PathMatcher<BunshineHandlers<any>>; | ||
routeMatcher: RouteMatcher<BunshineHandlers<any>>; | ||
handlers: BunHandlers; | ||
@@ -107,6 +109,2 @@ constructor(router: HttpRouter); | ||
export type ErrorHandler<ParamsShape extends Record<string, string> = Record<string, string>> = SingleErrorHandler<ParamsShape> | ErrorHandler<ParamsShape>[]; | ||
export type RouteInfo = { | ||
verb: string; | ||
handler: Handler<any>; | ||
}; | ||
export type ListenOptions = Omit<ServeOptions, "fetch" | "websocket"> | number; | ||
@@ -126,3 +124,3 @@ export type HttpMethods = "ALL" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE"; | ||
server: Server | undefined; | ||
pathMatcher: MatcherWithCache<RouteInfo>; | ||
routeMatcher: MatcherWithCache<SingleHandler>; | ||
_wsRouter?: SocketRouter; | ||
@@ -129,0 +127,0 @@ private _onErrors; |
{ | ||
"name": "bunshine", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"module": "server/server.ts", | ||
@@ -15,2 +15,3 @@ "type": "module", | ||
}, | ||
"sideEffects": false, | ||
"repository": { | ||
@@ -48,4 +49,3 @@ "type": "git", | ||
"dependencies": { | ||
"lru-cache": "10.2.2", | ||
"path-to-regexp": "6.2.2" | ||
"lru-cache": "11.0.1" | ||
}, | ||
@@ -55,11 +55,12 @@ "devDependencies": { | ||
"@types/ms": "0.7.34", | ||
"bun-types": "1.1.7", | ||
"bun-types": "1.1.29", | ||
"eventsource": "2.0.2", | ||
"globby": "14.0.1", | ||
"prettier": "3.2.5", | ||
"prettier-plugin-organize-imports": "3.2.4", | ||
"tinybench": "2.8.0", | ||
"type-fest": "4.18.2", | ||
"typescript": "5.4.5" | ||
"globby": "14.0.2", | ||
"prettier": "3.3.3", | ||
"prettier-plugin-organize-imports": "4.1.0", | ||
"redos-detector": "5.1.0", | ||
"tinybench": "2.9.0", | ||
"type-fest": "4.26.1", | ||
"typescript": "5.6.2" | ||
} | ||
} |
@@ -5,7 +5,11 @@ # Bunshine | ||
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=1.0.1" width="200" height="187" /> | ||
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=2.0.0" width="200" height="187" /> | ||
[](https://npmjs.com/package/bunshine) | ||
 | ||
[](https://opensource.org/licenses/ISC) | ||
[](https://npmjs.com/package/bunshine) | ||
[](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code) | ||
 | ||
[](https://bundlephobia.com/package/bunshine@2.0.0) | ||
[](https://www.npmjs.com/package/bunshine?activeTab=dependencies) | ||
[](https://www.npmjs.com/package/bunshine) | ||
[](https://opensource.org/licenses/ISC) | ||
@@ -46,3 +50,3 @@ ## Installation | ||
8. [Server Sent Events](#server-sent-events) | ||
9. [Routing examples](#routing-examples) | ||
9. [Route Matching](#route-matching) | ||
10. [Included middleware](#included-middleware) | ||
@@ -577,25 +581,63 @@ - [serveFiles](#servefiles) | ||
## Routing examples | ||
## Route Matching | ||
Bunshine uses the `path-to-regexp` package for processing path routes. For more | ||
info, checkout the [path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp). | ||
Bunshine v1 used the `path-to-regexp` package for processing path routes. | ||
Due to a discovered | ||
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106), | ||
Bunshine no longer uses | ||
[path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp). | ||
### Support | ||
Bunshine supports the following route matching features: | ||
- Named placeholders using colons (e.g. `/posts/:id`) | ||
- End wildcards using stars (e.g. `/assets/*`) | ||
- Middle non-slash wildcards using stars (e.g. `/assets/*/*.css`) | ||
- Static paths (e.g. `/posts`) | ||
- Custom Regular Expression (e.g. `/^\/author\/([a-z]+)$/i`) | ||
Support for other behaviors can lead to a Regular Expression Denial of service | ||
vulnerability where an attacker can request long URLs and tie up your server | ||
CPU with backtracking regular expression searches. | ||
### Path examples | ||
| Path | URL | params | | ||
| ---------------------- | --------------------- | ------------------------ | | ||
| `'/path'` | `'/path'` | `{}` | | ||
| `'/users/:id'` | `'/users/123'` | `{ id: '123' }` | | ||
| `'/users/:id/groups'` | `'/users/123/groups'` | `{ id: '123' }` | | ||
| `'/u/:id/groups/:gid'` | `'/u/1/groups/a'` | `{ id: '1', gid: 'a' }` | | ||
| `'/star/*'` | `'/star/man'` | `{ 0: 'man' }` | | ||
| `'/star/*/can'` | `'/star/man/can'` | `{ 0: 'man' }` | | ||
| `'/users/(\\d+)'` | `'/users/123'` | `{ 0: '123' }` | | ||
| `/users/(\d+)/` | `'/users/123'` | `{ 0: '123' }` | | ||
| `/users/([a-z-]+)/` | `'/users/abc-def'` | `{ 0: 'abc-def' }` | | ||
| `'/(users\|u)/:id'` | `'/users/123'` | `{ id: '123' }` | | ||
| `'/(users\|u)/:id'` | `'/u/123'` | `{ id: '123' }` | | ||
| `'/:a/:b?'` | `'/123'` | `{ a: '123' }` | | ||
| `'/:a/:b?'` | `'/123/abc'` | `{ a: '123', b: 'abc' }` | | ||
| Path | URL | params | | ||
| -------------------- | ------------------- | ----------------------- | | ||
| `/path` | `/path` | `{}` | | ||
| `/users/:id` | `/users/123` | `{ id: '123' }` | | ||
| `/users/:id/groups` | `/users/123/groups` | `{ id: '123' }` | | ||
| `/u/:id/groups/:gid` | `/u/1/groups/a` | `{ id: '1', gid: 'a' }` | | ||
| `/star/*` | `/star/man` | `{ 0: 'man' }` | | ||
| `/star/*` | `/star/man/can` | `{ 0: 'man/can' }` | | ||
| `/star/*/can` | `/star/man/can` | `{ 0: 'man' }` | | ||
| `/star/*/can/*` | `/star/man/can/go` | `{ 0: 'man', 1: 'go' }` | | ||
### Special Characters | ||
Note that all regular-expression special characters including | ||
`\ ^ $ * + ? . ( ) | { } [ ]` will be escaped. If you need any of these | ||
behaviors, you'll need to pass in a `RegExp`. | ||
For example, the dot in `/assets/*.js` will not match all characters--only dots.™™ | ||
### Not supported | ||
Support for regex-like syntax has been dropped in v2 due to a | ||
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106). | ||
For cases where you need to limit by character or specify optional segments, | ||
you'll need to pass in a `RegExp`. Be sure to check your `RegExp` with a ReDoS | ||
checker such as [Devina](https://devina.io/redos-checker) or | ||
[redos-checker on npm](https://www.npmjs.com/package/redos-detector). | ||
| Example | Explaination | Equivalent RegExp | | ||
| ------------------- | ----------------------------------------- | ------------------------ | | ||
| `/users/([a-z-]+)/` | Character classes are not supported | `^\/users\/([a-z-]+)$` | | ||
| `/users/(\\d+)` | Character class escapes are not supported | `^/\/users\/(\d+)$` | | ||
| `/(users\|u)/:id` | Pipes are not supported | `^\/(users\|u)/([^/]+)$` | | ||
| `/:a/:b?` | Optional params are not supported | `^\/([^/]*)\/(.*)$` | | ||
### Caching | ||
### HTTP methods | ||
@@ -623,2 +665,5 @@ | ||
// regular expression matchers are supported | ||
app.get(/^\/author\/([a-z]+)$/i, getPost); | ||
app.listen({ port: 3100 }); | ||
@@ -785,3 +830,3 @@ ``` | ||
"runtime": "Bun v1.1.4", | ||
"poweredBy": "Bunshine v1.0.1", | ||
"poweredBy": "Bunshine v2.0.0", | ||
"machine": "server1", | ||
@@ -805,3 +850,3 @@ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", | ||
"runtime": "Bun v1.1.4", | ||
"poweredBy": "Bunshine v1.0.1", | ||
"poweredBy": "Bunshine v2.0.0", | ||
"machine": "server1", | ||
@@ -808,0 +853,0 @@ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", |
@@ -14,4 +14,4 @@ import type { BunFile, ZlibCompressionOptions } from 'bun'; | ||
export function gzipString(text: string, zlibOptions?: ZlibCompressionOptions) { | ||
const buffer = Buffer.from(textEncoder.encode(text)); | ||
const buffer = new Uint8Array(textEncoder.encode(text)); | ||
return Bun.gzipSync(buffer, zlibOptions); | ||
} |
@@ -6,3 +6,2 @@ import type { ServeOptions, Server } from 'bun'; | ||
import MatcherWithCache from '../MatcherWithCache/MatcherWithCache.ts'; | ||
import PathMatcher from '../PathMatcher/PathMatcher'; | ||
import SocketRouter from '../SocketRouter/SocketRouter.ts'; | ||
@@ -40,7 +39,2 @@ import { fallback404 } from './fallback404'; | ||
type RouteInfo = { | ||
verb: string; | ||
handler: Handler<any>; | ||
}; | ||
export type ListenOptions = Omit<ServeOptions, 'fetch' | 'websocket'> | number; | ||
@@ -59,18 +53,2 @@ | ||
const getPathMatchFilter = (verb: string) => (target: RouteInfo) => { | ||
return target.verb === verb || target.verb === 'ALL'; | ||
}; | ||
const filters = { | ||
ALL: () => true, | ||
GET: getPathMatchFilter('GET'), | ||
POST: getPathMatchFilter('POST'), | ||
PUT: getPathMatchFilter('PUT'), | ||
PATCH: getPathMatchFilter('PATCH'), | ||
DELETE: getPathMatchFilter('DELETE'), | ||
HEAD: getPathMatchFilter('HEAD'), | ||
OPTIONS: getPathMatchFilter('OPTIONS'), | ||
TRACE: getPathMatchFilter('TRACE'), | ||
}; | ||
export type HttpRouterOptions = { | ||
@@ -90,3 +68,3 @@ cacheSize?: number; | ||
server: Server | undefined; | ||
pathMatcher: MatcherWithCache<RouteInfo>; | ||
routeMatcher: MatcherWithCache<SingleHandler>; | ||
_wsRouter?: SocketRouter; | ||
@@ -96,4 +74,3 @@ private _onErrors: any[] = []; | ||
constructor(options: HttpRouterOptions = {}) { | ||
this.pathMatcher = new MatcherWithCache<RouteInfo>( | ||
new PathMatcher(), | ||
this.routeMatcher = new MatcherWithCache<SingleHandler>( | ||
options.cacheSize || 4000 | ||
@@ -168,6 +145,3 @@ ); | ||
for (const handler of handlers.flat(9)) { | ||
this.pathMatcher.add(path, { | ||
verb: verbOrVerbs as string, | ||
handler: handler as SingleHandler<ParamsShape>, | ||
}); | ||
this.routeMatcher.add(verbOrVerbs, path, handler as SingleHandler); | ||
} | ||
@@ -253,4 +227,3 @@ return this; | ||
).toUpperCase(); | ||
const filter = filters[method] || getPathMatchFilter(method); | ||
const matched = this.pathMatcher.match(pathname, filter, this._on404s); | ||
const matched = this.routeMatcher.match(method, pathname, this._on404s); | ||
let i = 0; | ||
@@ -262,4 +235,4 @@ const next: NextFunction = async () => { | ||
} | ||
context.params = match.params; | ||
const handler = match.target.handler as SingleHandler; | ||
const handler = match[0] as SingleHandler; | ||
context.params = match[1]; | ||
@@ -266,0 +239,0 @@ try { |
import { LRUCache } from 'lru-cache'; | ||
import type PathMatcher from '../PathMatcher/PathMatcher.ts'; | ||
import RouteMatcher from '../RouteMatcher/RouteMatcher.ts'; | ||
export default class MatcherWithCache<Target = any> { | ||
matcher: PathMatcher<Target>; | ||
export default class MatcherWithCache< | ||
Target = any, | ||
> extends RouteMatcher<Target> { | ||
cache: LRUCache<string, any>; | ||
constructor(matcher: PathMatcher<Target>, size: number = 5000) { | ||
this.matcher = matcher; | ||
constructor(size: number = 5000) { | ||
super(); | ||
this.cache = new LRUCache<string, any>({ max: size }); | ||
} | ||
add(path: string | RegExp, target: Target) { | ||
this.matcher.add(path, target); | ||
} | ||
match( | ||
path: string, | ||
filter?: (target: Target) => boolean, | ||
fallbacks?: Function[] | ||
) { | ||
if (this.cache.has(path)) { | ||
return this.cache.get(path); | ||
match(method: string, subject: string, fallbacks?: Target[]) { | ||
const key = `${method}:${subject}`; | ||
if (this.cache.has(key)) { | ||
return this.cache.get(key); | ||
} | ||
const result = this.matcher.match(path, filter, fallbacks); | ||
this.cache.set(path, result); | ||
const result = super.match(method, subject, fallbacks); | ||
this.cache.set(key, result); | ||
return result; | ||
} | ||
} |
@@ -53,3 +53,3 @@ import { Server, ServerWebSocket, ServerWebSocketSendStatus } from 'bun'; | ||
} else if (message instanceof Buffer) { | ||
return this.ws!.sendBinary(message, compress); | ||
return this.ws!.sendBinary(new Uint8Array(message.buffer), compress); | ||
} else if (isBufferSource(message)) { | ||
@@ -99,7 +99,3 @@ return this.ws!.send(message, compress); | ||
ping(data?: string | Bun.BufferSource): ServerWebSocketSendStatus { | ||
if ( | ||
typeof data === 'string' || | ||
data instanceof Buffer || | ||
isBufferSource(data) | ||
) { | ||
if (typeof data === 'string' || isBufferSource(data)) { | ||
return this.ws!.ping(data); | ||
@@ -111,7 +107,3 @@ } else { | ||
pong(data?: string | Bun.BufferSource): ServerWebSocketSendStatus { | ||
if ( | ||
typeof data === 'string' || | ||
data instanceof Buffer || | ||
isBufferSource(data) | ||
) { | ||
if (typeof data === 'string' || isBufferSource(data)) { | ||
return this.ws!.pong(data); | ||
@@ -118,0 +110,0 @@ } else { |
@@ -5,3 +5,3 @@ import type { ServerWebSocket } from 'bun'; | ||
import HttpRouter, { NextFunction } from '../HttpRouter/HttpRouter'; | ||
import PathMatcher from '../PathMatcher/PathMatcher'; | ||
import RouteMatcher from '../RouteMatcher/RouteMatcher'; | ||
import SocketContext, { SocketMessage } from './SocketContext.ts'; | ||
@@ -77,3 +77,3 @@ | ||
httpRouter: HttpRouter; | ||
pathMatcher: PathMatcher<BunshineHandlers<any>>; | ||
routeMatcher: RouteMatcher<BunshineHandlers<any>>; | ||
handlers: BunHandlers; | ||
@@ -83,3 +83,3 @@ constructor(router: HttpRouter) { | ||
this.httpRouter._wsRouter = this; | ||
this.pathMatcher = new PathMatcher<BunshineHandlers<any>>(); | ||
this.routeMatcher = new RouteMatcher<BunshineHandlers<any>>(); | ||
this.handlers = { | ||
@@ -104,4 +104,4 @@ open: this._createHandler('open'), | ||
// capture the matcher details | ||
// @ts-expect-error | ||
this.pathMatcher.add(path, handlers); | ||
// @ts-expect-error Handlers are more specific than any | ||
this.routeMatcher.add('ALL', path, handlers); | ||
// console.log('ws handlers registered!', path); | ||
@@ -126,3 +126,2 @@ // create a router path that upgrades to a socket | ||
const error = e as Error; | ||
console.error('WebSocket upgrade error', error); | ||
return c.text('Internal server error', { | ||
@@ -132,5 +131,2 @@ status: 500, | ||
} | ||
console.error( | ||
'WebSocket upgrade failed: Client does not support WebSocket' | ||
); | ||
return c.text('Client does not support WebSocket', { | ||
@@ -154,4 +150,4 @@ status: 426, // 426 Upgrade Required | ||
const pathname = sc.url.pathname; | ||
const matched = this.pathMatcher.match(pathname); | ||
const rest: any = []; | ||
const matched = this.routeMatcher.match('', pathname); | ||
const rest: any[] = []; | ||
if (['message', 'ping', 'pong'].includes(eventName)) { | ||
@@ -162,3 +158,3 @@ rest.push(new SocketMessage(eventName, args[0])); | ||
} | ||
for (const { target } of matched) { | ||
for (const [target] of matched) { | ||
if (!target[eventName]) { | ||
@@ -168,3 +164,3 @@ continue; | ||
try { | ||
target[eventName](sc, ...rest); | ||
target[eventName](sc, rest[0], rest[1]); | ||
} catch (e) { | ||
@@ -171,0 +167,0 @@ const handlerError = e as Error; |
{ | ||
"include": ["index.ts", "package.json", "src/**/*", "examples", "benchmarks", "bin/**/*"], | ||
"exclude": [], | ||
"exclude": [ | ||
"**/*.spec.ts" | ||
], | ||
"compilerOptions": { | ||
@@ -5,0 +7,0 @@ "lib": ["ESNext", "dom", "dom.iterable"], |
Sorry, the diff of this file is not supported yet
143278
1
2906
1085
11
+ Addedlru-cache@11.0.1(transitive)
- Removedpath-to-regexp@6.2.2
- Removedlru-cache@10.2.2(transitive)
- Removedpath-to-regexp@6.2.2(transitive)
Updatedlru-cache@11.0.1