From 923a30e53d70e7f4c93c3ee50dfa4acb75a60ae7 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 19 May 2022 09:29:09 +0900 Subject: [PATCH] feat: serve-static middleware supports Module Worker mode (#250) --- package.json | 8 +++- src/middleware/serve-static/README.md | 18 +++++-- src/middleware/serve-static/index.ts | 42 +---------------- src/middleware/serve-static/module.mts | 15 ++++++ .../{index.test.ts => serve-static.test.ts} | 19 +++++++- src/middleware/serve-static/serve-static.ts | 46 ++++++++++++++++++ src/utils/cloudflare.ts | 47 ++++++++++++++----- tsconfig.build.esm.json | 16 +++++++ tsconfig.build.json | 2 +- tsconfig.json | 3 +- 10 files changed, 155 insertions(+), 61 deletions(-) create mode 100644 src/middleware/serve-static/module.mts rename src/middleware/serve-static/{index.test.ts => serve-static.test.ts} (84%) create mode 100644 src/middleware/serve-static/serve-static.ts create mode 100644 tsconfig.build.esm.json diff --git a/package.json b/package.json index a0f075c8..c7c6d739 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/middleware/serve-static/README.md b/src/middleware/serve-static/README.md index 3670479f..f648d759 100644 --- a/src/middleware/serve-static/README.md +++ b/src/middleware/serve-static/README.md @@ -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 diff --git a/src/middleware/serve-static/index.ts b/src/middleware/serve-static/index.ts index 9bd61013..78e46e0b 100644 --- a/src/middleware/serve-static/index.ts +++ b/src/middleware/serve-static/index.ts @@ -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' diff --git a/src/middleware/serve-static/module.mts b/src/middleware/serve-static/module.mts new file mode 100644 index 00000000..75fdbc61 --- /dev/null +++ b/src/middleware/serve-static/module.mts @@ -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 } diff --git a/src/middleware/serve-static/index.test.ts b/src/middleware/serve-static/serve-static.test.ts similarity index 84% rename from src/middleware/serve-static/index.test.ts rename to src/middleware/serve-static/serve-static.test.ts index b83526ba..0ca27482 100644 --- a/src/middleware/serve-static/index.test.ts +++ b/src/middleware/serve-static/serve-static.test.ts @@ -4,11 +4,12 @@ import { Hono } from '../../hono' import { serveStatic } from '.' // Mock -const store: { [key: string]: string } = { +const store: Record = { 'assets/static/plain.abcdef.txt': 'This is plain.txt', 'assets/static/hono.abcdef.html': '

Hono!

', 'assets/static/top/index.abcdef.html': '

Top

', '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) => { diff --git a/src/middleware/serve-static/serve-static.ts b/src/middleware/serve-static/serve-static.ts new file mode 100644 index 00000000..6f45c0f7 --- /dev/null +++ b/src/middleware/serve-static/serve-static.ts @@ -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() + } + } +} diff --git a/src/utils/cloudflare.ts b/src/utils/cloudflare.ts index 33e271bc..9f388b0a 100644 --- a/src/utils/cloudflare.ts +++ b/src/utils/cloudflare.ts @@ -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 => { - let ASSET_MANIFEST: Record - 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 => { + let ASSET_MANIFEST: Record = {} + + if (options && options.manifest) { + if (typeof options.manifest === 'string') { + ASSET_MANIFEST = JSON.parse(options.manifest) + } else { + ASSET_MANIFEST = options.manifest as Record + } } 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 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 diff --git a/tsconfig.build.esm.json b/tsconfig.build.esm.json new file mode 100644 index 00000000..76f93fbe --- /dev/null +++ b/tsconfig.build.esm.json @@ -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" + ] +} \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json index 07d57331..44a89598 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "include": [ - "src/**/*" + "src/**/*.ts" ], "exclude": [ "src/**/*.test.ts" diff --git a/tsconfig.json b/tsconfig.json index b9bfc546..23733fa6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ], } \ No newline at end of file