@stacksjs/bun-router 0.0.17 → 0.0.19

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