Skip to content

Commit

Permalink
feat(cloudflare-pages): generate _routes.json (#121)
Browse files Browse the repository at this point in the history
* feat(cloudflare-pages): detect static files automatically

* add changeset

* Update packages/cloudflare-pages/src/cloudflare-pages.ts

Co-authored-by: ryu <[email protected]>

* `readdir` in the `writeBundle`

* add `serveStaticDir` option and fixed the order

* don't set `serveStatc()` and fixed the order for `_routes.json`

---------

Co-authored-by: yudai-nkt <[email protected]>
Co-authored-by: ryu <[email protected]>
  • Loading branch information
3 people authored May 6, 2024
1 parent 9bed7aa commit cf3afbf
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-clouds-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/vite-cloudflare-pages': minor
---

feat: detect static files automatically
45 changes: 41 additions & 4 deletions packages/cloudflare-pages/src/cloudflare-pages.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { builtinModules } from 'module'
import type { Plugin, UserConfig } from 'vite'
import { readdir, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import type { Plugin, UserConfig, ResolvedConfig } from 'vite'
import { getEntryContent } from './entry.js'

type CloudflarePagesOptions = {
/**
* @default ['./src/index.tsx', './app/server.ts']
*/
*/
entry?: string | string[]
/**
* @default './dist'
Expand All @@ -19,20 +21,29 @@ type CloudflarePagesOptions = {
emptyOutDir?: boolean
}

export const defaultOptions: Required<CloudflarePagesOptions> = {
export const defaultOptions: Required<Omit<CloudflarePagesOptions, 'serveStaticDir'>> = {
entry: ['./src/index.tsx', './app/server.ts'],
outputDir: './dist',
external: [],
minify: true,
emptyOutDir: false,
}

const WORKER_JS_NAME = '_worker.js'

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

export const cloudflarePagesPlugin = (options?: CloudflarePagesOptions): Plugin => {
const virtualEntryId = 'virtual:cloudflare-pages-entry-module'
const resolvedVirtualEntryId = '\0' + virtualEntryId
let config: ResolvedConfig
const staticPaths: string[] = []

return {
name: '@hono/vite-cloudflare-pages',
configResolved: async (resolvedConfig) => {
config = resolvedConfig
},
resolveId(id) {
if (id === virtualEntryId) {
return resolvedVirtualEntryId
Expand All @@ -49,6 +60,32 @@ export const cloudflarePagesPlugin = (options?: CloudflarePagesOptions): Plugin
})
}
},
writeBundle: async () => {
const paths = await readdir(resolve(config.root, config.build.outDir), {
withFileTypes: true,
})
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,
options?.outputDir ?? defaultOptions.outputDir,
'_routes.json'
)
await writeFile(path, JSON.stringify(staticRoutes))
},
config: async (): Promise<UserConfig> => {
return {
ssr: {
Expand All @@ -64,7 +101,7 @@ export const cloudflarePagesPlugin = (options?: CloudflarePagesOptions): Plugin
external: [...builtinModules, /^node:/],
input: virtualEntryId,
output: {
entryFileNames: '_worker.js',
entryFileNames: WORKER_JS_NAME,
},
},
},
Expand Down
3 changes: 0 additions & 3 deletions packages/cloudflare-pages/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,8 @@ export const getEntryContent = async (options: Options) => {
`

return `import { Hono } from 'hono'
import { serveStatic } from 'hono/cloudflare-pages'
const worker = new Hono()
worker.get('/favicon.ico', serveStatic())
worker.get('/static/*', serveStatic())
${appStr}
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare-pages/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import { cloudflarePagesPlugin } from './cloudflare-pages.js'
import { cloudflarePagesPlugin, defaultOptions } from './cloudflare-pages.js'
export { defaultOptions }
export default cloudflarePagesPlugin
41 changes: 26 additions & 15 deletions packages/cloudflare-pages/test/cloudflare-pages.test.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,70 @@
import * as fs from 'node:fs'
import path from 'path'
import { build } from 'vite'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { describe, it, expect, afterAll } from 'vitest'
import cloudflarePagesPlugin from '../src/index'

describe('cloudflarePagesPlugin', () => {
const testDir = './test-project'
const testDir = './test/project'
const entryFile = `${testDir}/app/server.ts`

beforeAll(() => {
fs.mkdirSync(path.dirname(entryFile), { recursive: true })
fs.writeFileSync(entryFile, 'export default { fetch: () => new Response("Hello World") }')
})

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

it('Should build the project correctly with the plugin', async () => {
const outputFile = `${testDir}/dist/_worker.js`
const routesFile = `${testDir}/dist/_routes.json`

expect(fs.existsSync(entryFile)).toBe(true)

await build({
root: testDir,
plugins: [cloudflarePagesPlugin()],
build: {
emptyOutDir: true,
},
})

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

const output = fs.readFileSync(outputFile, 'utf-8')
expect(output).toContain('Hello World')

const routes = fs.readFileSync(routesFile, 'utf-8')
expect(routes).toContain(
'{"version":1,"include":["/*"],"exclude":["/favicon.ico","/static/*"]}'
)
})

it('Should build the project correctly with custom output directory', async () => {
const outputFile = `${testDir}/customDir/_worker.js`
const routesFile = `${testDir}/customDir/_routes.json`

afterAll(() => {
fs.rmSync(`${testDir}/customDir/`, { recursive: true, force: true })
})

expect(fs.existsSync(entryFile)).toBe(true)

await build({
root: testDir,
plugins: [cloudflarePagesPlugin({
outputDir: 'customDir',
})],
plugins: [
cloudflarePagesPlugin({
outputDir: 'customDir',
}),
],
build: {
emptyOutDir: true,
},
})

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

const output = fs.readFileSync(outputFile, 'utf-8')
expect(output).toContain('Hello World')

const routes = fs.readFileSync(routesFile, 'utf-8')
expect(routes).toContain(
'{"version":1,"include":["/*"],"exclude":["/favicon.ico","/static/*"]}'
)
})
})
3 changes: 3 additions & 0 deletions packages/cloudflare-pages/test/project/app/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
fetch: () => new Response('Hello World'),
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo

0 comments on commit cf3afbf

Please sign in to comment.