Skip to content

Commit

Permalink
add adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe committed Sep 14, 2024
1 parent ef95b02 commit 34b18dc
Show file tree
Hide file tree
Showing 22 changed files with 404 additions and 59 deletions.
11 changes: 9 additions & 2 deletions packages/build/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@hono/build",
"name": "@hono/vite-build",
"description": "Vite plugin to build your Hono app",
"version": "0.0.0",
"types": "dist/index.d.ts",
Expand All @@ -23,6 +23,10 @@
"./bun": {
"types": "./dist/adapter/bun/index.d.ts",
"import": "./dist/adapter/bun/index.js"
},
"./node": {
"types": "./dist/adapter/node/index.d.ts",
"import": "./dist/adapter/node/index.js"
}
},
"typesVersions": {
Expand All @@ -32,6 +36,9 @@
],
"bun": [
"./dist/adapter/bun/index.d.ts"
],
"node": [
"./dist/adapter/node/index.d.ts"
]
}
},
Expand Down Expand Up @@ -61,4 +68,4 @@
"engines": {
"node": ">=18.14.1"
}
}
}
26 changes: 15 additions & 11 deletions packages/build/src/adapter/bun/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import type { Plugin } from 'vite'
import type { BuildOptions } from '../../base'
import { buildPlugin } from '../../base'
import type { BuildOptions } from '../../base.js'
import buildPlugin from '../../base.js'
import { serveStaticHook } from '../../entry/serve-static.js'

export type BunBuildOptions = {
staticRoot?: string | undefined
} & BuildOptions

export const bunBuildPlugin = (pluginOptions: BunBuildOptions): Plugin => {
const bunBuildPlugin = (pluginOptions?: BunBuildOptions): Plugin => {
return {
...buildPlugin({
...{
entryContentBeforeHook: async (appName, options) => {
// eslint-disable-next-line quotes
let code = "import { serveStatic } from 'hono/bun'\n"
for (const path of options?.staticPaths ?? []) {
code += `${appName}.use('${path}', serveStatic({ root: '${pluginOptions.staticRoot ?? './'}' }))\n`
}
return code
},
entryContentBeforeHooks: [
async (appName, options) => {
// eslint-disable-next-line quotes
let code = "import { serveStatic } from 'hono/bun'\n"
code += serveStaticHook(appName, {
filePaths: options?.staticPaths,
root: pluginOptions?.staticRoot,
})
return code
},
],
},
...pluginOptions,
}),
Expand Down
61 changes: 61 additions & 0 deletions packages/build/src/adapter/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { readdir, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import type { Plugin, ResolvedConfig } from 'vite'
import type { BuildOptions } from '../../base.js'
import buildPlugin, { defaultOptions } from '../../base.js'

export type CloudflarePagesBuildOptions = BuildOptions

const WORKER_JS_NAME = '_worker.js'
const ROUTES_JSON_NAME = '_routes.json'

type StaticRoutes = { version: number; include: string[]; exclude: string[] }

const bunBuildPlugin = (pluginOptions?: CloudflarePagesBuildOptions): Plugin => {
let config: ResolvedConfig
const staticPaths: string[] = []

return {
...buildPlugin({
...pluginOptions,
output: WORKER_JS_NAME,
}),
configResolved: async (resolvedConfig) => {
config = resolvedConfig
},
writeBundle: async () => {
const paths = await readdir(resolve(config.root, config.build.outDir), {
withFileTypes: true,
})
// If _routes.json already exists, don't create it
if (paths.some((p) => p.name === ROUTES_JSON_NAME)) {
return
} else {
paths.forEach((p) => {
if (p.isDirectory()) {
staticPaths.push(`/${p.name}/*`)
} else {
if (p.name === WORKER_JS_NAME) {
return
}
staticPaths.push(`/${p.name}`)
}
})
const staticRoutes: StaticRoutes = {
version: 1,
include: ['/*'],
exclude: staticPaths,
}
const path = resolve(
config.root,
pluginOptions?.outputDir ?? defaultOptions.outputDir,
'_routes.json'
)
await writeFile(path, JSON.stringify(staticRoutes))
}
},
name: '@hono/vite-build/cloudflare-pages',
}
}

export default bunBuildPlugin
16 changes: 16 additions & 0 deletions packages/build/src/adapter/cloudflare-workers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Plugin } from 'vite'
import type { BuildOptions } from '../../base.js'
import buildPlugin from '../../base.js'

export type CloudflareWorkersBuildOptions = BuildOptions

const nodeBuildPlugin = (pluginOptions?: CloudflareWorkersBuildOptions): Plugin => {
return {
...buildPlugin({
...pluginOptions,
}),
name: '@hono/vite-build/cloudflare-workers',
}
}

export default nodeBuildPlugin
40 changes: 40 additions & 0 deletions packages/build/src/adapter/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Plugin } from 'vite'
import type { BuildOptions } from '../../base.js'
import buildPlugin from '../../base.js'
import { serveStaticHook } from '../../entry/serve-static.js'

export type NodeBuildOptions = {
staticRoot?: string | undefined
} & BuildOptions

const nodeBuildPlugin = (pluginOptions?: NodeBuildOptions): Plugin => {
return {
...buildPlugin({
...{
entryContentBeforeHooks: [
async (appName, options) => {
// eslint-disable-next-line quotes
let code = "import { serveStatic } from '@hono/node-server/serve-static'\n"
code += serveStaticHook(appName, {
filePaths: options?.staticPaths,
root: pluginOptions?.staticRoot,
})
return code
},
],
entryContentAfterHooks: [
async (appName) => {
// eslint-disable-next-line quotes
let code = "import { serve } from '@hono/node-server'\n"
code += `serve(${appName})`
return code
},
],
},
...pluginOptions,
}),
name: '@hono/vite-build/node',
}
}

export default nodeBuildPlugin
55 changes: 31 additions & 24 deletions packages/build/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { builtinModules } from 'module'
import { readdir } from 'node:fs/promises'
import { readdirSync, statSync } from 'node:fs'
import { resolve } from 'url'
import type { ConfigEnv, Plugin, ResolvedConfig, UserConfig } from 'vite'
import { getEntryContent } from './entry.js'
import type { GetEntryContentOptions } from './entry.js'
import { getEntryContent } from './entry/index.js'
import type { GetEntryContentOptions } from './entry/index.js'

export type BuildOptions = {
/**
* @default ['./src/index.tsx', './app/server.ts']
* @default ['src/index.ts', './src/index.tsx', './app/server.ts']
*/
entry: string | string[]
entry?: string | string[]
/**
* @default './dist'
*/
output?: string
outputDir?: string
external?: string[]
/**
Expand All @@ -24,8 +25,10 @@ export type BuildOptions = {
} & Omit<GetEntryContentOptions, 'entry'>

export const defaultOptions: Required<
Omit<BuildOptions, 'entry' | 'entryContentAfterHook' | 'entryContentBeforeHook'>
Omit<BuildOptions, 'entryContentAfterHooks' | 'entryContentBeforeHooks'>
> = {
entry: ['src/index.ts', './src/index.tsx', './app/server.ts'],
output: 'index.js',
outputDir: './dist',
external: [],
minify: true,
Expand All @@ -36,12 +39,10 @@ export const defaultOptions: Required<
}
return false
},
staticPaths: [''],
staticPaths: [],
}

const EntryFileName = 'app.js'

export const buildPlugin = (options: BuildOptions): Plugin => {
const buildPlugin = (options: BuildOptions): Plugin => {
const virtualEntryId = 'virtual:build-entry-module'
const resolvedVirtualEntryId = '\0' + virtualEntryId
let config: ResolvedConfig
Expand All @@ -59,20 +60,24 @@ export const buildPlugin = (options: BuildOptions): Plugin => {
async load(id) {
if (id === resolvedVirtualEntryId) {
const staticPaths: string[] = []
const paths = await readdir(resolve(config.root, config.publicDir), {
withFileTypes: true,
})
paths.forEach((p) => {
if (p.isDirectory()) {
staticPaths.push(`/${p.name}/*`)
} else {
staticPaths.push(`/${p.name}`)
}
})
try {
statSync(config.publicDir)
const paths = readdirSync(resolve(config.root, config.publicDir), {
withFileTypes: true,
})
paths.forEach((p) => {
if (p.isDirectory()) {
staticPaths.push(`/${p.name}/*`)
} else {
staticPaths.push(`/${p.name}`)
}
})
} catch {}
const entry = options.entry ?? defaultOptions.entry
return await getEntryContent({
entry: Array.isArray(options.entry) ? options.entry : [options.entry],
entryContentBeforeHook: options.entryContentBeforeHook,
entryContentAfterHook: options.entryContentAfterHook,
entry: Array.isArray(entry) ? entry : [entry],
entryContentBeforeHooks: options.entryContentBeforeHooks,
entryContentAfterHooks: options.entryContentAfterHooks,
staticPaths,
})
}
Expand All @@ -94,11 +99,13 @@ export const buildPlugin = (options: BuildOptions): Plugin => {
external: [...builtinModules, /^node:/],
input: virtualEntryId,
output: {
entryFileNames: EntryFileName,
entryFileNames: options.output ?? defaultOptions.output,
},
},
},
}
},
}
}

export default buildPlugin
29 changes: 23 additions & 6 deletions packages/build/src/entry.ts → packages/build/src/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export type EntryContentHook = (

export type GetEntryContentOptions = {
entry: string[]
entryContentBeforeHook?: EntryContentHook
entryContentAfterHook?: EntryContentHook
entryContentBeforeHooks?: EntryContentHook[]
entryContentAfterHooks?: EntryContentHook[]
staticPaths?: string[]
}

Expand All @@ -31,6 +31,23 @@ export const getEntryContent = async (options: GetEntryContentOptions) => {
const globStr = normalizePaths(options.entry)
.map((e) => `'${e}'`)
.join(',')

const hooksToString = async (appName: string, hooks?: EntryContentHook[]) => {
if (hooks) {
const str = (
await Promise.all(
hooks.map((hook) => {
return hook(appName, {
staticPaths,
})
})
)
).join('\n')
return str
}
return ''
}

const appStr = `const modules = import.meta.glob([${globStr}], { import: 'default', eager: true })
let added = false
for (const [, app] of Object.entries(modules)) {
Expand All @@ -43,16 +60,16 @@ export const getEntryContent = async (options: GetEntryContentOptions) => {
if (!added) {
throw new Error("Can't import modules from [${globStr}]")
}`
const foo = `import { Hono } from 'hono'

const mainAppStr = `import { Hono } from 'hono'
const mainApp = new Hono()
${options.entryContentBeforeHook ? await options.entryContentBeforeHook('mainApp', { staticPaths }) : ''}
${await hooksToString('mainApp', options.entryContentBeforeHooks)}
${appStr}
${options.entryContentAfterHook ? await options.entryContentAfterHook('mainApp', { staticPaths }) : ''}
${await hooksToString('mainApp', options.entryContentAfterHooks)}
export default mainApp`
return foo
return mainAppStr
}
12 changes: 12 additions & 0 deletions packages/build/src/entry/serve-static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type ServeStaticHookOptions = {
filePaths?: string[]
root?: string
}

export const serveStaticHook = (appName: string, options: ServeStaticHookOptions) => {
let code = ''
for (const path of options.filePaths ?? []) {
code += `${appName}.use('${path}', serveStatic({ root: '${options.root ?? './'}' }))\n`
}
return code
}
3 changes: 3 additions & 0 deletions packages/build/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
import { defaultOptions } from './base.js'
export { defaultOptions }

import basePlugin from './base.js'
export default basePlugin
Loading

0 comments on commit 34b18dc

Please sign in to comment.