Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-flowers-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/vite-build': minor
---

Added the support for the AWS lambda / lambda edge
16 changes: 15 additions & 1 deletion packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
"./vercel": {
"types": "./dist/adapter/vercel/index.d.ts",
"import": "./dist/adapter/vercel/index.js"
},
"./lambda-edge": {
"types": "./dist/adapter/lambda-edge/index.d.ts",
"import": "./dist/adapter/lambda-edge/index.js"
},
"./aws-lambda": {
"types": "./dist/adapter/aws-lambda/index.d.ts",
"import": "./dist/adapter/aws-lambda/index.js"
}
},
"typesVersions": {
Expand Down Expand Up @@ -74,6 +82,12 @@
],
"vercel": [
"./dist/adapter/vercel/index.d.ts"
],
"lambda-edge": [
"./dist/adapter/lambda-edge/index.d.ts"
],
"aws-lambda": [
"./dist/adapter/aws-lambda/index.d.ts"
]
}
},
Expand Down Expand Up @@ -103,4 +117,4 @@
"engines": {
"node": ">=18.14.1"
}
}
}
33 changes: 33 additions & 0 deletions packages/build/src/adapter/aws-lambda/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Plugin } from 'vite'
import type { BuildOptions } from '../../base.js'
import buildPlugin from '../../base.js'

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

// NOTE: Lambda requires the file to be named with .mjs extension because the file has ES modules syntax.
const WORKER_JS_NAME = 'worker.mjs'

const lambdaBuildPlugin = (pluginOptions?: LambdaBuildOptions): Plugin => {
return {
...buildPlugin({
...{
entryContentAfterHooks: [
async (appName) => {
let code = "import { handle } from 'hono/aws-lambda'\n"
code += `export const handler = handle(${appName})`
return code
},
],
// stop copying public dir to dist
copyPublicDir: false,
},
...pluginOptions,
output: WORKER_JS_NAME,
}),
name: '@hono/vite-build/aws-lambda',
}
}

export default lambdaBuildPlugin
34 changes: 34 additions & 0 deletions packages/build/src/adapter/lambda-edge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 LambdaEdgeBuildOptions = {
staticRoot?: string | undefined
} & BuildOptions

// NOTE: Lambda Edge requires the file to be named with .mjs extension because the file has ES modules syntax.
const WORKER_JS_NAME = 'worker.mjs'

const lambdaEdgeBuildPlugin = (pluginOptions?: LambdaEdgeBuildOptions): Plugin => {
return {
...buildPlugin({
...{
entryContentAfterHooks: [
async (appName) => {
let code = "import { handle } from 'hono/lambda-edge'\n"
code += `export const handler = handle(${appName})`
return code
},
],
// stop copying public dir to dist
copyPublicDir: false,
},
...pluginOptions,
output: WORKER_JS_NAME,
}),
name: '@hono/vite-build/lambda-edge',
}
}

export default lambdaEdgeBuildPlugin
6 changes: 6 additions & 0 deletions packages/build/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type BuildOptions = {
*/
minify?: boolean
emptyOutDir?: boolean
copyPublicDir?: boolean
sourcemap?: boolean
apply?: ((this: void, config: UserConfig, env: ConfigEnv) => boolean) | undefined
} & Omit<GetEntryContentOptions, 'entry'>

Expand All @@ -36,6 +38,8 @@ export const defaultOptions: Required<
external: [],
minify: true,
emptyOutDir: false,
copyPublicDir: true,
sourcemap: false,
apply: (_config, { command, mode }) => {
if (command === 'build' && mode !== 'client') {
return true
Expand Down Expand Up @@ -114,6 +118,8 @@ const buildPlugin = (options: BuildOptions): Plugin => {
emptyOutDir: options?.emptyOutDir ?? defaultOptions.emptyOutDir,
minify: options?.minify ?? defaultOptions.minify,
ssr: true,
copyPublicDir: options?.copyPublicDir ?? defaultOptions.copyPublicDir,
sourcemap: options?.sourcemap ?? defaultOptions.sourcemap,
rollupOptions: {
external: [...builtinModules, /^node:/],
input: virtualEntryId,
Expand Down
67 changes: 67 additions & 0 deletions packages/build/test/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { build } from 'vite'
import { existsSync, readFileSync, rmSync } from 'node:fs'
import awsLambdaBuildPlugin from '../src/adapter/aws-lambda'
import bunBuildPlugin from '../src/adapter/bun'
import cloudflarePagesPlugin from '../src/adapter/cloudflare-pages'
import cloudflareWorkersPlugin from '../src/adapter/cloudflare-workers'
import denoBuildPlugin from '../src/adapter/deno'
import lambdaEdgeBuildPlugin from '../src/adapter/lambda-edge'
import netlifyFunctionsPlugin from '../src/adapter/netlify-functions'
import nodeBuildPlugin from '../src/adapter/node'
import vercelBuildPlugin from '../src/adapter/vercel'
Expand Down Expand Up @@ -351,3 +353,68 @@ describe('Build Plugin with Vercel Adapter', () => {
expect(outputJsClientJs).toContain("console.log('foo')")
})
})

describe('Build Plugin with Lambda Edge Adapter', () => {
const testDir = './test/mocks/app-static-files'
const entry = './src/server.ts'

afterEach(() => {
rmSync(`${testDir}/dist`, { recursive: true, force: true })
})

it('Should build the project correctly with the plugin', async () => {
const outputFile = `${testDir}/dist/worker.mjs`

await build({
root: testDir,
plugins: [
lambdaEdgeBuildPlugin({
entry,
minify: false,
}),
],
})

expect(existsSync(outputFile)).toBe(true)

const output = readFileSync(outputFile, 'utf-8')
expect(output).toContain('Hello World')
// check if the output contains the handler assignment
expect(output).toContain('const handler = handle(mainApp)')
// check if the output contains the export statement for the handler
expect(output).toMatch(/export {[a-zA-Z\n\r, ]*handler[a-zA-Z\n\r, ]*}/)
})
})

describe('Build Plugin with AWS Lambda Adapter', () => {
const testDir = './test/mocks/app-static-files'
const entry = './src/server.ts'

afterEach(() => {
rmSync(`${testDir}/dist`, { recursive: true, force: true })
})

it('Should build the project correctly with the AWS Lambda plugin', async () => {
const outputFile = `${testDir}/dist/worker.mjs`

await build({
root: testDir,
plugins: [
awsLambdaBuildPlugin({
entry,
minify: false,
}),
],
})

expect(existsSync(outputFile)).toBe(true)

const output = readFileSync(outputFile, 'utf-8')
expect(output).toContain('Hello World')
// check if the output contains the handler assignment
expect(output).toContain('const handler = handle(mainApp)')
// check if the output contains the export statement for the handler
expect(output).toMatch(/export {[a-zA-Z\n\r, ]*handler[a-zA-Z\n\r, ]*}/)

})
})