Skip to content

Commit

Permalink
feat(dev-server): introduce env (#30)
Browse files Browse the repository at this point in the history
* feat(dev-server): introduce `env`

* add `deprecated` comment

* changeset
  • Loading branch information
yusukebe authored Nov 16, 2023
1 parent a073fe0 commit 12a8b15
Show file tree
Hide file tree
Showing 16 changed files with 676 additions and 257 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-waves-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/vite-dev-server': minor
---

feat: introduce `env`
49 changes: 17 additions & 32 deletions packages/dev-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,11 @@ bunx --bun vite
The options are below. `WorkerOptions` imported from `miniflare` are used for Cloudflare Bindings.

```ts
import type { MiniflareOptions, WorkerOptions } from 'miniflare'

export type DevServerOptions = {
entry?: string
injectClientScript?: boolean
exclude?: (string | RegExp)[]
cf?: Partial<
Omit<
WorkerOptions,
// We can ignore these properties:
'name' | 'script' | 'scriptPath' | 'modules' | 'modulesRoot' | 'modulesRules'
> &
Pick<
MiniflareOptions,
'cachePersist' | 'd1Persist' | 'durableObjectsPersist' | 'kvPersist' | 'r2Persist'
> & {
// Enable `env.ASSETS.fetch()` function for Cloudflare Pages.
assets?: boolean
}
>
env?: Env | EnvFunc
}
```
Expand Down Expand Up @@ -160,29 +145,39 @@ export default defineConfig({
})
```

## Cloudflare Bindings
### `env`

You can pass `ENV` variables to your application by setting the `env` option.
`ENV` values can be accessed using `c.env` in your Hono application.

Presets are available to define the `ENV`.

## `ENV` presets

You can use Cloudflare Bindings like variables, KV, D1, and others.
### Cloudflare Pages

You can use Cloudflare Pages `ENV` presets, which allow you to access bindings such as variables, KV, D1, and others.

```ts
import { getEnv } from '@hono/vite-dev-server/cloudflare-pages'

export default defineConfig({
plugins: [
devServer({
entry: 'src/index.ts',
cf: {
env: getEnv({
bindings: {
NAME: 'Hono',
},
kvNamespaces: ['MY_KV'],
},
}),
}),
],
})
```

These Bindings are emulated by Miniflare in the local.

### D1
#### D1

When using D1, your app will read `.mf/d1/DB/db.sqlite` which is generated automatically with the following configuration:

Expand Down Expand Up @@ -273,16 +268,6 @@ You can run the following command to build the client script.
vite build --mode client
```

## Notes

### Depending on Miniflare

`@hono/vite-dev-server` depends on `miniflare` for certain platforms you may want to run on. For example, if you want to run your applications on Node.js, the `miniflare` is not needed. However, it's necessary for Cloudflare Workers/Pages, which are important platforms for Hono. And `miniflare` is needed just for development; it will not be bundled for production.

### `cf` option with Bun

If properties are set in the `cf` option and it's running on Bun, an error will be thrown.

## Authors

- Yusuke Wada <https://github.com/yusukebe>
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { defineConfig } from 'vite'
import devServer, { defaultOptions } from '../src'
import { getEnv } from '../src/cloudflare-pages'

export default defineConfig({
plugins: [
devServer({
entry: './mock/worker.ts',
exclude: [...defaultOptions.exclude, '/app/.*'],
cf: {
env: getEnv({
bindings: {
NAME: 'Hono',
},
assets: true,
},
}),
}),
],
})
28 changes: 22 additions & 6 deletions packages/dev-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"module": "dist/index.js",
"type": "module",
"scripts": {
"test": "playwright test -c test/playwright.config.ts test/e2e.test.ts",
"build": "rimraf dist && tsup && publint",
"test:unit": "vitest --run",
"test:e2e": "playwright test -c e2e/playwright.config.ts e2e/e2e.test.ts",
"test": "yarn test:unit && yarn test:e2e",
"build": "rimraf dist && tsup --format esm,cjs --dts && publint",
"watch": "tsup --watch",
"prerelease": "yarn build && yarn test",
"release": "yarn publish"
Expand All @@ -17,14 +19,28 @@
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./cloudflare-pages": {
"types": "./dist/cloudflare-pages/index.d.ts",
"require": "./dist/cloudflare-pages/index.cjs",
"import": "./dist/cloudflare-pages/index.js"
}
},
"typesVersions": {
"*": {
"types": [
"./dist/types"
],
"cloudflare-pages": [
"./dist/cloudflare-pages/index.d.ts"
]
}
},
Expand All @@ -44,11 +60,11 @@
"glob": "^10.3.4",
"hono": "^3.5.8",
"playwright": "^1.39.0",
"publint": "^0.1.12",
"publint": "^0.2.5",
"rimraf": "^5.0.1",
"tsup": "^7.2.0",
"vite": "^4.4.9",
"vitest": "^0.31.4"
"vitest": "^0.34.6"
},
"engines": {
"node": ">=18.14.1"
Expand Down
17 changes: 17 additions & 0 deletions packages/dev-server/src/cloudflare-pages/cloudflare-pages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getEnv } from './cloudflare-pages.js'

describe('getEnv()', () => {
const envFunc = getEnv({
bindings: {
TOKEN: 'MY TOKEN',
},
})
it('Should return the value for bindings', async () => {
const env = await envFunc()
expect(env['TOKEN']).toBe('MY TOKEN')
})
it('Should contains ASSETS', async () => {
const env = await envFunc()
expect((env['ASSETS'] as { fetch: typeof fetch }).fetch).toBeDefined()
})
})
45 changes: 45 additions & 0 deletions packages/dev-server/src/cloudflare-pages/cloudflare-pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { MiniflareOptions, WorkerOptions } from 'miniflare'
import type { GetEnv } from '../types.js'

type Options = Partial<
Omit<
WorkerOptions,
// We can ignore these properties:
'name' | 'script' | 'scriptPath' | 'modules' | 'modulesRoot' | 'modulesRules'
> &
Pick<
MiniflareOptions,
'cachePersist' | 'd1Persist' | 'durableObjectsPersist' | 'kvPersist' | 'r2Persist'
> & {
// Enable `env.ASSETS.fetch()` function for Cloudflare Pages.
assets?: boolean
}
>

const nullScript = 'export default { fetch: () => new Response(null, { status: 404 }) };'

export const getEnv: GetEnv<Options> = (options) => async () => {
// Dynamic import Miniflare for environments like Bun.
const { Miniflare } = await import('miniflare')
const mf = new Miniflare({
modules: true,
script: nullScript,
...options,
})

const env = {
...(await mf.getBindings()),
// `env.ASSETS.fetch()` function for Cloudflare Pages.
ASSETS: {
async fetch(input: RequestInfo | URL, init?: RequestInit | undefined) {
try {
return await fetch(new Request(input, init))
} catch (e) {
console.error('Failed to execute ASSETS.fetch: ', e)
return new Response(null, { status: 500 })
}
},
},
}
return env
}
1 change: 1 addition & 0 deletions packages/dev-server/src/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getEnv } from './cloudflare-pages.js'
76 changes: 21 additions & 55 deletions packages/dev-server/src/dev-server.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import type http from 'http'
import { getRequestListener } from '@hono/node-server'
import type { Miniflare, MiniflareOptions, WorkerOptions } from 'miniflare'
import type { Plugin, ViteDevServer, Connect } from 'vite'
import { getEnv as cloudflarePagesGetEnv } from './cloudflare-pages/index.js'
import type { Env, Fetch, EnvFunc } from './types.js'

export type DevServerOptions = {
entry?: string
injectClientScript?: boolean
exclude?: (string | RegExp)[]
cf?: Partial<
Omit<
WorkerOptions,
// We can ignore these properties:
'name' | 'script' | 'scriptPath' | 'modules' | 'modulesRoot' | 'modulesRules'
> &
Pick<
MiniflareOptions,
'cachePersist' | 'd1Persist' | 'durableObjectsPersist' | 'kvPersist' | 'r2Persist'
> & {
// Enable `env.ASSETS.fetch()` function for Cloudflare Pages.
assets?: boolean
}
>
env?: Env | EnvFunc
} & {
/**
* @deprecated
* The `cf` option is maintained for backward compatibility, but it will be obsolete in the future.
* Instead, use the `env` option.
*/
cf?: Parameters<typeof cloudflarePagesGetEnv>[0]
}

export const defaultOptions: Required<Omit<DevServerOptions, 'cf'>> = {
export const defaultOptions: Required<Omit<DevServerOptions, 'env' | 'cf'>> = {
entry: './src/index.ts',
injectClientScript: true,
exclude: [
Expand All @@ -36,32 +31,11 @@ export const defaultOptions: Required<Omit<DevServerOptions, 'cf'>> = {
],
}

interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void
passThroughOnException(): void
}

type Fetch = (request: Request, env: {}, executionContext: ExecutionContext) => Promise<Response>

const nullScript = 'export default { fetch: () => new Response(null, { status: 404 }) };'

export function devServer(options?: DevServerOptions): Plugin {
const entry = options?.entry ?? defaultOptions.entry
const plugin: Plugin = {
name: '@hono/vite-dev-server',
configureServer: async (server) => {
let mf: undefined | Miniflare = undefined

// Dynamic import Miniflare for environments like Bun.
if (options?.cf) {
const { Miniflare } = await import('miniflare')
mf = new Miniflare({
modules: true,
script: nullScript,
...options.cf,
})
}

async function createMiddleware(server: ViteDevServer): Promise<Connect.HandleFunction> {
return async function (
req: http.IncomingMessage,
Expand All @@ -86,26 +60,18 @@ export function devServer(options?: DevServerOptions): Plugin {
}

getRequestListener(async (request) => {
let env = {}
if (mf) {
env = await mf.getBindings()
if (options?.cf?.assets) {
env = {
// `env.ASSETS.fetch()` function for Cloudflare Pages.
ASSETS: {
async fetch(input: RequestInfo | URL, init?: RequestInit | undefined) {
try {
return await fetch(new Request(input, init))
} catch (e) {
console.error('Failed to execute ASSETS.fetch: ', e)
return new Response(null, { status: 500 })
}
},
},
...env,
}
let env: Env = {}

if (options?.env) {
if (typeof options.env === 'function') {
env = await options.env()
} else {
env = options.env
}
} else if (options?.cf) {
env = await cloudflarePagesGetEnv(options.cf)()
}

const response = await app.fetch(request, env, {
waitUntil: async (fn) => fn,
passThroughOnException: () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/dev-server/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type Env = Record<string, unknown> | Promise<Record<string, unknown>>
export type EnvFunc = () => Env | Promise<Env>
export type GetEnv<Options> = (options: Options) => EnvFunc
export interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void
passThroughOnException(): void
}
export type Fetch = (
request: Request,
env: {},
executionContext: ExecutionContext
) => Promise<Response>
1 change: 1 addition & 0 deletions packages/dev-server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"module": "ES2022",
"target": "ES2022",
"types": [
"vitest/globals",
"vite/client"
]
},
Expand Down
8 changes: 8 additions & 0 deletions packages/dev-server/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig, configDefaults } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
exclude: [...configDefaults.exclude, 'e2e'],
},
})
Loading

0 comments on commit 12a8b15

Please sign in to comment.