@stacksjs/bun-router 0.0.4 → 0.0.6
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 +2 -2
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/chunk-jhsk5c5b.js +15758 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -2
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +320 -206
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +245 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +113 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +276 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1776 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +18 -4
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +678 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +219 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +264 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +612 -0
- package/src/response/response-factory.ts +588 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +670 -0
- package/src/router/file-streaming.ts +372 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +185 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +212 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1508 -0
- package/src/router/server.ts +406 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2049 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
- package/dist/chunk-cjdekj87.js +0 -25886
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static File Serving
|
|
3
|
+
*
|
|
4
|
+
* Efficient file serving using Bun.file() with advanced caching and optimization
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface StaticFileConfig {
|
|
8
|
+
root: string
|
|
9
|
+
maxAge?: number
|
|
10
|
+
immutable?: boolean
|
|
11
|
+
etag?: boolean
|
|
12
|
+
lastModified?: boolean
|
|
13
|
+
index?: string[]
|
|
14
|
+
extensions?: string[]
|
|
15
|
+
fallthrough?: boolean
|
|
16
|
+
setHeaders?: (res: Response, path: string, stat: any) => void
|
|
17
|
+
compression?: boolean
|
|
18
|
+
compressionThreshold?: number
|
|
19
|
+
cacheControl?: string
|
|
20
|
+
dotfiles?: 'allow' | 'deny' | 'ignore'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FileCache {
|
|
24
|
+
content: ArrayBuffer
|
|
25
|
+
etag: string
|
|
26
|
+
lastModified: string
|
|
27
|
+
mimeType: string
|
|
28
|
+
size: number
|
|
29
|
+
compressed?: ArrayBuffer
|
|
30
|
+
compressedEtag?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FileStats {
|
|
34
|
+
hits: number
|
|
35
|
+
misses: number
|
|
36
|
+
bytesServed: number
|
|
37
|
+
compressionRatio: number
|
|
38
|
+
averageResponseTime: number
|
|
39
|
+
lastAccessed: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* High-performance static file server using Bun.file()
|
|
44
|
+
*/
|
|
45
|
+
export class StaticFileServer {
|
|
46
|
+
private config: Required<StaticFileConfig>
|
|
47
|
+
private cache = new Map<string, FileCache>()
|
|
48
|
+
private stats = new Map<string, FileStats>()
|
|
49
|
+
private compressionCache = new Map<string, ArrayBuffer>()
|
|
50
|
+
|
|
51
|
+
constructor(config: StaticFileConfig) {
|
|
52
|
+
this.config = {
|
|
53
|
+
maxAge: config.maxAge ?? 86400, // 1 day
|
|
54
|
+
immutable: config.immutable ?? false,
|
|
55
|
+
etag: config.etag ?? true,
|
|
56
|
+
lastModified: config.lastModified ?? true,
|
|
57
|
+
index: config.index ?? ['index.html', 'index.htm'],
|
|
58
|
+
extensions: config.extensions ?? [],
|
|
59
|
+
fallthrough: config.fallthrough ?? true,
|
|
60
|
+
setHeaders: config.setHeaders ?? (() => {}),
|
|
61
|
+
compression: config.compression ?? true,
|
|
62
|
+
compressionThreshold: config.compressionThreshold ?? 1024,
|
|
63
|
+
cacheControl: config.cacheControl ?? '',
|
|
64
|
+
dotfiles: config.dotfiles ?? 'ignore',
|
|
65
|
+
...config,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Serve static file
|
|
71
|
+
*/
|
|
72
|
+
async serve(request: Request): Promise<Response | null> {
|
|
73
|
+
const url = new URL(request.url)
|
|
74
|
+
const pathname = decodeURIComponent(url.pathname)
|
|
75
|
+
|
|
76
|
+
// Security: prevent directory traversal
|
|
77
|
+
if (pathname.includes('..') || pathname.includes('\0')) {
|
|
78
|
+
return new Response('Forbidden', { status: 403 })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle dotfiles
|
|
82
|
+
if (this.config.dotfiles !== 'allow' && pathname.includes('/.')) {
|
|
83
|
+
if (this.config.dotfiles === 'deny') {
|
|
84
|
+
return new Response('Forbidden', { status: 403 })
|
|
85
|
+
}
|
|
86
|
+
if (this.config.fallthrough) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try to serve file
|
|
92
|
+
const filePath = await this.resolveFile(pathname)
|
|
93
|
+
if (!filePath) {
|
|
94
|
+
return this.config.fallthrough ? null : new Response('Not Found', { status: 404 })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return await this.serveFile(request, filePath, pathname)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Serve specific file
|
|
102
|
+
*/
|
|
103
|
+
async serveFile(request: Request, filePath: string, requestPath: string): Promise<Response> {
|
|
104
|
+
const startTime = performance.now()
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Check cache first
|
|
108
|
+
const cached = this.cache.get(filePath)
|
|
109
|
+
const file = Bun.file(filePath)
|
|
110
|
+
|
|
111
|
+
// Check if file exists and get stats
|
|
112
|
+
const exists = await file.exists()
|
|
113
|
+
if (!exists) {
|
|
114
|
+
return new Response('Not Found', { status: 404 })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lastModified = new Date(file.lastModified).toUTCString()
|
|
118
|
+
const size = file.size
|
|
119
|
+
|
|
120
|
+
// Check if cached version is still valid
|
|
121
|
+
if (cached && cached.lastModified === lastModified) {
|
|
122
|
+
return this.serveCachedFile(request, cached, requestPath, startTime)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Read and cache file
|
|
126
|
+
const content = await file.arrayBuffer()
|
|
127
|
+
const mimeType = file.type || this.getMimeType(filePath)
|
|
128
|
+
const etag = this.generateEtag(content, lastModified)
|
|
129
|
+
|
|
130
|
+
const fileCache: FileCache = {
|
|
131
|
+
content,
|
|
132
|
+
etag,
|
|
133
|
+
lastModified,
|
|
134
|
+
mimeType,
|
|
135
|
+
size,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compress if enabled and file is large enough
|
|
139
|
+
if (this.config.compression && size >= this.config.compressionThreshold) {
|
|
140
|
+
const compressed = await this.compressContent(content)
|
|
141
|
+
if (compressed && compressed.byteLength < content.byteLength) {
|
|
142
|
+
fileCache.compressed = compressed
|
|
143
|
+
fileCache.compressedEtag = this.generateEtag(compressed, lastModified)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.cache.set(filePath, fileCache)
|
|
148
|
+
return this.serveCachedFile(request, fileCache, requestPath, startTime)
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error('Error serving file:', error)
|
|
152
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Serve cached file with proper headers
|
|
158
|
+
*/
|
|
159
|
+
private serveCachedFile(
|
|
160
|
+
request: Request,
|
|
161
|
+
cached: FileCache,
|
|
162
|
+
requestPath: string,
|
|
163
|
+
startTime: number,
|
|
164
|
+
): Response {
|
|
165
|
+
const headers = new Headers()
|
|
166
|
+
|
|
167
|
+
// Check if client supports compression
|
|
168
|
+
const acceptEncoding = request.headers.get('accept-encoding') || ''
|
|
169
|
+
const supportsGzip = acceptEncoding.includes('gzip')
|
|
170
|
+
const useCompressed = supportsGzip && cached.compressed
|
|
171
|
+
|
|
172
|
+
// Set content type
|
|
173
|
+
headers.set('Content-Type', cached.mimeType)
|
|
174
|
+
|
|
175
|
+
// Set ETag
|
|
176
|
+
if (this.config.etag) {
|
|
177
|
+
const etag = useCompressed ? cached.compressedEtag! : cached.etag
|
|
178
|
+
headers.set('ETag', etag)
|
|
179
|
+
|
|
180
|
+
// Check if client has cached version
|
|
181
|
+
const ifNoneMatch = request.headers.get('if-none-match')
|
|
182
|
+
if (ifNoneMatch === etag) {
|
|
183
|
+
this.updateStats(requestPath, 0, startTime, true)
|
|
184
|
+
return new Response(null, { status: 304, headers })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Set Last-Modified
|
|
189
|
+
if (this.config.lastModified) {
|
|
190
|
+
headers.set('Last-Modified', cached.lastModified)
|
|
191
|
+
|
|
192
|
+
// Check if client has cached version
|
|
193
|
+
const ifModifiedSince = request.headers.get('if-modified-since')
|
|
194
|
+
if (ifModifiedSince === cached.lastModified) {
|
|
195
|
+
this.updateStats(requestPath, 0, startTime, true)
|
|
196
|
+
return new Response(null, { status: 304, headers })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Set cache control
|
|
201
|
+
if (this.config.cacheControl) {
|
|
202
|
+
headers.set('Cache-Control', this.config.cacheControl)
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const cacheControl = this.config.immutable
|
|
206
|
+
? `public, max-age=${this.config.maxAge}, immutable`
|
|
207
|
+
: `public, max-age=${this.config.maxAge}`
|
|
208
|
+
headers.set('Cache-Control', cacheControl)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Set compression headers
|
|
212
|
+
const content = useCompressed ? cached.compressed! : cached.content
|
|
213
|
+
if (useCompressed) {
|
|
214
|
+
headers.set('Content-Encoding', 'gzip')
|
|
215
|
+
headers.set('Vary', 'Accept-Encoding')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
headers.set('Content-Length', content.byteLength.toString())
|
|
219
|
+
|
|
220
|
+
// Custom headers
|
|
221
|
+
this.config.setHeaders(new Response(), requestPath, { size: cached.size })
|
|
222
|
+
|
|
223
|
+
// Update stats
|
|
224
|
+
this.updateStats(requestPath, content.byteLength, startTime, false)
|
|
225
|
+
|
|
226
|
+
return new Response(content, { headers })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve file path, handling index files and extensions
|
|
231
|
+
*/
|
|
232
|
+
private async resolveFile(pathname: string): Promise<string | null> {
|
|
233
|
+
const fullPath = this.joinPath(this.config.root, pathname)
|
|
234
|
+
|
|
235
|
+
// Try exact path first
|
|
236
|
+
const file = Bun.file(fullPath)
|
|
237
|
+
if (await file.exists()) {
|
|
238
|
+
const stat = await file.stat()
|
|
239
|
+
if (stat.isFile()) {
|
|
240
|
+
return fullPath
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If directory, try index files
|
|
244
|
+
if (stat.isDirectory()) {
|
|
245
|
+
for (const index of this.config.index) {
|
|
246
|
+
const indexPath = this.joinPath(fullPath, index)
|
|
247
|
+
const indexFile = Bun.file(indexPath)
|
|
248
|
+
if (await indexFile.exists()) {
|
|
249
|
+
return indexPath
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Try with extensions
|
|
256
|
+
for (const ext of this.config.extensions) {
|
|
257
|
+
const extPath = fullPath + ext
|
|
258
|
+
const extFile = Bun.file(extPath)
|
|
259
|
+
if (await extFile.exists()) {
|
|
260
|
+
return extPath
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Join paths safely
|
|
269
|
+
*/
|
|
270
|
+
private joinPath(...parts: string[]): string {
|
|
271
|
+
return parts
|
|
272
|
+
.map(part => part.replace(/^\/+|\/+$/g, ''))
|
|
273
|
+
.filter(part => part.length > 0)
|
|
274
|
+
.join('/')
|
|
275
|
+
.replace(/^(?!\/)/, '/')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Generate ETag for content
|
|
280
|
+
*/
|
|
281
|
+
private generateEtag(content: ArrayBuffer, lastModified: string): string {
|
|
282
|
+
const hash = Bun.hash(new Uint8Array(content))
|
|
283
|
+
return `"${hash.toString(16)}-${Date.parse(lastModified).toString(16)}"`
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compress content using gzip
|
|
288
|
+
*/
|
|
289
|
+
private async compressContent(content: ArrayBuffer): Promise<ArrayBuffer | null> {
|
|
290
|
+
try {
|
|
291
|
+
const compressed = Bun.gzipSync(new Uint8Array(content))
|
|
292
|
+
return compressed.buffer
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
console.error('Compression failed:', error)
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get MIME type for file
|
|
302
|
+
*/
|
|
303
|
+
private getMimeType(filePath: string): string {
|
|
304
|
+
const ext = filePath.split('.').pop()?.toLowerCase()
|
|
305
|
+
|
|
306
|
+
const mimeTypes: Record<string, string> = {
|
|
307
|
+
html: 'text/html',
|
|
308
|
+
htm: 'text/html',
|
|
309
|
+
css: 'text/css',
|
|
310
|
+
js: 'application/javascript',
|
|
311
|
+
mjs: 'application/javascript',
|
|
312
|
+
json: 'application/json',
|
|
313
|
+
png: 'image/png',
|
|
314
|
+
jpg: 'image/jpeg',
|
|
315
|
+
jpeg: 'image/jpeg',
|
|
316
|
+
gif: 'image/gif',
|
|
317
|
+
svg: 'image/svg+xml',
|
|
318
|
+
ico: 'image/x-icon',
|
|
319
|
+
woff: 'font/woff',
|
|
320
|
+
woff2: 'font/woff2',
|
|
321
|
+
ttf: 'font/ttf',
|
|
322
|
+
eot: 'application/vnd.ms-fontobject',
|
|
323
|
+
pdf: 'application/pdf',
|
|
324
|
+
txt: 'text/plain',
|
|
325
|
+
xml: 'application/xml',
|
|
326
|
+
zip: 'application/zip',
|
|
327
|
+
mp4: 'video/mp4',
|
|
328
|
+
webm: 'video/webm',
|
|
329
|
+
mp3: 'audio/mpeg',
|
|
330
|
+
wav: 'audio/wav',
|
|
331
|
+
ogg: 'audio/ogg',
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return mimeTypes[ext || ''] || 'application/octet-stream'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Update file serving statistics
|
|
339
|
+
*/
|
|
340
|
+
private updateStats(path: string, bytes: number, startTime: number, cached: boolean): void {
|
|
341
|
+
const responseTime = performance.now() - startTime
|
|
342
|
+
|
|
343
|
+
let stats = this.stats.get(path)
|
|
344
|
+
if (!stats) {
|
|
345
|
+
stats = {
|
|
346
|
+
hits: 0,
|
|
347
|
+
misses: 0,
|
|
348
|
+
bytesServed: 0,
|
|
349
|
+
compressionRatio: 0,
|
|
350
|
+
averageResponseTime: 0,
|
|
351
|
+
lastAccessed: 0,
|
|
352
|
+
}
|
|
353
|
+
this.stats.set(path, stats)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (cached) {
|
|
357
|
+
stats.hits++
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
stats.misses++
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
stats.bytesServed += bytes
|
|
364
|
+
stats.averageResponseTime = (stats.averageResponseTime * (stats.hits + stats.misses - 1) + responseTime) / (stats.hits + stats.misses)
|
|
365
|
+
stats.lastAccessed = Date.now()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get serving statistics
|
|
370
|
+
*/
|
|
371
|
+
getStats(): Record<string, FileStats> {
|
|
372
|
+
return Object.fromEntries(this.stats.entries())
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Clear cache
|
|
377
|
+
*/
|
|
378
|
+
clearCache(): void {
|
|
379
|
+
this.cache.clear()
|
|
380
|
+
this.compressionCache.clear()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get cache size
|
|
385
|
+
*/
|
|
386
|
+
getCacheSize(): number {
|
|
387
|
+
let size = 0
|
|
388
|
+
for (const cached of this.cache.values()) {
|
|
389
|
+
size += cached.content.byteLength
|
|
390
|
+
if (cached.compressed) {
|
|
391
|
+
size += cached.compressed.byteLength
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return size
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Preload files into cache
|
|
399
|
+
*/
|
|
400
|
+
async preloadFiles(paths: string[]): Promise<void> {
|
|
401
|
+
const promises = paths.map(async (path) => {
|
|
402
|
+
const filePath = await this.resolveFile(path)
|
|
403
|
+
if (filePath) {
|
|
404
|
+
const mockRequest = new Request(`http://localhost${path}`)
|
|
405
|
+
await this.serveFile(mockRequest, filePath, path)
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
await Promise.all(promises)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Static file middleware factory
|
|
415
|
+
*/
|
|
416
|
+
export function createStaticFileMiddleware(config: StaticFileConfig) {
|
|
417
|
+
const server = new StaticFileServer(config)
|
|
418
|
+
|
|
419
|
+
return async (request: Request, next: () => Promise<Response>): Promise<Response> => {
|
|
420
|
+
// Only handle GET and HEAD requests
|
|
421
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
422
|
+
return await next()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const response = await server.serve(request)
|
|
426
|
+
|
|
427
|
+
if (response) {
|
|
428
|
+
// Handle HEAD requests
|
|
429
|
+
if (request.method === 'HEAD') {
|
|
430
|
+
return new Response(null, {
|
|
431
|
+
status: response.status,
|
|
432
|
+
headers: response.headers,
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
return response
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return await next()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Static file serving helpers
|
|
444
|
+
*/
|
|
445
|
+
export const StaticFileHelpers = {
|
|
446
|
+
/**
|
|
447
|
+
* Create development server with hot reload
|
|
448
|
+
*/
|
|
449
|
+
createDevelopmentServer: (config: StaticFileConfig): StaticFileServer => {
|
|
450
|
+
return new StaticFileServer({
|
|
451
|
+
...config,
|
|
452
|
+
maxAge: 0,
|
|
453
|
+
etag: false,
|
|
454
|
+
lastModified: true,
|
|
455
|
+
compression: false,
|
|
456
|
+
})
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create production server with aggressive caching
|
|
461
|
+
*/
|
|
462
|
+
createProductionServer: (config: StaticFileConfig): StaticFileServer => {
|
|
463
|
+
return new StaticFileServer({
|
|
464
|
+
...config,
|
|
465
|
+
maxAge: 31536000, // 1 year
|
|
466
|
+
immutable: true,
|
|
467
|
+
etag: true,
|
|
468
|
+
lastModified: true,
|
|
469
|
+
compression: true,
|
|
470
|
+
compressionThreshold: 512,
|
|
471
|
+
})
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create SPA server with fallback to index.html
|
|
476
|
+
*/
|
|
477
|
+
createSPAServer: (config: StaticFileConfig): StaticFileServer => {
|
|
478
|
+
return new StaticFileServer({
|
|
479
|
+
...config,
|
|
480
|
+
fallthrough: false,
|
|
481
|
+
index: ['index.html'],
|
|
482
|
+
setHeaders: (res, path) => {
|
|
483
|
+
// Don't cache HTML files in SPA mode
|
|
484
|
+
if (path.endsWith('.html')) {
|
|
485
|
+
res.headers.set('Cache-Control', 'no-cache')
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Create CDN-optimized server
|
|
493
|
+
*/
|
|
494
|
+
createCDNServer: (config: StaticFileConfig): StaticFileServer => {
|
|
495
|
+
return new StaticFileServer({
|
|
496
|
+
...config,
|
|
497
|
+
maxAge: 86400 * 30, // 30 days
|
|
498
|
+
immutable: false,
|
|
499
|
+
etag: true,
|
|
500
|
+
lastModified: true,
|
|
501
|
+
compression: true,
|
|
502
|
+
compressionThreshold: 256,
|
|
503
|
+
cacheControl: 'public, max-age=2592000, s-maxage=31536000',
|
|
504
|
+
})
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* File serving utilities
|
|
510
|
+
*/
|
|
511
|
+
export const FileServingUtils = {
|
|
512
|
+
/**
|
|
513
|
+
* Calculate optimal cache settings based on file type
|
|
514
|
+
*/
|
|
515
|
+
getOptimalCacheSettings: (filePath: string): Partial<StaticFileConfig> => {
|
|
516
|
+
const ext = filePath.split('.').pop()?.toLowerCase()
|
|
517
|
+
|
|
518
|
+
// Long-term cacheable assets
|
|
519
|
+
if (['css', 'js', 'woff', 'woff2', 'ttf', 'eot'].includes(ext || '')) {
|
|
520
|
+
return {
|
|
521
|
+
maxAge: 31536000, // 1 year
|
|
522
|
+
immutable: true,
|
|
523
|
+
compression: true,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Images
|
|
528
|
+
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico'].includes(ext || '')) {
|
|
529
|
+
return {
|
|
530
|
+
maxAge: 86400 * 7, // 1 week
|
|
531
|
+
immutable: false,
|
|
532
|
+
compression: ext === 'svg',
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// HTML files
|
|
537
|
+
if (['html', 'htm'].includes(ext || '')) {
|
|
538
|
+
return {
|
|
539
|
+
maxAge: 3600, // 1 hour
|
|
540
|
+
immutable: false,
|
|
541
|
+
compression: true,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Default
|
|
546
|
+
return {
|
|
547
|
+
maxAge: 86400, // 1 day
|
|
548
|
+
immutable: false,
|
|
549
|
+
compression: true,
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Generate security headers for static files
|
|
555
|
+
*/
|
|
556
|
+
generateSecurityHeaders: (): Record<string, string> => {
|
|
557
|
+
return {
|
|
558
|
+
'X-Content-Type-Options': 'nosniff',
|
|
559
|
+
'X-Frame-Options': 'DENY',
|
|
560
|
+
'X-XSS-Protection': '1; mode=block',
|
|
561
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create file watcher for cache invalidation
|
|
567
|
+
*/
|
|
568
|
+
createFileWatcher: async (server: StaticFileServer, watchPath: string): Promise<{ close: () => void }> => {
|
|
569
|
+
const fs = await import('node:fs')
|
|
570
|
+
const watcher = fs.watch(watchPath, { recursive: true }, (eventType: string, filename: string | null) => {
|
|
571
|
+
if (eventType === 'change' || eventType === 'rename') {
|
|
572
|
+
server.clearCache()
|
|
573
|
+
console.warn(`File changed: ${filename || 'unknown'}, cache cleared`)
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
close: () => watcher.close(),
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { default as Auth } from './auth'
|
|
2
|
+
export * from './auth'
|
|
3
|
+
export * from './config'
|
|
4
|
+
export * as Errors from './errors'
|
|
5
|
+
export * from './middleware'
|
|
6
|
+
export { getCurrentRequest, request, runWithRequest, setCurrentRequest } from './request/context'
|
|
7
|
+
export * from './session'
|
|
8
|
+
export * from './response/response-factory'
|
|
9
|
+
export * from './router'
|
|
10
|
+
export { MiddlewareGroupRegistry, middlewareGroups } from './router/middleware-groups'
|
|
11
|
+
export * from './testing'
|
|
12
|
+
export * from './types'
|
|
13
|
+
export { clearNamedRoutes, getNamedRoutePath, getNamedRoutes, registerNamedRoute, url, type UrlOptions } from './url'
|
|
14
|
+
export * from './utils'
|