├── .npmrc ├── .eslintignore ├── .prettierignore ├── .gitignore ├── bun.lockb ├── .npmignore ├── .husky └── pre-commit ├── src ├── index.ts ├── types.ts ├── middleware │ └── bao.ts ├── utils │ ├── collapse-slashes.ts │ └── get-file-info.ts └── serve-static.ts ├── .prettierrc.cjs ├── .vscode └── settings.json ├── .lintstagedrc.cjs ├── .editorconfig ├── .eslintrc.cjs ├── tsconfig.json ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | sign-git-tag=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | dist/ 3 | .eslintcache 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImLunaHey/serve-static-bun/HEAD/bun.lockb -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode/ 3 | .editorconfig 4 | .gitignore 5 | tsconfig.json 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun lint-staged 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import serveStatic from "./serve-static"; 3 | export default serveStatic; 4 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: "lf", 4 | printWidth: 100, 5 | trailingComma: "all", 6 | useTabs: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "explorer.fileNesting.patterns": { 3 | "package.json": ".editorconfig, .eslint*, .lintstaged*, .npm*, .prettier*, bun.lockb, tsconfig.json", 4 | "readme*": "changelog*, license*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Run TypeScript check on whole project 3 | "*.ts": () => "tsc --noEmit", 4 | // Run ESLint on TS files 5 | "*.ts": "eslint --fix", 6 | // Run Prettier everywhere 7 | "*": "prettier --write --ignore-unknown", 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 100 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Errorlike } from "bun"; 2 | 3 | /** 4 | * Checks if an object is an error-like object, i.e. has a `code` property. 5 | * 6 | * @param error The error to check 7 | */ 8 | export function isErrorlike(error: unknown): error is Errorlike { 9 | if (typeof error !== "object" || error === null) { 10 | return false; 11 | } 12 | return Object.hasOwn(error, "code"); 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').ESLint.ConfigData} */ 2 | module.exports = { 3 | root: true, 4 | parser: "@typescript-eslint/parser", 5 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 6 | plugins: ["@typescript-eslint"], 7 | ignorePatterns: ["*.cjs"], 8 | parserOptions: { sourceType: "module" }, 9 | env: { 10 | es2020: true, 11 | node: true, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/middleware/bao.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "baojs/dist/context"; 2 | 3 | export default function getBaoMiddleware( 4 | getResponse: (req: Request) => Promise, 5 | handleErrors: boolean, 6 | ) { 7 | return async (ctx: Context) => { 8 | const res = await getResponse(ctx.req); 9 | switch (res.status) { 10 | case 403: 11 | case 404: 12 | return handleErrors ? ctx.sendRaw(res).forceSend() : ctx; 13 | 14 | default: 15 | return ctx.sendRaw(res).forceSend(); 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "verbatimModuleSyntax": true, 7 | "lib": ["ESNext"], 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": true, 12 | "noImplicitOverride": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "outDir": "dist", 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "target": "ESNext", 23 | "types": ["bun-types"] 24 | }, 25 | "include": ["src/**/*.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/collapse-slashes.ts: -------------------------------------------------------------------------------- 1 | interface CollapseSlashesOptions { 2 | /** 3 | * Keep leading slashes. 4 | * 5 | * @default true 6 | */ 7 | keepLeading?: boolean; 8 | 9 | /** 10 | * Keep trailing slashes. 11 | * 12 | * @default true 13 | */ 14 | keepTrailing?: boolean; 15 | } 16 | 17 | /** 18 | * By default, collapses all leading, trailing and duplicate slashes into one. 19 | * Can also remove leading and trailing slashes entirely. 20 | * 21 | * @param options 22 | * @returns New string with slashes normalized 23 | */ 24 | export function collapseSlashes(str: string, options: CollapseSlashesOptions = {}) { 25 | const { keepLeading = true, keepTrailing = true } = options; 26 | 27 | str = `/${str}/`.replaceAll(/[/]+/g, "/"); 28 | 29 | if (!keepLeading) { 30 | str = str.substring(1); 31 | } 32 | 33 | if (!keepTrailing) { 34 | str = str.substring(0, str.length - 1); 35 | } 36 | 37 | return str; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Jakob Bouchard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serve-static-bun", 3 | "version": "0.5.2", 4 | "description": "Serve static files using Bun.serve or Bao.js", 5 | "author": "Jakob Bouchard (https://jakobbouchard.dev/)", 6 | "contributors": [ 7 | "Volodymyr Palamar <@gornostay25>" 8 | ], 9 | "funding": [ 10 | "https://github.com/sponsors/jakobbouchard" 11 | ], 12 | "license": "MIT", 13 | "repository": "github:jakobbouchard/serve-static-bun", 14 | "homepage": "https://github.com/jakobbouchard/serve-static-bun#readme", 15 | "bugs": "https://github.com/jakobbouchard/serve-static-bun/issues", 16 | "keywords": [ 17 | "bun", 18 | "bunjs", 19 | "static", 20 | "serve-static", 21 | "serve", 22 | "baojs" 23 | ], 24 | "type": "module", 25 | "source": "./src/index.ts", 26 | "main": "./dist/index.js", 27 | "module": "./dist/index.js", 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "prepare": "husky install", 31 | "prepublish": "bun run build", 32 | "build": "rm -rf dist/ && tsc", 33 | "check": "tsc --noEmit", 34 | "lint": "prettier --check --cache . && eslint --cache .", 35 | "format": "prettier --write --cache ." 36 | }, 37 | "devDependencies": { 38 | "@typescript-eslint/eslint-plugin": "^5.59.2", 39 | "@typescript-eslint/parser": "^5.59.2", 40 | "baojs": "^0.2.1", 41 | "bun-types": "^0.5.8", 42 | "eslint": "^8.39.0", 43 | "eslint-config-prettier": "^8.8.0", 44 | "husky": "^8.0.3", 45 | "lint-staged": "^13.2.2", 46 | "prettier": "^2.8.8", 47 | "typescript": "^5.0.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/get-file-info.ts: -------------------------------------------------------------------------------- 1 | import type { FileBlob } from "bun"; 2 | import { isErrorlike } from "../types"; 3 | 4 | export interface FileInfo { 5 | /** 6 | * A blob with the file's info. 7 | * 8 | * @see Bun.FileBlob 9 | */ 10 | blob: FileBlob; 11 | 12 | /** 13 | * Whether the file exists. 14 | */ 15 | exists: boolean; 16 | 17 | /** 18 | * Whether the file is a file. If `false`, it is a directory. 19 | */ 20 | isFile: boolean; 21 | 22 | /** 23 | * The mime type of the file, if it can be determined. 24 | * If it cannot be determined, it will be `undefined`. 25 | */ 26 | mimeType?: string; 27 | } 28 | 29 | function getMimeType({ type }: FileBlob) { 30 | const charsetIndex = type.indexOf(";charset"); 31 | return charsetIndex !== -1 ? type.substring(0, charsetIndex) : type; 32 | } 33 | 34 | /** 35 | * Returns information about a file. 36 | * 37 | * @param path The path to the file 38 | * @returns Information about the file 39 | */ 40 | export default async function getFileInfo(path: string) { 41 | const info: FileInfo = { 42 | blob: Bun.file(path), 43 | exists: false, 44 | isFile: false, 45 | }; 46 | 47 | try { 48 | await info.blob.arrayBuffer(); 49 | info.exists = true; 50 | info.isFile = true; 51 | const mimeType = getMimeType(info.blob); 52 | info.mimeType = mimeType === "application/octet-stream" ? undefined : mimeType; 53 | } catch (error) { 54 | if (isErrorlike(error)) { 55 | switch (error.code) { 56 | case "EISDIR": 57 | info.exists = true; 58 | break; 59 | } 60 | } 61 | } 62 | 63 | return info; 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.5.0] - 2022-12-29 10 | 11 | ### Added 12 | 13 | - Descriptions for options in types. 14 | 15 | ### Changed 16 | 17 | - Use typings for bun v0.4.0 18 | - Updated descriptions for options in README.md 19 | - Will now immediately send a 404 response if the file is not found, instead of redirecting to a normalized path before. 20 | - `handleErrors` option is now `false` by default. 21 | 22 | ## [0.4.0] - 2022-11-21 23 | 24 | ### Changed 25 | 26 | - Use typings for Bao.js v0.2.x 27 | 28 | ## [0.3.0] - 2022-10-31 29 | 30 | ### Changed 31 | 32 | - The package is now an ES module instead of CJS. 33 | - The `src/` folder is now included in the npm build. 34 | 35 | ## [0.2.0] - 2022-08-04 36 | 37 | ### Added 38 | 39 | - [BREAKING] `dotfiles` option with `deny` as default. Your dotfiles will now respond with a 403 by default! No more leaking secrets. 40 | - `defaultMimeType` option. Defaults to `text/plain`. Files that usually responded with `application/octet-stream` will now respond with the `defaultMimeType`, instead of automatically downloading, which was quite weird. 41 | 42 | ## [0.1.4] - 2022-07-12 43 | 44 | ### Changes 45 | 46 | - Won't look for an index file if the path is not a directory. 47 | 48 | ## [0.1.3] - 2022-07-12 49 | 50 | ### Changes 51 | 52 | - Use `Bun.file` instead of `node:fs`. 53 | 54 | ### Removed 55 | 56 | - `mime` dependency. Now uses `Bun.file(file).type`! 57 | - `fileEncoding` option is gone, replaced by `charset` only. 58 | 59 | ## [0.1.1] - 2022-07-12 60 | 61 | ### Removed 62 | 63 | - Source maps 64 | 65 | ## [0.1.0] - 2022-07-12 66 | 67 | ### Added 68 | 69 | - Initial release 70 | - Support for `Bun.serve` 71 | - Support for `Bao.js` 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > I no longer work in web development and I don't really have the time or interest in maintaining this repository anymore. If anybody is interested in maintaining it, I would gladly transfer the repository and NPM package 3 | 4 | # serve-static-bun 5 | 6 | [![npm version](https://img.shields.io/npm/v/serve-static-bun?style=flat-square)](https://npm.im/serve-static-bun) 7 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/serve-static-bun?style=flat-square)](https://npm.im/serve-static-bun) 8 | [![npm downloads](https://img.shields.io/npm/dt/serve-static-bun?style=flat-square)](https://npm.im/serve-static-bun) 9 | 10 | Serve static files using `Bun.serve` or Bao.js. 11 | 12 | Currently in beta. Aiming for similar features as [`expressjs/serve-static`](https://github.com/expressjs/serve-static). 13 | 14 | ## Install 15 | 16 | This is a [Bun](https://bun.sh/) module available through the [npm registry](https://www.npmjs.com/). Installation is done using the [`bun install` command](https://github.com/oven-sh/bun#bun-install): 17 | 18 | ```sh 19 | bun install serve-static-bun 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```js 25 | import serveStatic from "serve-static-bun"; 26 | ``` 27 | 28 | ### `serveStatic(root, options)` 29 | 30 | Create a new middleware function to serve files from within a given root directory. The file to serve will be determined by combining `req.url` with the provided root directory. 31 | 32 | When a file is not found, it will send a 404 response. If a directory is accessed, but no index file is found, a 403 response will be sent. 33 | 34 | When used in middleware mode, the 404 and 403 responses will not be sent and will instead return the context to Bao.js, so that other routes can continue. 35 | 36 | By default, slashes are automatically collapsed in the path, and a trailing slash is added when the path is a directory. For example, if you have `blog/example/index.html` and access `https://example.com//blog///example`, it will redirect to `https://example.com/blog/example/`. 37 | 38 | #### Options 39 | 40 | ##### `index` 41 | 42 | By default this module will send "index.html" files in response to a request on a directory. To disable this, set it to `null`. To supply a new index, pass a string. 43 | 44 | ##### `dirTrailingSlash` 45 | 46 | Redirect to trailing "/" when the pathname is a dir. Defaults to `true`. 47 | 48 | ##### `collapseSlashes` 49 | 50 | Collapse all slashes in the pathname (`//blog///test` => `/blog/test`). Defaults to `true`. 51 | 52 | ##### `stripFromPathname` 53 | 54 | Removes the first occurence of the specified string from the pathname. Is not defined by default (no stripping). 55 | 56 | ##### `headers` 57 | 58 | Headers to add to the response. The "Content-Type" header cannot be overwritten. If you want to change the charset, use the `charset` option. If `collapseSlashes` or `dirTrailingSlash` is set, a "Location" header will be set if the pathname needs to be changed. 59 | 60 | ##### `dotfiles` 61 | 62 | This option allows you to configure how the module handles dotfiles, i.e. files or directories that begin with a dot ("."). Dotfiles return a 403 by default (when this is set to "deny"), but this can be changed with this option. 63 | 64 | ##### `defaultMimeType` 65 | 66 | The default mime type to send in the "Content-Type" HTTP header, when the file's cannot be determined. Defaults to `text/plain`. 67 | 68 | ##### `charset` 69 | 70 | The "Content-Type" HTTP header charset parameter. Defaults to `utf-8`. 71 | 72 | #### Middleware mode options 73 | 74 | ##### `middlewareMode` 75 | 76 | When set to `"bao"`, it will return a Bao.js compatible handler function instead. 77 | 78 | ##### `handleErrors` 79 | 80 | If set to `false`, in the case of a 403 or 404 response, the unmodified context will be returned to Bao.js. This allows you to handle the error yourself. 81 | If set to `true`, the error response will be sent to the client, without continuing the middleware chain. 82 | Defaults to `false`. 83 | 84 | ## Examples 85 | 86 | ### Serve files with vanilla `Bun.serve` 87 | 88 | ```ts 89 | import serveStatic from "serve-static-bun"; 90 | 91 | Bun.serve({ fetch: serveStatic("public") }); 92 | ``` 93 | 94 | ### Serve files with Bao.js 95 | 96 | ```ts 97 | import Bao from "baojs"; 98 | import serveStatic from "serve-static-bun"; 99 | 100 | const app = new Bao(); 101 | 102 | // *any can be anything 103 | // We need to strip /assets from the pathname, because when the root gets combined with the pathname, 104 | // it results in /assets/assets/file.js. 105 | app.get( 106 | "/assets/*any", 107 | serveStatic("assets", { middlewareMode: "bao", stripFromPathname: "/assets" }), 108 | ); 109 | 110 | app.get("/", (ctx) => ctx.sendText("Hello Bao!")); 111 | 112 | app.listen(); 113 | ``` 114 | 115 | ### Serve only static files with Bao.js 116 | 117 | **All** paths will be handled by `serve-static-bun`. 118 | 119 | ```ts 120 | import Bao from "baojs"; 121 | import serveStatic from "serve-static-bun"; 122 | 123 | const app = new Bao(); 124 | 125 | // *any can be anything 126 | app.get("/*any", serveStatic("web", { middlewareMode: "bao" })); 127 | 128 | app.listen(); 129 | ``` 130 | 131 | ## License 132 | 133 | [MIT](LICENSE) 134 | -------------------------------------------------------------------------------- /src/serve-static.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "baojs/dist/context"; 2 | import getBaoMiddleware from "./middleware/bao"; 3 | import getFileInfo, { type FileInfo } from "./utils/get-file-info"; 4 | 5 | /** 6 | * Options for serveStatic(). 7 | */ 8 | interface ServeStaticBaseOptions { 9 | /** 10 | * By default this module will send "index.html" files in response to a request on a directory. 11 | * To disable this, set it to `null`. To supply a new index, pass a string. 12 | * 13 | * @default "index.html" 14 | */ 15 | index?: string | null; 16 | 17 | /** 18 | * Redirect to trailing "/" when the pathname is a dir. 19 | * 20 | * @default true 21 | */ 22 | dirTrailingSlash?: boolean; 23 | 24 | /** 25 | * Collapse all slashes in the pathname (`//blog///test` => `/blog/test`). 26 | * 27 | * @default true 28 | */ 29 | collapseSlashes?: boolean; 30 | 31 | /** 32 | * Remove the first occurence of the specified string from the pathname. 33 | * Is not defined by default (no stripping). 34 | */ 35 | stripFromPathname?: string; 36 | 37 | /** 38 | * Headers to add to the response. The "Content-Type" header cannot be overwritten. If you want to 39 | * change the charset, use the `charset` option. If `collapseSlashes` or `dirTrailingSlash` is set, 40 | * a "Location" header will be set if the pathname needs to be changed. 41 | */ 42 | headers?: HeadersInit; 43 | 44 | /** 45 | * This option allows you to configure how the module handles dotfiles, i.e. files or directories that begin with a dot ("."). 46 | * Dotfiles return a 403 by default (when this is set to "deny"), but this can be changed with this option. 47 | * 48 | * @default "deny" 49 | */ 50 | dotfiles?: "allow" | "deny"; 51 | 52 | /** 53 | * The default mime type to send in the "Content-Type" HTTP header, when the file's cannot be determined. 54 | * 55 | * @default "text/plain" 56 | */ 57 | defaultMimeType?: string; 58 | 59 | /** 60 | * The "Content-Type" HTTP header charset parameter. 61 | * 62 | * @default "utf-8" 63 | */ 64 | charset?: string; 65 | } 66 | 67 | /** 68 | * Options for serveStatic() when used as a middleware. 69 | */ 70 | interface ServeStaticMiddlewareOptions extends ServeStaticBaseOptions { 71 | /** 72 | * The type of middleware to generate. 73 | * When set to `"bao"`, it will return a Bao.js compatible handler function instead. 74 | */ 75 | middlewareMode: "bao"; 76 | 77 | /** 78 | * If set to `false`, in the case of a 403 or 404 response, the unmodified context will be returned to Bao.js. 79 | * This allows you to handle the error yourself. 80 | * 81 | * If set to `true`, the error response will be sent to the client, without continuing the middleware chain. 82 | * 83 | * @default false 84 | */ 85 | handleErrors?: boolean; 86 | } 87 | 88 | /** 89 | * Type guard for ServeStaticMiddlewareOptions. 90 | */ 91 | function isMiddleware(options: ServeStaticOptions): options is ServeStaticMiddlewareOptions { 92 | return Object.hasOwn(options, "middlewareMode"); 93 | } 94 | 95 | type ServeStaticOptions = ServeStaticBaseOptions | ServeStaticMiddlewareOptions; 96 | 97 | /** 98 | * Get the correct pathname from the requested URL. 99 | * @param url The requested URL 100 | * @param stripFromPathname The string to remove from the pathname, if necessary 101 | */ 102 | function getPathname( 103 | { pathname }: URL, 104 | stripFromPathname: ServeStaticBaseOptions["stripFromPathname"], 105 | ) { 106 | return stripFromPathname ? pathname.replace(stripFromPathname, "") : pathname; 107 | } 108 | 109 | /** 110 | * Get the normalized path to redirect to. 111 | * @param pathname The requested pathname 112 | * @param requestedFile The requested file 113 | * @param options The serveStatic() options 114 | */ 115 | async function getRedirectPath( 116 | pathname: string, 117 | { isFile }: FileInfo, 118 | { 119 | collapseSlashes, 120 | dirTrailingSlash, 121 | }: Pick, 122 | ) { 123 | let redirectPath = pathname; 124 | 125 | // Normalize slashes 126 | if (collapseSlashes) { 127 | const pkg = await import("./utils/collapse-slashes"); 128 | redirectPath = pkg.collapseSlashes(redirectPath, { 129 | keepTrailing: redirectPath.endsWith("/"), // Preserve trailing slash if it exists 130 | }); 131 | } 132 | 133 | // Add trailing slash 134 | if (dirTrailingSlash && !isFile && !redirectPath.endsWith("/")) { 135 | redirectPath = `${redirectPath}/`; 136 | } 137 | 138 | return redirectPath; 139 | } 140 | 141 | /** 142 | * Get the file to serve, either the requested file or the folder's index file. 143 | * @param pathname The requested pathname 144 | * @param requestedFile The requested file 145 | * @param root The root path 146 | * @param options The serveStatic() options 147 | * @returns The file to serve, or null if none exists 148 | */ 149 | async function getFileToServe( 150 | pathname: string, 151 | requestedFile: FileInfo, 152 | root: string, 153 | { index, dotfiles }: Pick, 154 | ) { 155 | const isDotfile = pathname.split("/").pop()?.startsWith("."); 156 | if (requestedFile.isFile && (!isDotfile || dotfiles === "allow")) { 157 | return requestedFile; 158 | } 159 | 160 | // If it is a folder and it has an index 161 | const indexFile = index === null ? null : await getFileInfo(`${root}/${pathname}/${index}`); 162 | if (indexFile?.exists && indexFile.isFile) { 163 | return indexFile; 164 | } 165 | 166 | return null; 167 | } 168 | 169 | /** 170 | * For use with {@link Bun.serve}'s fetch function directly. 171 | * 172 | * @example 173 | * 174 | * ```ts 175 | * import serveStatic from "serve-static-bun"; 176 | * 177 | * Bun.serve({ fetch: serveStatic("frontend") }); 178 | * ``` 179 | * @param root The path to the static files to serve 180 | * @param options 181 | */ 182 | export default function serveStatic( 183 | root: string, 184 | options?: ServeStaticBaseOptions, 185 | ): (req: Request) => Promise; 186 | 187 | /** 188 | * For use as a bao middleware. 189 | * 190 | * @example 191 | * 192 | * Serve files with Bao.js 193 | * 194 | * ```ts 195 | * import Bao from "baojs"; 196 | * import serveStatic from "serve-static-bun"; 197 | * 198 | * const app = new Bao(); 199 | * 200 | * // *any can be anything 201 | * // We need to strip /assets from the pathname, because when the root gets combined with the pathname, 202 | * // it results in /assets/assets/file.js. 203 | * app.get("/assets/*any", serveStatic("assets", { middlewareMode: "bao", stripFromPathname: "/assets" })); 204 | * 205 | * app.get("/", (ctx) => ctx.sendText("Hello Bao!")); 206 | * 207 | * app.listen(); 208 | * ``` 209 | * 210 | * @example 211 | * 212 | * Serve only static files with Bao.js 213 | * 214 | * **All** paths will be handled by `serve-static-bun`. 215 | * 216 | * ```ts 217 | * import Bao from "baojs"; 218 | * import serveStatic from "serve-static-bun"; 219 | * 220 | * const app = new Bao(); 221 | * 222 | * // *any can be anything 223 | * app.get("/*any", serveStatic("web", { middlewareMode: "bao" })); 224 | * 225 | * app.listen(); 226 | * ``` 227 | * @param root The path to the static files to serve 228 | * @param options 229 | */ 230 | export default function serveStatic( 231 | root: string, 232 | options: ServeStaticMiddlewareOptions, 233 | ): (ctx: Context) => Promise; 234 | 235 | export default function serveStatic(root: string, options: ServeStaticOptions = {}) { 236 | root = `${process.cwd()}/${root}`; 237 | const { 238 | index = "index.html", 239 | dirTrailingSlash = true, 240 | collapseSlashes = true, 241 | stripFromPathname, 242 | headers, 243 | dotfiles = "deny", 244 | defaultMimeType = "text/plain", 245 | charset = "utf-8", 246 | } = options; 247 | const wantsMiddleware = isMiddleware(options); 248 | 249 | const getResponse = async (req: Request) => { 250 | const pathname = getPathname(new URL(req.url), stripFromPathname); 251 | const requestedFile = await getFileInfo(`${root}/${pathname}`); 252 | 253 | // If path does not exists, return 404 254 | if (!requestedFile.exists) { 255 | return new Response("404 Not Found", { 256 | status: 404, 257 | headers: { 258 | ...headers, 259 | "Content-Type": `text/plain; charset=${charset}`, 260 | }, 261 | }); 262 | } 263 | 264 | // Redirect to normalized path, if needed 265 | const redirectPath = await getRedirectPath(pathname, requestedFile, { 266 | collapseSlashes, 267 | dirTrailingSlash, 268 | }); 269 | if (redirectPath !== pathname) { 270 | return new Response(undefined, { 271 | status: 308, // Permanent Redirect, cacheable 272 | headers: { 273 | ...headers, 274 | Location: redirectPath, 275 | }, 276 | }); 277 | } 278 | 279 | // Serve file or index, if one of them exists 280 | const fileToServe = await getFileToServe(pathname, requestedFile, root, { index, dotfiles }); 281 | if (fileToServe) { 282 | return new Response(fileToServe.blob, { 283 | headers: { 284 | ...headers, 285 | "Content-Type": `${fileToServe.mimeType ?? defaultMimeType}; charset=${charset}`, 286 | }, 287 | }); 288 | } 289 | 290 | // Fallback to 403 291 | return new Response("403 Forbidden", { 292 | status: 403, 293 | headers: { 294 | ...headers, 295 | "Content-Type": `text/plain; charset=${charset}`, 296 | }, 297 | }); 298 | }; 299 | 300 | if (wantsMiddleware) { 301 | const { middlewareMode, handleErrors = false } = options; 302 | 303 | switch (middlewareMode) { 304 | case "bao": 305 | return getBaoMiddleware(getResponse, handleErrors); 306 | 307 | // No default 308 | } 309 | } 310 | 311 | return getResponse; 312 | } 313 | --------------------------------------------------------------------------------