diff --git a/.changeset/thick-flowers-punch.md b/.changeset/thick-flowers-punch.md new file mode 100644 index 0000000..429119d --- /dev/null +++ b/.changeset/thick-flowers-punch.md @@ -0,0 +1,5 @@ +--- +'@hono/vite-build': minor +--- + +Added the support for the AWS lambda / lambda edge diff --git a/packages/build/package.json b/packages/build/package.json index 7504f70..1e02fdd 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -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": { @@ -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" ] } }, @@ -103,4 +117,4 @@ "engines": { "node": ">=18.14.1" } -} \ No newline at end of file +} diff --git a/packages/build/src/adapter/aws-lambda/index.ts b/packages/build/src/adapter/aws-lambda/index.ts new file mode 100644 index 0000000..8002542 --- /dev/null +++ b/packages/build/src/adapter/aws-lambda/index.ts @@ -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 diff --git a/packages/build/src/adapter/lambda-edge/index.ts b/packages/build/src/adapter/lambda-edge/index.ts new file mode 100644 index 0000000..40e65cd --- /dev/null +++ b/packages/build/src/adapter/lambda-edge/index.ts @@ -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 \ No newline at end of file diff --git a/packages/build/src/base.ts b/packages/build/src/base.ts index ec62e01..11f65ea 100644 --- a/packages/build/src/base.ts +++ b/packages/build/src/base.ts @@ -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 @@ -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 @@ -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, diff --git a/packages/build/test/adapter.test.ts b/packages/build/test/adapter.test.ts index a8eb611..7dae5df 100644 --- a/packages/build/test/adapter.test.ts +++ b/packages/build/test/adapter.test.ts @@ -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' @@ -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, ]*}/) + + }) +})