This release is 9 versions behind 4.7.9 — the latest version of @hono/hono. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
Web framework built on Web Standards
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




JSR Score
76%
Published
4 months ago (4.6.20)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150/** * @module * Serve Static Middleware for Hono. */ import type { Context, Data } from '../../context.ts' import type { Env, MiddlewareHandler } from '../../types.ts' import { COMPRESSIBLE_CONTENT_TYPE_REGEX } from '../../utils/compress.ts' import { getFilePath, getFilePathWithoutDefaultDocument } from '../../utils/filepath.ts' import { getMimeType } from '../../utils/mime.ts' export type ServeStaticOptions<E extends Env = Env> = { root?: string path?: string precompressed?: boolean mimes?: Record<string, string> rewriteRequestPath?: (path: string) => string onFound?: (path: string, c: Context<E>) => void | Promise<void> onNotFound?: (path: string, c: Context<E>) => void | Promise<void> } const ENCODINGS = { br: '.br', zstd: '.zst', gzip: '.gz', } as const const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS) as (keyof typeof ENCODINGS)[] const DEFAULT_DOCUMENT = 'index.html' const defaultPathResolve = (path: string) => path /** * This middleware is not directly used by the user. Create a wrapper specifying `getContent()` by the environment such as Deno or Bun. */ export const serveStatic = <E extends Env = Env>( options: ServeStaticOptions<E> & { getContent: (path: string, c: Context<E>) => Promise<Data | Response | null> pathResolve?: (path: string) => string isDir?: (path: string) => boolean | undefined | Promise<boolean | undefined> } ): MiddlewareHandler => { let isAbsoluteRoot = false let root: string if (options.root) { if (options.root.startsWith('/')) { isAbsoluteRoot = true root = new URL(`file://${options.root}`).pathname } else { root = options.root } } return async (c, next) => { // Do nothing if Response is already set if (c.finalized) { await next() return } let filename = options.path ?? decodeURI(c.req.path) filename = options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename // If it was Directory, force `/` on the end. if (!filename.endsWith('/') && options.isDir) { const path = getFilePathWithoutDefaultDocument({ filename, root, }) if (path && (await options.isDir(path))) { filename += '/' } } let path = getFilePath({ filename, root, defaultDocument: DEFAULT_DOCUMENT, }) if (!path) { return await next() } if (isAbsoluteRoot) { path = '/' + path } const getContent = options.getContent const pathResolve = options.pathResolve ?? defaultPathResolve path = pathResolve(path) let content = await getContent(path, c) if (!content) { let pathWithoutDefaultDocument = getFilePathWithoutDefaultDocument({ filename, root, }) if (!pathWithoutDefaultDocument) { return await next() } pathWithoutDefaultDocument = pathResolve(pathWithoutDefaultDocument) if (pathWithoutDefaultDocument !== path) { content = await getContent(pathWithoutDefaultDocument, c) if (content) { path = pathWithoutDefaultDocument } } } if (content instanceof Response) { return c.newResponse(content.body, content) } if (content) { const mimeType = (options.mimes && getMimeType(path, options.mimes)) || getMimeType(path) c.header('Content-Type', mimeType || 'application/octet-stream') if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { const acceptEncodingSet = new Set( c.req .header('Accept-Encoding') ?.split(',') .map((encoding) => encoding.trim()) ) for (const encoding of ENCODINGS_ORDERED_KEYS) { if (!acceptEncodingSet.has(encoding)) { continue } const compressedContent = (await getContent(path + ENCODINGS[encoding], c)) as Data | null if (compressedContent) { content = compressedContent c.header('Content-Encoding', encoding) c.header('Vary', 'Accept-Encoding', { append: true }) break } } } await options.onFound?.(path, c) return c.body(content) } await options.onNotFound?.(path, c) await next() return } }