Skip to content

Commit

Permalink
feat: esbuild Transpiler Middleware (#231)
Browse files Browse the repository at this point in the history
* feat: esbuild Transpiler Middleware

* changeset

* add linkt to docs

* add `global.d.ts` for docs

* update readme

Co-authored-by: Andres C. Rodriguez <acrodrig@users.noreply.github.com>
  • Loading branch information
yusukebe and acrodrig authored Nov 5, 2023
1 parent 87c3795 commit 1243fc5
Show file tree
Hide file tree
Showing 12 changed files with 1,303 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-mayflies-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/esbuild-transpiler': minor
---

feat: introduce esbuild Transpiler Middleware
25 changes: 25 additions & 0 deletions .github/workflows/ci-esbuild-transpiler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-esbuild-transpiler
on:
push:
branches: [main]
paths:
- 'packages/esbuild-transpiler/**'
pull_request:
branches: ['*']
paths:
- 'packages/esbuild-transpiler/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/esbuild-transpiler
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
106 changes: 106 additions & 0 deletions packages/esbuild-transpiler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# esbuild Transpiler Middleware

The **esbuild Transpiler Middleware** is a Hono Middleware designed to transpile content such as TypeScript or TSX.
You can place your script written in TypeScript in a directory and serve it using `serveStatic`.
When you apply this Middleware, the script will be transpiled into JavaScript code.

This Middleware uses esbuild. It works on _Cloudflare Workers, Deno, Deno Deploy, or Node.js_.

## Usage

Usage differs depending on the platform.

### Cloudflare Workers / Pages

#### Installation

```text
npm i hono @hono/esbuild-transpiler esbuild-wasm
```

#### Example

```ts
import { Hono } from 'hono'
import { serveStatic } from 'hono/cloudflare-workers'
import { esbuildTranspiler } from '@hono/esbuild-transpiler/wasm'
// Specify the path of the esbuild wasm file.
import wasm from '../node_modules/esbuild-wasm/esbuild.wasm'

const app = new Hono()

app.get('/static/:scriptName{.+.tsx?}', esbuildTranspiler({ wasmModule: wasm }))
app.get('/static/*', serveStatic({ root: './' }))

export default app
```

`global.d.ts`:

```ts
declare module '*.wasm'
```
### Deno / Deno Deploy
#### Example
```ts
import { Hono } from 'npm:hono'

import { serveStatic } from 'npm:hono/deno'
import { esbuildTranspiler } from 'npm:@hono/esbuild-transpiler'
import * as esbuild from 'https://deno.land/x/esbuild@v0.19.5/wasm.js'

const app = new Hono()

await esbuild.initialize({
wasmURL: 'https://deno.land/x/esbuild@v0.19.5/esbuild.wasm',
worker: false,
})

app.get('/static/*', esbuildTranspiler({ esbuild }))
app.get('/static/*', serveStatic())

Deno.serve(app.fetch)
```

### Node.js

#### Installation

```text
npm i hono @hono/node-server @hono/esbuild-transpiler esbuild
```

#### Example

```ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { esbuildTranspiler } from '@hono/esbuild-transpiler/node'

const app = new Hono()

app.get('/static/:scriptName{.+.tsx?}', esbuildTranspiler())
app.get('/static/*', serveStatic({ root: './' }))

serve(app)
```

## Notes

- This middleware does not have a cache feature. If you want to cache the transpiled code, use [Cache Middleware](https://hono.dev/middleware/builtin/cache) or your own custom middleware.
- `@hono/vite-dev-server` does not support Wasm, so you can't use this Middleware with it. However, Vite can transpile them, so you might not need to use this.

## Authors

- Yusuke Wada <https://github.com/yusukebe>
- Andres C. Rodriguez <https://github.com/acrodrig>

Original idea and implementation for "_Typescript Transpiler Middleware_" is by Andres C. Rodriguez.

## License

MIT
64 changes: 64 additions & 0 deletions packages/esbuild-transpiler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "@hono/esbuild-transpiler",
"version": "0.0.0",
"description": "esbuild Transpiler Middleware for Hono",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "vitest run",
"build": "tsup ./src/*.ts ./src/transpilers/*.ts --format esm,cjs --dts --no-splitting --external esbuild-wasm,esbuild",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./wasm": {
"types": "./dist/transpilers/wasm.d.ts",
"import": "./dist/transpilers/wasm.js"
},
"./node": {
"types": "./dist/transpilers/node.d.ts",
"import": "./dist/transpilers/node.js"
}
},
"typesVersions": {
"*": {
"wasm": [
"./dist/transpilers/wasm"
],
"node": [
"./dist/transpilers/node"
]
}
},
"license": "MIT",
"private": false,
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": ">=3.9.2"
},
"devDependencies": {
"esbuild": "^0.19.5",
"esbuild-wasm": "^0.19.5",
"hono": "^3.9.2",
"vitest": "^0.34.6"
},
"engines": {
"node": ">=18.14.1"
}
}
1 change: 1 addition & 0 deletions packages/esbuild-transpiler/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { esbuildTranspiler } from './transpiler'
50 changes: 50 additions & 0 deletions packages/esbuild-transpiler/src/transpiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createMiddleware } from 'hono/factory'
import type { transform, initialize } from './types.esbuild'

export type EsbuildLike = {
transform: typeof transform
initialize: typeof initialize
}

export type TransformOptions = Partial<Parameters<typeof transform>[1]>

export type EsbuildTranspilerOptions = {
extensions?: string[]
cache?: boolean
esbuild?: EsbuildLike
contentType?: string
transformOptions?: TransformOptions
}

export const esbuildTranspiler = (options?: EsbuildTranspilerOptions) => {
const esbuild: EsbuildLike | undefined = options?.esbuild

return createMiddleware(async (c, next) => {
await next()
if (esbuild) {
const url = new URL(c.req.url)
const extensions = options?.extensions ?? ['.ts', '.tsx']

if (extensions.every((ext) => !url.pathname.endsWith(ext))) return

const headers = { 'content-type': options?.contentType ?? 'text/javascript' }
const script = await c.res.text()

const transformOptions: TransformOptions = options?.transformOptions ?? {}

try {
const { code } = await esbuild.transform(script, {
loader: 'tsx',
...transformOptions,
})
c.res = c.body(code, {
headers,
})
c.res.headers.delete('content-length')
} catch (ex) {
console.warn('Error transpiling ' + url.pathname + ': ' + ex)
c.res = new Response(script, { status: 500, headers })
}
}
})
}
15 changes: 15 additions & 0 deletions packages/esbuild-transpiler/src/transpilers/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as esbuild from 'esbuild'
import { createMiddleware } from 'hono/factory'
import { esbuildTranspiler as baseTranspiler } from '../transpiler'
import type { EsbuildTranspilerOptions } from '../transpiler'

const transpiler = (options?: Partial<Omit<EsbuildTranspilerOptions, 'esbuild'>>) => {
return createMiddleware(async (c, next) => {
return await baseTranspiler({
esbuild,
...options,
})(c, next)
})
}

export { transpiler as esbuildTranspiler }
38 changes: 38 additions & 0 deletions packages/esbuild-transpiler/src/transpilers/wasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as esbuild from 'esbuild-wasm'
import { createMiddleware } from 'hono/factory'
import { esbuildTranspiler as baseTranspiler } from '../transpiler'
import type { EsbuildTranspilerOptions } from '../transpiler'

let initialized = false

const transpiler = (
options: Partial<Omit<EsbuildTranspilerOptions, 'esbuild'>> & {
wasmModule?: WebAssembly.Module
wasmURL?: string | URL
}
) => {
return createMiddleware(async (c, next) => {
if (!initialized) {
if (options.wasmModule) {
await esbuild.initialize({
wasmModule: options.wasmModule,
worker: false,
})
} else if (options.wasmURL) {
await esbuild.initialize({
wasmURL: options.wasmURL,
worker: false,
})
} else {
throw 'wasmModule or wasmURL option is required.'
}
initialized = true
}
return await baseTranspiler({
esbuild,
...options,
})(c, next)
})
}

export { transpiler as esbuildTranspiler }
Loading

0 comments on commit 1243fc5

Please sign in to comment.