@stacksjs/bun-router 0.0.17 → 0.0.18

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 (146) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/container/service-provider.d.ts +3 -3
  3. package/dist/development/performance-profiler.d.ts +3 -3
  4. package/dist/development/route-debugger.d.ts +1 -1
  5. package/dist/development/route-inspector.d.ts +2 -2
  6. package/dist/observability/integration.d.ts +1 -1
  7. package/dist/response/macros.d.ts +1 -1
  8. package/dist/router/index.d.ts +7 -7
  9. package/dist/router/router.d.ts +0 -32
  10. package/dist/testing/performance-testing.d.ts +2 -2
  11. package/dist/testing/test-client.d.ts +1 -1
  12. package/dist/testing/websocket-testing.d.ts +1 -1
  13. package/package.json +2 -3
  14. package/src/auth.ts +0 -489
  15. package/src/cache/lru-cache.ts +0 -457
  16. package/src/cache/middleware-memoization.ts +0 -531
  17. package/src/cache/route-cache-warmer.ts +0 -486
  18. package/src/cache/sqlite-cache.ts +0 -783
  19. package/src/cache/streaming-cache.ts +0 -572
  20. package/src/cli/colors.ts +0 -29
  21. package/src/cli/index.ts +0 -287
  22. package/src/cli/middleware.ts +0 -291
  23. package/src/cli/openapi.ts +0 -407
  24. package/src/cli/router.ts +0 -188
  25. package/src/cli/routes.ts +0 -265
  26. package/src/cli/utils.ts +0 -531
  27. package/src/cli.ts +0 -5
  28. package/src/config.ts +0 -322
  29. package/src/container/container.ts +0 -786
  30. package/src/container/contextual-binding.ts +0 -606
  31. package/src/container/decorators.ts +0 -368
  32. package/src/container/index.ts +0 -49
  33. package/src/container/service-provider.ts +0 -596
  34. package/src/development/hot-reload.ts +0 -673
  35. package/src/development/index.ts +0 -499
  36. package/src/development/performance-profiler.ts +0 -717
  37. package/src/development/route-debugger.ts +0 -527
  38. package/src/development/route-inspector.ts +0 -749
  39. package/src/development/typescript-utilities.ts +0 -682
  40. package/src/docs.ts +0 -397
  41. package/src/errors/circuit-breaker.ts +0 -733
  42. package/src/errors/error-handler.ts +0 -569
  43. package/src/errors/error-reporting.ts +0 -672
  44. package/src/errors/exceptions.ts +0 -536
  45. package/src/errors/graceful-degradation.ts +0 -623
  46. package/src/errors/index.ts +0 -21
  47. package/src/errors/router-errors.ts +0 -632
  48. package/src/file-serving/static-files.ts +0 -718
  49. package/src/index.ts +0 -66
  50. package/src/middleware/auth.ts +0 -220
  51. package/src/middleware/content_security_policy.ts +0 -215
  52. package/src/middleware/cors.ts +0 -142
  53. package/src/middleware/csrf.ts +0 -166
  54. package/src/middleware/ddos_protection.ts +0 -279
  55. package/src/middleware/file_security.ts +0 -191
  56. package/src/middleware/file_upload.ts +0 -275
  57. package/src/middleware/helmet.ts +0 -268
  58. package/src/middleware/index.ts +0 -117
  59. package/src/middleware/input_validation.ts +0 -453
  60. package/src/middleware/json_body.ts +0 -37
  61. package/src/middleware/performance_alerting.ts +0 -540
  62. package/src/middleware/performance_dashboard.ts +0 -661
  63. package/src/middleware/performance_monitor.ts +0 -945
  64. package/src/middleware/pipeline.ts +0 -489
  65. package/src/middleware/rate_limit.ts +0 -251
  66. package/src/middleware/request_id.ts +0 -36
  67. package/src/middleware/request_signing.ts +0 -638
  68. package/src/middleware/request_tracer.ts +0 -653
  69. package/src/middleware/response_cache.ts +0 -778
  70. package/src/middleware/security.ts +0 -482
  71. package/src/middleware/security_suite.ts +0 -257
  72. package/src/middleware/session.ts +0 -91
  73. package/src/model-binding/index.ts +0 -18
  74. package/src/model-binding/model-middleware.ts +0 -425
  75. package/src/model-binding/model-registry.ts +0 -550
  76. package/src/model-binding.ts +0 -370
  77. package/src/model-resolver-factory.ts +0 -106
  78. package/src/observability/correlation.ts +0 -691
  79. package/src/observability/health-checks.ts +0 -729
  80. package/src/observability/index.ts +0 -184
  81. package/src/observability/integration.ts +0 -548
  82. package/src/observability/metrics.ts +0 -753
  83. package/src/observability/tracing.ts +0 -638
  84. package/src/optimization/bun-utilities.ts +0 -778
  85. package/src/query-builder-integration.ts +0 -137
  86. package/src/request/context.ts +0 -109
  87. package/src/request/enhanced-request.ts +0 -876
  88. package/src/request/macros.ts +0 -822
  89. package/src/response/macros.ts +0 -665
  90. package/src/response/response-factory.ts +0 -596
  91. package/src/router/api-routes.ts +0 -243
  92. package/src/router/file-based-routing.ts +0 -697
  93. package/src/router/file-streaming.ts +0 -383
  94. package/src/router/fluent-routing.ts +0 -866
  95. package/src/router/group-organization.ts +0 -229
  96. package/src/router/handler-resolver.ts +0 -327
  97. package/src/router/http-methods.ts +0 -407
  98. package/src/router/index.ts +0 -193
  99. package/src/router/middleware-groups.ts +0 -222
  100. package/src/router/middleware-integration.ts +0 -399
  101. package/src/router/middleware.ts +0 -234
  102. package/src/router/model-binding.ts +0 -215
  103. package/src/router/optimized-route-matching.ts +0 -302
  104. package/src/router/route-building.ts +0 -221
  105. package/src/router/route-compiler.ts +0 -691
  106. package/src/router/route-matching.ts +0 -368
  107. package/src/router/route-trie.ts +0 -450
  108. package/src/router/router.ts +0 -1616
  109. package/src/router/server.ts +0 -684
  110. package/src/router/validation-integration.ts +0 -449
  111. package/src/router/view-rendering.ts +0 -233
  112. package/src/router/websocket.ts +0 -100
  113. package/src/routing/route-caching.ts +0 -402
  114. package/src/routing/route-throttling.ts +0 -469
  115. package/src/routing/subdomain-routing.ts +0 -492
  116. package/src/session/database-store.ts +0 -109
  117. package/src/session/file-store.ts +0 -148
  118. package/src/session/index.ts +0 -244
  119. package/src/session/memory-store.ts +0 -88
  120. package/src/session/redis-store.ts +0 -93
  121. package/src/streaming/index.ts +0 -17
  122. package/src/streaming/sse-handler.ts +0 -482
  123. package/src/streaming/stream-handler.ts +0 -552
  124. package/src/testing/auth-testing.ts +0 -448
  125. package/src/testing/file-upload-testing.ts +0 -543
  126. package/src/testing/index.ts +0 -10
  127. package/src/testing/middleware-testing.ts +0 -322
  128. package/src/testing/model-binding-testing.ts +0 -645
  129. package/src/testing/performance-testing.ts +0 -738
  130. package/src/testing/test-client.ts +0 -310
  131. package/src/testing/test-request.ts +0 -315
  132. package/src/testing/test-response.ts +0 -332
  133. package/src/testing/types.ts +0 -224
  134. package/src/testing/websocket-testing.ts +0 -590
  135. package/src/types/controller-types.ts +0 -385
  136. package/src/types/core.ts +0 -683
  137. package/src/types/middleware-types.ts +0 -422
  138. package/src/types/request-response-augmentation.ts +0 -489
  139. package/src/types/route-inference.ts +0 -372
  140. package/src/types.ts +0 -2123
  141. package/src/url.ts +0 -131
  142. package/src/utils/index.ts +0 -1
  143. package/src/utils/query-preservation.ts +0 -201
  144. package/src/utils.ts +0 -395
  145. package/src/validation/validator.ts +0 -688
  146. 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
- }