From 92f1e721d4a1e1846c3fdfa121d63fafdaf8f4b3 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 24 Jan 2024 09:26:51 +0900 Subject: [PATCH] feat: introduce SSG plugin (#59) * feat: introduce SSG plugin * changeset * Revert "changeset" This reverts commit 3376a01dfdae1986399db17b38c5108afaf4bf85. * changeset * typo --- .changeset/shiny-dragons-protect.md | 5 + ...fare-pages.yml => ci-cloudflare-pages.yml} | 0 .github/workflows/ci-ssg.yml | 28 ++++++ .gitignore | 1 + packages/ssg/README.md | 92 +++++++++++++++++++ packages/ssg/package.json | 56 +++++++++++ packages/ssg/src/index.ts | 2 + packages/ssg/src/ssg.ts | 76 +++++++++++++++ packages/ssg/test/app.ts | 9 ++ packages/ssg/test/ssg.test.ts | 40 ++++++++ packages/ssg/tsconfig.build.json | 12 +++ packages/ssg/tsconfig.json | 14 +++ packages/ssg/tsup.config.ts | 17 ++++ yarn.lock | 22 +++++ 14 files changed, 374 insertions(+) create mode 100644 .changeset/shiny-dragons-protect.md rename .github/workflows/{ci-cloudfare-pages.yml => ci-cloudflare-pages.yml} (100%) create mode 100644 .github/workflows/ci-ssg.yml create mode 100644 packages/ssg/README.md create mode 100644 packages/ssg/package.json create mode 100644 packages/ssg/src/index.ts create mode 100644 packages/ssg/src/ssg.ts create mode 100644 packages/ssg/test/app.ts create mode 100644 packages/ssg/test/ssg.test.ts create mode 100644 packages/ssg/tsconfig.build.json create mode 100644 packages/ssg/tsconfig.json create mode 100644 packages/ssg/tsup.config.ts diff --git a/.changeset/shiny-dragons-protect.md b/.changeset/shiny-dragons-protect.md new file mode 100644 index 0000000..1a28f64 --- /dev/null +++ b/.changeset/shiny-dragons-protect.md @@ -0,0 +1,5 @@ +--- +'@hono/vite-ssg': patch +--- + +Initial release diff --git a/.github/workflows/ci-cloudfare-pages.yml b/.github/workflows/ci-cloudflare-pages.yml similarity index 100% rename from .github/workflows/ci-cloudfare-pages.yml rename to .github/workflows/ci-cloudflare-pages.yml diff --git a/.github/workflows/ci-ssg.yml b/.github/workflows/ci-ssg.yml new file mode 100644 index 0000000..768a2ce --- /dev/null +++ b/.github/workflows/ci-ssg.yml @@ -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 diff --git a/.gitignore b/.gitignore index da78e82..98bb071 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ package-lock.json sandbox test-results playwright-report +.hono # yarn .yarn/* diff --git a/packages/ssg/README.md b/packages/ssg/README.md new file mode 100644 index 0000000..714793c --- /dev/null +++ b/packages/ssg/README.md @@ -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 + +## License + +MIT diff --git a/packages/ssg/package.json b/packages/ssg/package.json new file mode 100644 index 0000000..6fd7a33 --- /dev/null +++ b/packages/ssg/package.json @@ -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 (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" + } +} diff --git a/packages/ssg/src/index.ts b/packages/ssg/src/index.ts new file mode 100644 index 0000000..a5b13e2 --- /dev/null +++ b/packages/ssg/src/index.ts @@ -0,0 +1,2 @@ +import { ssgBuild } from './ssg.js' +export default ssgBuild diff --git a/packages/ssg/src/ssg.ts b/packages/ssg/src/ssg.ts new file mode 100644 index 0000000..a6609cc --- /dev/null +++ b/packages/ssg/src/ssg.ts @@ -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 = { + 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(`Build 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, + }, + } + }, + } +} diff --git a/packages/ssg/test/app.ts b/packages/ssg/test/app.ts new file mode 100644 index 0000000..acd0ffc --- /dev/null +++ b/packages/ssg/test/app.ts @@ -0,0 +1,9 @@ +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/', (c) => { + return c.html('

Hello!

') +}) + +export default app diff --git a/packages/ssg/test/ssg.test.ts b/packages/ssg/test/ssg.test.ts new file mode 100644 index 0000000..c103dfd --- /dev/null +++ b/packages/ssg/test/ssg.test.ts @@ -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('

Hello!

') + }) +}) diff --git a/packages/ssg/tsconfig.build.json b/packages/ssg/tsconfig.build.json new file mode 100644 index 0000000..563f9b1 --- /dev/null +++ b/packages/ssg/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src/" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/packages/ssg/tsconfig.json b/packages/ssg/tsconfig.json new file mode 100644 index 0000000..14d4f5b --- /dev/null +++ b/packages/ssg/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src", + "test" + ], + "compilerOptions": { + "module": "ES2022", + "target": "ES2022", + "types": [ + "vite/client" + ] + }, +} \ No newline at end of file diff --git a/packages/ssg/tsup.config.ts b/packages/ssg/tsup.config.ts new file mode 100644 index 0000000..db5ddae --- /dev/null +++ b/packages/ssg/tsup.config.ts @@ -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', +}) diff --git a/yarn.lock b/yarn.lock index aafa0be..212feb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,6 +775,21 @@ __metadata: languageName: unknown linkType: soft +"@hono/vite-ssg@workspace:packages/ssg": + version: 0.0.0-use.local + resolution: "@hono/vite-ssg@workspace:packages/ssg" + dependencies: + 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" + languageName: unknown + linkType: soft + "@humanwhocodes/config-array@npm:^0.11.13": version: 0.11.13 resolution: "@humanwhocodes/config-array@npm:0.11.13" @@ -3357,6 +3372,13 @@ __metadata: languageName: unknown linkType: soft +"hono@npm:4.0.0-rc.2": + version: 4.0.0-rc.2 + resolution: "hono@npm:4.0.0-rc.2" + checksum: 33cf82a066b33d64de846b2325d08264b466b906ac4146e0cd53d4c1ef2e0c7b97e44afe4581ff86aa4af73c12597f62f4ae209a666d60a23b9d855a07ef9dde + languageName: node + linkType: hard + "hono@npm:^3.11.7": version: 3.11.7 resolution: "hono@npm:3.11.7"