mirror of
https://github.com/honojs/hono.git
synced 2024-11-21 18:18:57 +01:00
feat: serve-static middleware supports Module Worker mode (#250)
This commit is contained in:
parent
9cfba5731f
commit
923a30e53d
@ -11,7 +11,7 @@
|
||||
"test": "jest",
|
||||
"lint": "eslint --ext js,ts src .eslintrc.js",
|
||||
"lint:fix": "eslint --ext js,ts src .eslintrc.js --fix",
|
||||
"build": "rimraf dist && tsc --project tsconfig.build.json",
|
||||
"build": "rimraf dist && tsc --project tsconfig.build.json && tsc --project tsconfig.build.esm.json",
|
||||
"watch": "tsc --project tsconfig.build.json -w",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
@ -29,6 +29,7 @@
|
||||
"./powered-by": "./dist/middleware/powered-by/index.js",
|
||||
"./pretty-json": "./dist/middleware/pretty-json/index.js",
|
||||
"./serve-static": "./dist/middleware/serve-static/index.js",
|
||||
"./serve-static.module": "./dist/middleware/serve-static/module.mjs",
|
||||
"./router/trie-router": "./dist/router/trie-router/index.js",
|
||||
"./router/reg-exp-router": "./dist/router/reg-exp-router/index.js",
|
||||
"./utils/jwt": "./dist/utils/jwt/index.js",
|
||||
@ -70,7 +71,10 @@
|
||||
"./dist/middleware/pretty-json"
|
||||
],
|
||||
"serve-static": [
|
||||
"./dist/middleware/serve-static"
|
||||
"./dist/middleware/serve-static/index.d.ts"
|
||||
],
|
||||
"serve-static.module": [
|
||||
"./dist/middleware/serve-static/module.d.ts"
|
||||
],
|
||||
"router/trie-router": [
|
||||
"./dist/router/trie-router/router.d.ts"
|
||||
|
@ -1,12 +1,12 @@
|
||||
# Serve Static Middleware
|
||||
|
||||
Mustache Middleware is available only on Cloudflare Workers.
|
||||
Serve Static Middleware is available only on Cloudflare Workers.
|
||||
|
||||
## Usage
|
||||
|
||||
index.js:
|
||||
index.ts:
|
||||
|
||||
```js
|
||||
```ts
|
||||
import { Hono } from 'hono'
|
||||
import { serveStatic } from 'hono/serve-static'
|
||||
|
||||
@ -18,6 +18,18 @@ app.get('/', (c) => c.text('This is Home! You can access: /static/hello.txt'))
|
||||
app.fire()
|
||||
```
|
||||
|
||||
In Module Worker mode:
|
||||
|
||||
```ts
|
||||
import { Hono } from 'hono'
|
||||
import { serveStatic } from 'hono/serve-static.module' // <---
|
||||
|
||||
const app = new Hono()
|
||||
//...
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
wrangler.toml:
|
||||
|
||||
```toml
|
||||
|
@ -1,41 +1 @@
|
||||
import type { Context } from '../../context'
|
||||
import type { Next } from '../../hono'
|
||||
import { getContentFromKVAsset, getKVFilePath } from '../../utils/cloudflare'
|
||||
import { getMimeType } from '../../utils/mime'
|
||||
|
||||
type Options = {
|
||||
root: string
|
||||
}
|
||||
|
||||
const DEFAULT_DOCUMENT = 'index.html'
|
||||
|
||||
// This middleware is available only on Cloudflare Workers.
|
||||
export const serveStatic = (opt: Options = { root: '' }) => {
|
||||
return async (c: Context, next: Next) => {
|
||||
// Do nothing if Response is already set
|
||||
if (c.res) {
|
||||
await next()
|
||||
}
|
||||
|
||||
const url = new URL(c.req.url)
|
||||
|
||||
const path = getKVFilePath({
|
||||
filename: url.pathname,
|
||||
root: opt.root,
|
||||
defaultDocument: DEFAULT_DOCUMENT,
|
||||
})
|
||||
|
||||
const content = await getContentFromKVAsset(path)
|
||||
if (content) {
|
||||
const mimeType = getMimeType(path)
|
||||
if (mimeType) {
|
||||
c.header('Content-Type', mimeType)
|
||||
}
|
||||
// Return Response object
|
||||
return c.body(content)
|
||||
} else {
|
||||
console.warn(`Static file: ${path} is not found`)
|
||||
await next()
|
||||
}
|
||||
}
|
||||
}
|
||||
export { serveStatic } from './serve-static'
|
||||
|
15
src/middleware/serve-static/module.mts
Normal file
15
src/middleware/serve-static/module.mts
Normal file
@ -0,0 +1,15 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// For ES module mode
|
||||
import manifest from '__STATIC_CONTENT_MANIFEST'
|
||||
import type { ServeStaticOptions } from './serve-static'
|
||||
import { serveStatic } from './serve-static'
|
||||
|
||||
const module = (options: ServeStaticOptions = { root: '' }) => {
|
||||
return serveStatic({
|
||||
root: options.root,
|
||||
manifest: options.manifest ? options.manifest : manifest,
|
||||
})
|
||||
}
|
||||
|
||||
export { module as serveStatic }
|
@ -4,11 +4,12 @@ import { Hono } from '../../hono'
|
||||
import { serveStatic } from '.'
|
||||
|
||||
// Mock
|
||||
const store: { [key: string]: string } = {
|
||||
const store: Record<string, string> = {
|
||||
'assets/static/plain.abcdef.txt': 'This is plain.txt',
|
||||
'assets/static/hono.abcdef.html': '<h1>Hono!</h1>',
|
||||
'assets/static/top/index.abcdef.html': '<h1>Top</h1>',
|
||||
'static-no-root/plain.abcdef.txt': 'That is plain.txt',
|
||||
'assets/static/options/foo.abcdef.txt': 'With options',
|
||||
}
|
||||
const manifest = JSON.stringify({
|
||||
'assets/static/plain.txt': 'assets/static/plain.abcdef.txt',
|
||||
@ -65,6 +66,22 @@ describe('ServeStatic Middleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('With options', () => {
|
||||
const manifest = {
|
||||
'assets/static/options/foo.txt': 'assets/static/options/foo.abcdef.txt',
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
app.use('/static/*', serveStatic({ root: './assets', manifest: manifest }))
|
||||
|
||||
it('Should return foo.txt', async () => {
|
||||
const res = await app.request('http://localhost/static/options/foo.txt')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('With options')
|
||||
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8')
|
||||
})
|
||||
})
|
||||
|
||||
describe('With middleware', () => {
|
||||
const app = new Hono()
|
||||
const md1 = async (c: Context, next: Next) => {
|
46
src/middleware/serve-static/serve-static.ts
Normal file
46
src/middleware/serve-static/serve-static.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { Context } from '../../context'
|
||||
import type { Handler, Next } from '../../hono'
|
||||
import { getContentFromKVAsset, getKVFilePath } from '../../utils/cloudflare'
|
||||
import { getMimeType } from '../../utils/mime'
|
||||
|
||||
export type ServeStaticOptions = {
|
||||
root: string
|
||||
manifest?: object | string
|
||||
namespace?: KVNamespace
|
||||
}
|
||||
|
||||
const DEFAULT_DOCUMENT = 'index.html'
|
||||
|
||||
// This middleware is available only on Cloudflare Workers.
|
||||
export const serveStatic = (options: ServeStaticOptions = { root: '' }): Handler => {
|
||||
return async (c: Context, next: Next) => {
|
||||
// Do nothing if Response is already set
|
||||
if (c.res) {
|
||||
await next()
|
||||
}
|
||||
|
||||
const url = new URL(c.req.url)
|
||||
|
||||
const path = getKVFilePath({
|
||||
filename: url.pathname,
|
||||
root: options.root,
|
||||
defaultDocument: DEFAULT_DOCUMENT,
|
||||
})
|
||||
|
||||
const content = await getContentFromKVAsset(path, {
|
||||
manifest: options.manifest,
|
||||
namespace: options.namespace ? options.namespace : c.env ? c.env.__STATIC_CONTENT : undefined,
|
||||
})
|
||||
if (content) {
|
||||
const mimeType = getMimeType(path)
|
||||
if (mimeType) {
|
||||
c.header('Content-Type', mimeType)
|
||||
}
|
||||
// Return Response object
|
||||
return c.body(content)
|
||||
} else {
|
||||
console.warn(`Static file: ${path} is not found`)
|
||||
await next()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,37 @@
|
||||
declare const __STATIC_CONTENT: KVNamespace, __STATIC_CONTENT_MANIFEST: string
|
||||
declare const __STATIC_CONTENT: KVNamespace
|
||||
declare const __STATIC_CONTENT_MANIFEST: string
|
||||
|
||||
export const getContentFromKVAsset = async (path: string): Promise<ArrayBuffer> => {
|
||||
let ASSET_MANIFEST: Record<string, string>
|
||||
if (typeof __STATIC_CONTENT_MANIFEST === 'string') {
|
||||
ASSET_MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST)
|
||||
export type KVAssetOptions = {
|
||||
manifest?: object | string
|
||||
namespace?: KVNamespace
|
||||
}
|
||||
|
||||
export const getContentFromKVAsset = async (
|
||||
path: string,
|
||||
options?: KVAssetOptions
|
||||
): Promise<ArrayBuffer> => {
|
||||
let ASSET_MANIFEST: Record<string, string> = {}
|
||||
|
||||
if (options && options.manifest) {
|
||||
if (typeof options.manifest === 'string') {
|
||||
ASSET_MANIFEST = JSON.parse(options.manifest)
|
||||
} else {
|
||||
ASSET_MANIFEST = options.manifest as Record<string, string>
|
||||
}
|
||||
} else {
|
||||
ASSET_MANIFEST = __STATIC_CONTENT_MANIFEST
|
||||
if (typeof __STATIC_CONTENT_MANIFEST === 'string') {
|
||||
ASSET_MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST)
|
||||
} else {
|
||||
ASSET_MANIFEST = __STATIC_CONTENT_MANIFEST
|
||||
}
|
||||
}
|
||||
|
||||
const ASSET_NAMESPACE = __STATIC_CONTENT
|
||||
let ASSET_NAMESPACE: KVNamespace
|
||||
if (options && options.namespace) {
|
||||
ASSET_NAMESPACE = options.namespace
|
||||
} else {
|
||||
ASSET_NAMESPACE = __STATIC_CONTENT
|
||||
}
|
||||
|
||||
const key = ASSET_MANIFEST[path] || path
|
||||
if (!key) {
|
||||
@ -23,16 +46,16 @@ export const getContentFromKVAsset = async (path: string): Promise<ArrayBuffer>
|
||||
return content
|
||||
}
|
||||
|
||||
type Options = {
|
||||
type FilePathOptions = {
|
||||
filename: string
|
||||
root?: string
|
||||
defaultDocument?: string
|
||||
}
|
||||
|
||||
export const getKVFilePath = (opt: Options): string => {
|
||||
let filename = opt.filename
|
||||
let root = opt.root || ''
|
||||
const defaultDocument = opt.defaultDocument || 'index.html'
|
||||
export const getKVFilePath = (options: FilePathOptions): string => {
|
||||
let filename = options.filename
|
||||
let root = options.root || ''
|
||||
const defaultDocument = options.defaultDocument || 'index.html'
|
||||
|
||||
if (filename.endsWith('/')) {
|
||||
// /top/ => /top/index.html
|
||||
|
16
tsconfig.build.esm.json
Normal file
16
tsconfig.build.esm.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./dist/",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.mts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts"
|
||||
|
@ -3,6 +3,7 @@
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@ -17,7 +18,7 @@
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
}
|
Loading…
Reference in New Issue
Block a user