Skip to content

Commit

Permalink
feat: introduce SSG plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe committed Jan 23, 2024
1 parent e2e6ef0 commit b7aeb73
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 0 deletions.
File renamed without changes.
28 changes: 28 additions & 0 deletions .github/workflows/ci-ssg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ci-ssg
on:
push:
branches: [main]
paths:
- 'packages/ssg/**'
pull_request:
branches: ['*']
paths:
- 'packages/ssg/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/ssg
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- uses: pnpm/action-setup@v2
with:
version: 8
- run: yarn install
- run: yarn build
- run: yarn test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package-lock.json
sandbox
test-results
playwright-report
.hono

# yarn
.yarn/*
Expand Down
92 changes: 92 additions & 0 deletions packages/ssg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# @hono/vite-ssg

`@hono/vite-ssg` is a Vite plugin to generate a static site from your Hono application.

## Usage

### Installation

You can install `vite` and `@hono/vite-ssg` via npm.

```plain
npm i -D vite @hono/vite-ssg
```

Or you can install them with Bun.

```plain
bun add vite @hono/vite-ssg
```

### Settings

Add `"type": "module"` to your `package.json`. Then, create `vite.config.ts` and edit it.

```ts
import { defineConfig } from 'vite'
import ssg from '@hono/vite-ssg'

export default defineConfig({
plugins: [ssg()],
})
```

### Build

Just run `vite build`.

```text
npm exec vite build
```

Or

```text
bunx --bun vite build
```

### Deploy to Cloudflare Pages

Run the `wrangler` command.

```text
wrangler pages deploy ./dist
```

## Options

The options are below.

```ts
type BuildConfig = {
outputDir?: string
publicDir?: string
}

type SSGOptions = {
entry?: string
rootDir?: string
build?: BuildConfig
}
```
Default values:
```ts
const defaultOptions = {
entry: './src/index.tsx',
tempDir: '.hono',
build: {
outputDir: '../dist',
publicDir: '../public',
},
}
```

## Authors

- Yusuke Wada <https://github.com/yusukebe>

## License

MIT
56 changes: 56 additions & 0 deletions packages/ssg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@hono/vite-ssg",
"description": "Vite plugin to generate a static site from your Hono application",
"version": "0.0.0",
"types": "dist/index.d.ts",
"module": "dist/index.js",
"type": "module",
"scripts": {
"test": "vitest --run",
"build": "rimraf dist && tsup && publint",
"watch": "tsup --watch",
"prerelease": "yarn build",
"release": "yarn publish"
},
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"typesVersions": {
"*": {
"types": [
"./dist/types"
]
}
},
"author": "Yusuke Wada <[email protected]> (https://github.com/yusukebe)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/honojs/vite-plugins.git"
},
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"homepage": "https://github.com/honojs/vite-plugins",
"devDependencies": {
"hono": "4.0.0-rc.2",
"publint": "^0.1.12",
"rimraf": "^5.0.1",
"tsup": "^7.2.0",
"vite": "^5.0.12",
"vitest": "^1.2.1"
},
"peerDependencies": {
"hono": ">=4.0.0"
},
"engines": {
"node": ">=18.14.1"
}
}
2 changes: 2 additions & 0 deletions packages/ssg/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { ssgBuild } from './ssg.js'
export default ssgBuild
76 changes: 76 additions & 0 deletions packages/ssg/src/ssg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Hono } from 'hono'
import { toSSG } from 'hono/ssg'
import type { Plugin } from 'vite'
import { createServer } from 'vite'

type BuildConfig = {
outputDir?: string
publicDir?: string
}

type SSGOptions = {
entry?: string
tempDir?: string
build?: BuildConfig
}

export const defaultOptions: Required<SSGOptions> = {
entry: './src/index.tsx',
tempDir: '.hono',
build: {
outputDir: '../dist',
publicDir: '../public',
},
}

export const ssgBuild = (options?: SSGOptions): Plugin => {
const entry = options?.entry ?? defaultOptions.entry
const tempDir = options?.tempDir ?? defaultOptions.tempDir
return {
name: '@hono/vite-ssg',
apply: 'build',
config: async () => {
// Create a server to load the module
const server = await createServer({
plugins: [],
build: { ssr: true },
})
const module = await server.ssrLoadModule(entry)
server.close()

const app = module['default'] as Hono

if (!app) {
throw new Error(`Failed to find a named export "default" from ${entry}`)
}

console.log(`Built files into temp directory: ${tempDir}`)

const result = await toSSG(app, fs, { dir: tempDir })

if (!result.success) {
throw result.error
}

if (result.files) {
for (const file of result.files) {
console.log(`Generated: ${file}`)
}
}

return {
root: tempDir,
publicDir: options?.build?.publicDir ?? defaultOptions.build.publicDir,
build: {
outDir: options?.build?.outputDir ?? defaultOptions.build.outputDir,
rollupOptions: {
input: result.files ? [...result.files] : [],
},
emptyOutDir: true,
},
}
},
}
}
9 changes: 9 additions & 0 deletions packages/ssg/test/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
return c.html('<html><body><h1>Hello!</h1></body></html>')
})

export default app
40 changes: 40 additions & 0 deletions packages/ssg/test/ssg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs'
import path from 'node:path'
import { build } from 'vite'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import ssgPlugin from '../src/index'

describe('ssgPlugin', () => {
const testDir = './test-project'
const entryFile = './test/app.ts'
const outputFile = path.resolve(testDir, 'dist', 'index.html')

beforeAll(() => {
fs.mkdirSync(testDir, { recursive: true })
})

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

it('Should generate the content correctly with the plugin', async () => {
expect(fs.existsSync(entryFile)).toBe(true)

await build({
plugins: [
ssgPlugin({
entry: entryFile,
tempDir: path.resolve(testDir, '.hono'),
}),
],
build: {
emptyOutDir: true,
},
})

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

const output = fs.readFileSync(outputFile, 'utf-8')
expect(output).toBe('<html><body><h1>Hello!</h1></body></html>')
})
})
12 changes: 12 additions & 0 deletions packages/ssg/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src/"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts"
]
}
14 changes: 14 additions & 0 deletions packages/ssg/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src",
"test"
],
"compilerOptions": {
"module": "ES2022",
"target": "ES2022",
"types": [
"vite/client"
]
},
}
17 changes: 17 additions & 0 deletions packages/ssg/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { glob } from 'glob'
import { defineConfig } from 'tsup'

const entryPoints = glob.sync('./src/**/*.+(ts|tsx|json)', {
ignore: ['./src/**/*.test.+(ts|tsx)'],
})

export default defineConfig({
entry: entryPoints,
dts: true,
tsconfig: './tsconfig.build.json',
splitting: false,
minify: false,
format: ['esm'],
bundle: false,
platform: 'node',
})
Loading

0 comments on commit b7aeb73

Please sign in to comment.