Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(build): Add netlify-functions adapter #218

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
61 changes: 61 additions & 0 deletions .changeset/tall-teachers-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
'@hono/vite-build': minor
---

Added a new Netlify Functions build adapter.

This adapter can be imported from `@hono/vite-build/netlify-functions` and will
compile your Hono app to comply with the requirements of the Netlify Functions
runtime.

* The default export will have the `hono/netlify` adapter applied to it.
* A `config` object will be exported, setting the function path to `'/*'` and
`preferStatic` to `true`.

Please note, this is for the Netlify Functions runtime, not the Netlify Edge
Functions runtime.

Example:

```ts
// vite.config.ts
import { defineConfig } from "vite";
import devServer from "@hono/vite-dev-server";
import build from "@hono/vite-build/netlify-functions";

export default defineConfig({
plugins: [
devServer({
entry: "./src/index.ts",
}),
build({
entry: "./src/index.ts",
output: "functions/server/index.js"
})
],
});
```

If you also have a `public/publish` directory for your assets that should be
published to the corresponding Netlify site, then after running a build, you
would end up with a directory structure like:

```
dist/
functions/
server/
index.js
publish/
robots.txt
....
```

then you can use a netlify.toml that looks like:

```toml
# https://ntl.fyi/file-based-build-config
[build]
command = "vite build"
functions = "dist/functions"
publish = "dist/publish"
```
4 changes: 3 additions & 1 deletion packages/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Here are the modules included:
- `@hono/vite-build/cloudflare-workers`
- `@hono/vite-build/bun`
- `@hono/vite-build/node`
- `@hono/vite-build/netlify-functions`

## Usage

Expand Down Expand Up @@ -36,6 +37,7 @@ import build from '@hono/vite-build/bun'
// import build from '@hono/vite-build/cloudflare-pages'
// import build from '@hono/vite-build/cloudflare-workers'
// import build from '@hono/vite-build/node'
// import build from '@hono/vite-build/netlify-functions'

export default defineConfig({
plugins: [
Expand All @@ -51,7 +53,7 @@ export default defineConfig({

### Build

Just run `vite build`.
Just run `vite build`.

```bash
npm exec vite build
Expand Down
7 changes: 7 additions & 0 deletions packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"types": "./dist/adapter/cloudflare-workers/index.d.ts",
"import": "./dist/adapter/cloudflare-workers/index.js"
},
"./netlify-functions": {
"types": "./dist/adapter/netlify-functions/index.d.ts",
"import": "./dist/adapter/netlify-functions/index.js"
},
"./deno": {
"types": "./dist/adapter/deno/index.d.ts",
"import": "./dist/adapter/deno/index.js"
Expand All @@ -57,6 +61,9 @@
],
"cloudflare-workers": [
"./dist/adapter/cloudflare-workers/index.d.ts"
],
"netlify-functions": [
"./dist/adapter/netlify-functions/index.d.ts"
]
}
},
Expand Down
21 changes: 21 additions & 0 deletions packages/build/src/adapter/netlify-functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Plugin } from 'vite'
import type { BuildOptions } from '../../base.js'
import buildPlugin from '../../base.js'

export type NetlifyFunctionsBuildOptions = BuildOptions

export default function netlifyFunctionsBuildPlugin(
pluginOptions?: NetlifyFunctionsBuildOptions
): Plugin {
return {
...buildPlugin({
...{
entryContentBeforeHooks: [() => 'import { handle } from "hono/netlify"'],
entryContentAfterHooks: [() => 'export const config = { path: "/*", preferStatic: true }'],
entryContentDefaultExportHook: (appName) => `export default handle(${appName})`,
},
...pluginOptions,
}),
name: '@hono/vite-build/netlify-functions',
}
}
7 changes: 5 additions & 2 deletions packages/build/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export type BuildOptions = {
} & Omit<GetEntryContentOptions, 'entry'>

export const defaultOptions: Required<
Omit<BuildOptions, 'entryContentAfterHooks' | 'entryContentBeforeHooks'>
Omit<
BuildOptions,
'entryContentAfterHooks' | 'entryContentBeforeHooks' | 'entryContentDefaultExportHook'
>
> = {
entry: ['src/index.ts', './src/index.tsx', './app/server.ts'],
output: 'index.js',
Expand All @@ -46,7 +49,6 @@ const buildPlugin = (options: BuildOptions): Plugin => {
const virtualEntryId = 'virtual:build-entry-module'
const resolvedVirtualEntryId = '\0' + virtualEntryId
let config: ResolvedConfig

const output = options.output ?? defaultOptions.output

return {
Expand Down Expand Up @@ -94,6 +96,7 @@ const buildPlugin = (options: BuildOptions): Plugin => {
entry: Array.isArray(entry) ? entry : [entry],
entryContentBeforeHooks: options.entryContentBeforeHooks,
entryContentAfterHooks: options.entryContentAfterHooks,
entryContentDefaultExportHook: options.entryContentDefaultExportHook,
staticPaths,
})
}
Expand Down
15 changes: 12 additions & 3 deletions packages/build/src/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export type GetEntryContentOptions = {
entry: string[]
entryContentBeforeHooks?: EntryContentHook[]
entryContentAfterHooks?: EntryContentHook[]
/**
* Explicitly specify the default export for the app. Make sure your export
* incorporates the app passed as the `appName` argument.
*
* @default `export default ${appName}`
*/
entryContentDefaultExportHook?: EntryContentHook
staticPaths?: string[]
}

Expand Down Expand Up @@ -67,7 +74,10 @@ export const getEntryContent = async (options: GetEntryContentOptions) => {
throw new Error("Can't import modules from [${globStr}]")
}`

const mainAppStr = `import { Hono } from 'hono'
const defaultExportHook =
options.entryContentDefaultExportHook ?? (() => 'export default mainApp')

return `import { Hono } from 'hono'
const mainApp = new Hono()

${await hooksToString('mainApp', options.entryContentBeforeHooks)}
Expand All @@ -76,6 +86,5 @@ ${appStr}

${await hooksToString('mainApp', options.entryContentAfterHooks)}

export default mainApp`
return mainAppStr
${await hooksToString('mainApp', [defaultExportHook])}`
}
38 changes: 38 additions & 0 deletions packages/build/test/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { existsSync, readFileSync, rmSync } from 'node:fs'
import bunBuildPlugin from '../src/adapter/bun'
import cloudflarePagesPlugin from '../src/adapter/cloudflare-pages'
import denoBuildPlugin from '../src/adapter/deno'
import netlifyFunctionsPlugin from '../src/adapter/netlify-functions'
import nodeBuildPlugin from '../src/adapter/node'

describe('Build Plugin with Bun Adapter', () => {
Expand Down Expand Up @@ -41,6 +42,43 @@ describe('Build Plugin with Bun Adapter', () => {
})
})

describe('Build Plugin with Netlify Functions 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/index.js`

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

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

const output = readFileSync(outputFile, 'utf-8')
expect(output).toContain('Hello World')
expect(output).toContain('{ path: "/*", preferStatic: true }')
expect(output).toContain('handle(mainApp)')

const outputFooTxt = readFileSync(`${testDir}/dist/foo.txt`, 'utf-8')
expect(outputFooTxt).toContain('foo')

const outputJsClientJs = readFileSync(`${testDir}/dist/js/client.js`, 'utf-8')
// eslint-disable-next-line quotes
expect(outputJsClientJs).toContain("console.log('foo')")
})
})

describe('Build Plugin with Cloudflare Pages Adapter', () => {
const testDir = './test/mocks/app-static-files'

Expand Down
3 changes: 2 additions & 1 deletion packages/dev-server/e2e-bun/mock/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ app.get('/', (c) => {
})

app.get('/with-nonce', (c) => {
c.header('content-security-policy', 'script-src-elem \'self\' \'nonce-ZMuLoN/taD7JZTUXfl5yvQ==\';')
// eslint-disable-next-line quotes -- allowing using double-quotes bc of embedded single quotes
c.header('content-security-policy', "script-src-elem 'self' 'nonce-ZMuLoN/taD7JZTUXfl5yvQ==';")
return c.html('<h1>Hello Vite!</h1>')
})

Expand Down
3 changes: 2 additions & 1 deletion packages/dev-server/e2e/mock/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ app.get('/', (c) => {
})

app.get('/with-nonce', (c) => {
c.header('content-security-policy', 'script-src-elem \'self\' \'nonce-ZMuLoN/taD7JZTUXfl5yvQ==\';')
// eslint-disable-next-line quotes -- allowing using double-quotes bc of embedded single quotes
c.header('content-security-policy', "script-src-elem 'self' 'nonce-ZMuLoN/taD7JZTUXfl5yvQ==';")
return c.html('<h1>Hello Vite!</h1>')
})

Expand Down
2 changes: 1 addition & 1 deletion packages/ssg/test/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ app.get('/', (c) => {
})

app.get('/dynamic-import', async (c) => {
// @ts-expect-error
// @ts-expect-error this is a test
const module = await import('./sample.js')
return c.text('Dynamic import works: ' + module.default)
})
Expand Down
Loading