0
0
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:
Yusuke Wada 2022-05-19 09:29:09 +09:00 committed by GitHub
parent 9cfba5731f
commit 923a30e53d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 61 deletions

View File

@ -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"

View File

@ -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

View File

@ -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'

View 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 }

View File

@ -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) => {

View 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()
}
}
}

View File

@ -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
View 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"
]
}

View File

@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*"
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts"

View File

@ -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"
],
}