1- import {
2- Context ,
3- type ErrorMiddleware ,
4- type Middleware ,
5- type MiddlewareEntry ,
6- type RouteHandler ,
7- type RouteMetadata ,
8- type ServeOptions ,
9- type StaticFileHandler
10- } from '@app/index.ts'
1+ import type {
2+ ErrorMiddleware ,
3+ Middleware ,
4+ MiddlewareEntry ,
5+ RouteHandler ,
6+ RouteMetadata ,
7+ ServeOptions ,
8+ StaticFileHandler
9+ } from '@app/Types.ts'
1110import { pathToFileURL } from 'node:url'
1211import { FastRouter } from '@neabyte/fast-router'
1312import { allowedExtensions , contentTypes , httpMethods } from '@app/Constant.ts'
13+ import { Context } from '@app/Context.ts'
1414
1515/**
1616 * Request handler class.
@@ -43,8 +43,9 @@ export class Handler {
4343 const metadata : RouteMetadata = {
4444 handler : {
4545 staticRoute : true ,
46+ urlPath,
4647 execute : async ( ctx : Context ) => {
47- return await this . serveStaticFile ( ctx , options )
48+ return await this . serveStaticFile ( ctx , options , urlPath )
4849 }
4950 } ,
5051 pattern : routePattern
@@ -82,7 +83,8 @@ export class Handler {
8283 'staticRoute' in handler &&
8384 handler . staticRoute
8485 ) {
85- return await ( handler as StaticFileHandler ) . execute ( ctx )
86+ const staticHandler = handler as StaticFileHandler
87+ return await staticHandler . execute ( ctx )
8688 }
8789 try {
8890 return await ( handler as RouteHandler ) ( ctx )
@@ -120,11 +122,11 @@ export class Handler {
120122 }
121123
122124 /**
123- * Handles responses with optional custom error middleware .
124- * @param ctx - The context object
125+ * Handles responses for routes and errors .
126+ * @param ctx - Context object
125127 * @param statusCode - HTTP status code
126128 * @param error - Error object
127- * @returns Response
129+ * @returns Response object
128130 */
129131 handleResponse ( ctx : Context , statusCode : number , error : Error ) : Response {
130132 if ( this . errorMiddleware ) {
@@ -138,7 +140,38 @@ export class Handler {
138140 return customResponse
139141 }
140142 }
141- return ctx . send . custom ( null , { status : statusCode , headers : ctx . responseHeadersMap } )
143+ const isJson = ctx . request . headers . get ( 'accept' ) ?. includes ( 'application/json' )
144+ if ( isJson ) {
145+ return ctx . send . json (
146+ {
147+ error : error . message ,
148+ path : ctx . pathname ,
149+ statusCode
150+ } ,
151+ { status : statusCode }
152+ )
153+ }
154+ const html = `
155+ <!DOCTYPE html>
156+ <html>
157+ <head>
158+ <title>${ statusCode } - ${ error . message } </title>
159+ <style>
160+ body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #fafafa; color: #333; }
161+ .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
162+ h1 { font-size: 3rem; margin: 0; color: #ff6b6b; }
163+ p { color: #666; margin: 1rem 0; }
164+ </style>
165+ </head>
166+ <body>
167+ <div class="card">
168+ <h1>${ statusCode } </h1>
169+ <p>${ error . message } </p>
170+ </div>
171+ </body>
172+ </html>
173+ `
174+ return ctx . send . html ( html , { status : statusCode } )
142175 }
143176
144177 /**
@@ -163,7 +196,7 @@ export class Handler {
163196 const routePattern = this . createRoutePattern ( routePath )
164197 if ( routePattern ) {
165198 this . validateRouteModule ( fileModule , routePath )
166- Object . keys ( fileModule ) . forEach ( ( method ) => {
199+ Object . keys ( fileModule ) . forEach ( method => {
167200 const handler = fileModule [ method ] as RouteHandler
168201 const metadata : RouteMetadata = {
169202 handler,
@@ -198,7 +231,7 @@ export class Handler {
198231 * @throws {Error } When module is invalid
199232 */
200233 validateRouteModule ( module : Record < string , unknown > , routePath : string ) : void {
201- const exportedMethods = Object . keys ( module ) . filter ( ( key ) => httpMethods . includes ( key ) )
234+ const exportedMethods = Object . keys ( module ) . filter ( key => httpMethods . includes ( key ) )
202235 if ( exportedMethods . length === 0 ) {
203236 throw new Error (
204237 `Route ${ routePath } : Must export at least one HTTP method (${ httpMethods . join ( ', ' ) } )`
@@ -220,9 +253,14 @@ export class Handler {
220253 * @returns Response if middleware returned one, undefined otherwise
221254 */
222255 private async executeMiddlewares ( ctx : Context , pathname : string ) : Promise < Response | undefined > {
223- const applicableMiddlewares = this . entryMiddleware . filter (
224- ( mw ) => mw . path === '' || pathname . startsWith ( mw . path ) || mw . path === '*'
225- )
256+ const applicableMiddlewares = this . entryMiddleware . filter ( mw => {
257+ if ( mw . path === '' || mw . path === '*' ) return true
258+ if ( mw . path . endsWith ( '/**' ) ) {
259+ const base = mw . path . slice ( 0 , - 3 )
260+ return pathname . startsWith ( base )
261+ }
262+ return pathname === mw . path
263+ } )
226264 let index = 0
227265 const next = async ( ) : Promise < Response > => {
228266 if ( index >= applicableMiddlewares . length ) {
@@ -246,43 +284,60 @@ export class Handler {
246284 * Serves static files from the filesystem.
247285 * @param ctx - Context object
248286 * @param options - Static file serving options
287+ * @param urlPath - URL mount point
249288 * @returns Response with file or 404
250289 */
251- private async serveStaticFile ( ctx : Context , options : ServeOptions ) : Promise < Response > {
290+ private async serveStaticFile (
291+ ctx : Context ,
292+ options : ServeOptions ,
293+ urlPath : string
294+ ) : Promise < Response > {
252295 try {
253- const filePath = ctx . pathname === '/' ? 'index.html' : ctx . pathname . slice ( 1 )
296+ let filePath = ctx . pathname
297+ if ( urlPath !== '/' ) {
298+ filePath = ctx . pathname . slice ( urlPath . length )
299+ }
300+ if ( filePath === '/' || filePath === '' ) {
301+ filePath = 'index.html'
302+ } else if ( filePath . startsWith ( '/' ) ) {
303+ filePath = filePath . slice ( 1 )
304+ }
254305 const basePath = options . path . startsWith ( '/' ) ? options . path : `${ Deno . cwd ( ) } /${ options . path } `
255306 const fullPath = new URL ( filePath , `file://${ basePath . replace ( / ^ \. \/ / , '' ) } /` ) . pathname
256307 const fileInfo = await Deno . stat ( fullPath ) . catch ( ( ) => null )
257308 if ( ! fileInfo || ! fileInfo . isFile ) {
258309 return ctx . handleError ( 404 , new Error ( 'File not found' ) )
259310 }
260- const fileData = await Deno . readFile ( fullPath )
261311 const extension = filePath . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? ''
262312 const contentType = contentTypes [ extension ] ?? 'application/octet-stream'
313+ const file = await Deno . open ( fullPath , { read : true } )
263314 let etag : string | null = null
264315 if ( options . etag ) {
265- const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , fileData )
316+ const hashBuffer = await crypto . subtle . digest (
317+ 'SHA-256' ,
318+ new TextEncoder ( ) . encode ( `${ fileInfo . size } -${ fileInfo . mtime ?. getTime ( ) } ` )
319+ )
266320 const hashArray = Array . from ( new Uint8Array ( hashBuffer ) )
267- const hashHex = hashArray . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' )
321+ const hashHex = hashArray . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' )
268322 etag = `"${ hashHex } "`
269323 }
270324 if ( etag && ctx . request . headers . get ( 'If-None-Match' ) === etag ) {
325+ file . close ( )
271326 ctx . setHeader ( 'ETag' , etag )
272327 if ( options . cacheControl !== undefined ) {
273328 ctx . setHeader ( 'Cache-Control' , `public, max-age=${ options . cacheControl } ` )
274329 }
275330 return ctx . handleError ( 304 , new Error ( 'Not Modified' ) )
276331 }
277332 ctx . setHeader ( 'Content-Type' , contentType )
278- ctx . setHeader ( 'Content-Length' , fileData . length . toString ( ) )
333+ ctx . setHeader ( 'Content-Length' , fileInfo . size . toString ( ) )
279334 if ( etag ) {
280335 ctx . setHeader ( 'ETag' , etag )
281336 }
282337 if ( options . cacheControl !== undefined ) {
283338 ctx . setHeader ( 'Cache-Control' , `public, max-age=${ options . cacheControl } ` )
284339 }
285- return ctx . send . custom ( fileData )
340+ return ctx . send . custom ( file . readable )
286341 } catch ( error ) {
287342 return ctx . handleError ( 500 , error as Error )
288343 }
0 commit comments