Open18

HonoXでPark UIが動くか試す

babiebabie
$ pnpm create hono@latest
create-hono version 0.11.0
? Target directory honox-with-park-ui
? Which template do you want to use? x-basic
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? pnpm
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd honox-with-park-ui
babiebabie

jsx/jsxImportSourceを確認。

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "lib": [
      "ESNext",
      "DOM"
    ],
    "types": [
      "vite/client",
      "@cloudflare/workers-types"
    ],
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  },
  "include": [
    "**/*.ts",
    "**/*.tsx"
  ]
}

とりあえず、純正Reactじゃなくhono/jsxで動くか試す。

babiebabie
$ pnpm install -D @pandacss/dev
 WARN  2 deprecated subdependencies found: rollup-plugin-inject@3.0.2, sourcemap-codec@1.4.8
Packages: +114 -1
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Progress: resolved 387, reused 264, downloaded 31, added 114, done

devDependencies:
+ @pandacss/dev 0.44.0

Done in 16.7s
babiebabie
$ pnpm panda init --postcss
🐼 info [cli] Panda v0.44.0

🐼 info [init:postcss] creating postcss config file: `postcss.config.cjs`
🐼 info [init:config] creating panda config file: `panda.config.ts`

🚀 Thanks for choosing Panda to write your css.

You are set up to start using Panda!

✔️ `styled-system/css`: the css function to author styles
✔️ `styled-system/tokens`: the css variables and js function to query your tokens
✔️ `styled-system/patterns`: functions to implement and apply common layout patterns

╭────────────────────────── 🐼 Sweet! ✨ ──────────────────────────╮
│                                                                  │
│                                                                  │
│   Next steps:                                                    │
│                                                                  │
│   [1] Create a `index.css` file in your project that contains:   │
│                                                                  │
│       @layer reset, base, tokens, recipes, utilities;            │
│                                                                  │
│                                                                  │
│   [2] Import the `index.css` file at the root of your project.   │
│                                                                  │
│                                                                  │
╰──────────────────────────────────────────────────────────────────╯
🐼 info [hrtime] ✨ Panda initialized (357.65ms)
babiebabie

package.jsonにscripts.prepare部分を追加。

package.json
{
  "name": "basic",
  "type": "module",
  "scripts": {
    "prepare": "panda codegen",
    "dev": "vite",
    "build": "vite build --mode client && vite build",
    "preview": "wrangler pages dev",
    "deploy": "$npm_execpath run build && wrangler pages deploy"
  },
  "private": true,
  "dependencies": {
    "hono": "^4.5.3",
    "honox": "^0.1.23"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20240529.0",
    "@hono/vite-cloudflare-pages": "^0.4.2",
    "@hono/vite-dev-server": "^0.13.1",
    "@pandacss/dev": "^0.44.0",
    "vite": "^5.2.12",
    "wrangler": "^3.57.2"
  }
}
babiebabie

includeオプションをHonoXのディレクトリ規約に合わせて変更。

panda.config.ts
import { defineConfig } from "@pandacss/dev";

export default defineConfig({
  // Whether to use css reset
  preflight: true,

  // Where to look for your css declarations
  include: ["./app/**/*.{js,jsx,ts,tsx}"],

  // Files to exclude
  exclude: [],

  // Useful for theme customization
  theme: {
    extend: {},
  },

  // The output directory for your css system
  outdir: "styled-system",
});
babiebabie
app/routes/index.css
@layer reset, base, tokens, recipes, utilities;
app/routes/_renderer.tsx
diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx
index 03b150c..94e2e9a 100644
--- a/app/routes/_renderer.tsx
+++ b/app/routes/_renderer.tsx
@@ -1,6 +1,6 @@
-import { Style } from 'hono/css'
 import { jsxRenderer } from 'hono/jsx-renderer'
 import { Script } from 'honox/server'
+import indexCss from './index.css?url'

 export default jsxRenderer(({ children, title }) => {
   return (
@@ -11,7 +11,7 @@ export default jsxRenderer(({ children, title }) => {
         <title>{title}</title>
         <link rel="icon" href="/favicon.ico" />
         <Script src="/app/client.ts" async />
-        <Style />
+        <link rel="stylesheet" href={indexCss} />
       </head>
       <body>{children}</body>
     </html>
app/routes/index.tsx
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index fc34955..584c847 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -1,18 +1,16 @@
-import { css } from 'hono/css'
 import { createRoute } from 'honox/factory'
 import Counter from '../islands/counter'
-
-const className = css`
-  font-family: sans-serif;
-`
+import { css } from '../../styled-system/css'

 export default createRoute((c) => {
   const name = c.req.query('name') ?? 'Hono'
   return c.render(
-    <div class={className}>
-      <h1>Hello, {name}!</h1>
+    <div>
+      <h1 class={css({ fontSize: '2xl', fontWeight: 'bold' })}>
+        Hello, {name}!
+      </h1>
       <Counter />
     </div>,
-    { title: name }
+    { title: name },
   )
 })
$ pnpm dev

ここまでで、Panda CSSは動いた。

babiebabie

Ark UIを入れる。Reactコンポーネントがhono/jsxで動けばいいが……

$ pnpm add @ark-ui/react
 WARN  2 deprecated subdependencies found: rollup-plugin-inject@3.0.2, sourcemap-codec@1.4.8
Packages: +81
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 468, reused 304, downloaded 72, added 81, done

dependencies:
+ @ark-ui/react 3.6.2

Done in 14.2s
babiebabie

Park UIを入れる。

pnpm add -D @park-ui/panda-preset
 WARN  2 deprecated subdependencies found: rollup-plugin-inject@3.0.2, sourcemap-codec@1.4.8
Packages: +65
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 533, reused 377, downloaded 64, added 65, done

devDependencies:
+ @park-ui/panda-preset 0.42.0

Done in 8.9s
babiebabie

Park UIのプリセットを使うようにPanda CSSの設定ファイルに書く。

panda.config.ts
diff --git a/panda.config.ts b/panda.config.ts
index 0ba29b6..af3f37a 100644
--- a/panda.config.ts
+++ b/panda.config.ts
@@ -4,12 +4,16 @@ export default defineConfig({
   // Whether to use css reset
   preflight: true,

+  presets: ['@pandacss/preset-base', '@park-ui/panda-preset'],
+
   // Where to look for your css declarations
   include: ['./app/**/*.{js,jsx,ts,tsx}'],

   // Files to exclude
   exclude: [],

+  jsxFramework: 'react',
+
   // Useful for theme customization
   theme: {
     extend: {},
babiebabie

パス・エイリアス(~/*をプロジェクトルート、@/*appディレクトリにマッピング)とパッケージ・エイリアス(reacthono/jsxreact-domhono/jsx/domにマッピング)の設定をしていく。

パス・エイリアスに必要なパッケージをインストール。

$ pnpm add -D vite-tsconfig-paths

パッケージ・エイリアスを設定する前に、TypeScript用の型ファイルを入れておく。

$ pnpm add -D @types/react @types/react-dom

tsconfig.jsonにパス・エイリアスとパッケージ・エイリアス(baseUrl & paths)を設定。
(フォーマットかかって改行が変わってるけど許せ)

tsconfig.json
diff --git a/tsconfig.json b/tsconfig.json
index 485d881..234682a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,22 +2,21 @@
   "compilerOptions": {
     "target": "ESNext",
     "module": "ESNext",
-    "moduleResolution": "Bundler",
+    "moduleResolution": "bundler",
+    "moduleDetection": "auto",
     "strict": true,
     "skipLibCheck": true,
-    "lib": [
-      "ESNext",
-      "DOM"
-    ],
-    "types": [
-      "vite/client",
-      "@cloudflare/workers-types"
-    ],
+    "lib": ["ESNext", "DOM"],
+    "types": ["vite/client", "@cloudflare/workers-types"],
     "jsx": "react-jsx",
-    "jsxImportSource": "hono/jsx"
+    "jsxImportSource": "hono/jsx",
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./*"],
+      "@/*": ["./app/*"],
+      "react": ["./node_modules/hono/jsx"],
+      "react-dom": ["./node_modules/hono/jsx/dom"]
+    }
   },
-  "include": [
-    "**/*.ts",
-    "**/*.tsx"
-  ]
+  "include": ["**/*.ts", "**/*.tsx"]
 }

Viteの設定ファイルでもパス・エイリアス(tsconfigPathsプラグイン)を有効に。
あと、パッケージ・エイリアス(resolve.alias)も設定しておく。

vite.config.ts
diff --git a/vite.config.ts b/vite.config.ts
index 88b8cbd..891b6b5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,7 +2,18 @@ import pages from '@hono/vite-cloudflare-pages'
 import adapter from '@hono/vite-dev-server/cloudflare'
 import honox from 'honox/vite'
 import { defineConfig } from 'vite'
+import tsconfigPaths from 'vite-tsconfig-paths'

 export default defineConfig({
-  plugins: [honox({ devServer: { adapter } }), pages()]
+  plugins: [
+    tsconfigPaths({ root: './' }),
+    honox({ devServer: { adapter } }),
+    pages(),
+  ],
+  resolve: {
+    alias: {
+      'react': 'hono/jsx', // prettier-ignore
+      'react-dom': 'hono/jsx/dom',
+    },
+  },
 })

package.jsonでパッケージ・エイリアスの設定。
hono/jsxreactとして使う)

package.json
diff --git a/package.json b/package.json
index 7790d54..e2bd2fb 100644
--- a/package.json
+++ b/package.json
@@ -10,15 +10,22 @@
   },
   "private": true,
   "dependencies": {
+    "@ark-ui/react": "^3.6.2",
     "hono": "^4.5.3",
-    "honox": "^0.1.23"
+    "honox": "^0.1.23",
+    "react": "npm:hono/jsx",
+    "react-dom": "npm:hono/jsx/dom"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^4.20240529.0",
     "@hono/vite-cloudflare-pages": "^0.4.2",
     "@hono/vite-dev-server": "^0.13.1",
     "@pandacss/dev": "^0.44.0",
+    "@park-ui/panda-preset": "^0.42.0",
+    "@types/react": "18.3.0",
+    "@types/react-dom": "18.3.0",
     "vite": "^5.2.12",
+    "vite-tsconfig-paths": "^4.3.2",
     "wrangler": "^3.57.2"
   }
 }
babiebabie

Park UIコンポーネントを追加。
(欲しいのはButtonコンポーネントだが、ButtonコンポーネントはSpinnerコンポーネントに依存してるためまとめてインストールする)

$ pnpm dlx @park-ui/cli components add button spinner
┌   Park UI vundefined
│
◇  Components installed.
│
└  Happy Hacking 🤞
babiebabie

うわあああああああ!

エラーが多発するbutton.tsx)

ん〜、ちょっと無理っぽい。アイランドも動いてないし。

次から、Honoで、付属JSXを使うのではなくReactを使うように、変更してみる。

babiebabie

レンダラーをReactに変える前に、上でやっていたパッケージ・エイリアスの設定を外していく。

tsconfig.json
diff --git a/tsconfig.json b/tsconfig.json
index 234682a..59ad389 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,13 +9,11 @@
     "lib": ["ESNext", "DOM"],
     "types": ["vite/client", "@cloudflare/workers-types"],
     "jsx": "react-jsx",
-    "jsxImportSource": "hono/jsx",
+    "jsxImportSource": "react",
     "baseUrl": ".",
     "paths": {
       "~/*": ["./*"],
-      "@/*": ["./app/*"],
-      "react": ["./node_modules/hono/jsx"],
-      "react-dom": ["./node_modules/hono/jsx/dom"]
+      "@/*": ["./app/*"]
     }
   },
   "include": ["**/*.ts", "**/*.tsx"]
vite.config.ts
diff --git a/vite.config.ts b/vite.config.ts
index 891b6b5..2a6401b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -10,10 +10,4 @@ export default defineConfig({
     honox({ devServer: { adapter } }),
     pages(),
   ],
-  resolve: {
-    alias: {
-      'react': 'hono/jsx', // prettier-ignore
-      'react-dom': 'hono/jsx/dom',
-    },
-  },
 })
package.json
diff --git a/package.json b/package.json
index e2bd2fb..256e34e 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,7 @@
   "dependencies": {
     "@ark-ui/react": "^3.6.2",
     "hono": "^4.5.3",
-    "honox": "^0.1.23",
-    "react": "npm:hono/jsx",
-    "react-dom": "npm:hono/jsx/dom"
+    "honox": "^0.1.23"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^4.20240529.0",

こんなもんかな。

babiebabie

レンダラーをHono JSXからReactへ差し替える。
(参考:BYOR - Bring Your Own RendererReact Renderer Middleware

まず、あらためてReact/ReactDOMやらをインストールする。

$ pnpm add react react-dom hono @hono/react-renderer
 WARN  2 deprecated subdependencies found: rollup-plugin-inject@3.0.2, sourcemap-codec@1.4.8
Packages: +1
+
Progress: resolved 540, reused 447, downloaded 1, added 1, done

dependencies:
+ @hono/react-renderer 0.2.1
+ react 18.3.1
+ react-dom 18.3.1

Done in 3s

レンダラーへのプロパティを追加する。

app/global.d.ts
diff --git a/app/global.d.ts b/app/global.d.ts
index 288f02b..6be2e3b 100644
--- a/app/global.d.ts
+++ b/app/global.d.ts
@@ -1,4 +1,5 @@
 import {} from 'hono'
+import '@hono/react-renderer'

 type Head = {
   title?: string
@@ -10,6 +11,15 @@ declare module 'hono' {
     Bindings: {}
   }
   interface ContextRenderer {
-    (content: string | Promise<string>, head?: Head): Response | Promise<Response>
+    (
+      content: string | Promise<string>,
+      head?: Head,
+    ): Response | Promise<Response>
+  }
+}
+
+declare module '@hono/react-renderer' {
+  interface Props {
+    title?: string
   }
 }

レンダラー差し替えと、prodとそれ以外でクライアントスクリプトとCSSが切り替わるよう調整。
<!DOCTYPE html>が付くようにオプションも追加)

app/routes/_renderer.tsx
diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx
index 94e2e9a..5ed7328 100644
--- a/app/routes/_renderer.tsx
+++ b/app/routes/_renderer.tsx
@@ -1,19 +1,32 @@
-import { jsxRenderer } from 'hono/jsx-renderer'
-import { Script } from 'honox/server'
-import indexCss from './index.css?url'
+import { reactRenderer } from '@hono/react-renderer'

-export default jsxRenderer(({ children, title }) => {
-  return (
-    <html lang="en">
-      <head>
-        <meta charset="utf-8" />
-        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-        <title>{title}</title>
-        <link rel="icon" href="/favicon.ico" />
-        <Script src="/app/client.ts" async />
-        <link rel="stylesheet" href={indexCss} />
-      </head>
-      <body>{children}</body>
-    </html>
-  )
-})
+export default reactRenderer(
+  ({ children, title }) => {
+    return (
+      <html lang="en">
+        <head>
+          <meta charSet="utf-8" />
+          <meta
+            name="viewport"
+            content="width=device-width, initial-scale=1.0"
+          />
+          <title>{title}</title>
+          <link rel="icon" href="/favicon.ico" />
+          {import.meta.env.PROD ? (
+            <>
+              <script type="module" src="/static/client.js" async />
+              <link rel="stylesheet" href="/static/app.css" />
+            </>
+          ) : (
+            <>
+              <script type="module" src="/app/client.ts" async />
+              <link rel="stylesheet" href="/app/app.css" />
+            </>
+          )}
+        </head>
+        <body>{children}</body>
+      </html>
+    )
+  },
+  { docType: true },
+)

ViteのSSRでReact/ReactDOMを使えるようにする。
Viteのssr.externalオプションにReactのものとPark UIのものを追加。
honox()のオプションのdevServer.excludeは、そのまま追加するとマージじゃなく置き換えになって、普通のページ部分も動かなくなるので、ソースからごそっと持ってきたやつにstyled-systemディレクトリを足してある。

vite.config.ts
diff --git a/vite.config.ts b/vite.config.ts
index 891b6b5..8b06f41 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,19 +1,46 @@
 import pages from '@hono/vite-cloudflare-pages'
 import adapter from '@hono/vite-dev-server/cloudflare'
-import honox from 'honox/vite'
+import honox, { devServerDefaultOptions } from 'honox/vite'
 import { defineConfig } from 'vite'
 import tsconfigPaths from 'vite-tsconfig-paths'

-export default defineConfig({
-  plugins: [
-    tsconfigPaths({ root: './' }),
-    honox({ devServer: { adapter } }),
-    pages(),
-  ],
-  resolve: {
-    alias: {
-      'react': 'hono/jsx', // prettier-ignore
-      'react-dom': 'hono/jsx/dom',
-    },
-  },
+export default defineConfig(({ mode }) => {
+  if (mode === 'client') {
+    return {
+      plugins: [tsconfigPaths({ root: './' })],
+      build: {
+        rollupOptions: {
+          input: ['./app/client.ts', './app/app.css'],
+          output: {
+            entryFileNames: 'static/client.js',
+            chunkFileNames: 'static/[name]-[hash].js',
+            assetFileNames: 'static/[name].[ext]',
+          },
+        },
+        emptyOutDir: false,
+      },
+    }
+  } else {
+    return {
+      ssr: {
+        external: ['react', 'react-dom', 'styled-system'],
+      },
+      plugins: [
+        tsconfigPaths({ root: './' }),
+        honox({
+          devServer: {
+            adapter,
+            exclude: [
+              ...devServerDefaultOptions.exclude,
+              /^\/app\/.+/,
+              /^\/favicon.ico/,
+              /^\/static\/.+/,
+              /^\/styled-system\/.+/,
+            ],
+          },
+        }),
+        pages(),
+      ],
+    }
+  }
 })

app/routes/index.cssapp/app.cssに移動&リネーム。
(index.cssだとindex.tsxページに対応しそうだな〜と思ったので)
root.cssにリネームの方が良かったかも)

$ mv app/routes/index.css app/app.css

HonoXが使うhydrate()関数やcreateElement()関数を、hono/jsxのものからreactのものに差し替える。

app/client.ts
diff --git a/app/client.ts b/app/client.ts
index 16ecf96..8702afd 100644
--- a/app/client.ts
+++ b/app/client.ts
@@ -1,3 +1,12 @@
 import { createClient } from 'honox/client'

-createClient()
+createClient({
+  hydrate: async (elem, root) => {
+    const { hydrateRoot } = await import('react-dom/client')
+    hydrateRoot(root, elem as any)
+  },
+  createElement: async (type: any, props: any) => {
+    const { createElement } = await import('react')
+    return createElement(type, props) as any
+  },
+})

ページでHTML属性をReact語に変換。

app/routes/index.tsx
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index 584c847..ac1c831 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -6,7 +6,7 @@ export default createRoute((c) => {
   const name = c.req.query('name') ?? 'Hono'
   return c.render(
     <div>
-      <h1 class={css({ fontSize: '2xl', fontWeight: 'bold' })}>
+      <h1 className={css({ fontSize: '2xl', fontWeight: 'bold' })}>
         Hello, {name}!
       </h1>
       <Counter />

Counterコンポーネントでhono/jsxreactに変更。

app/islands/counter.tsx
diff --git a/app/islands/counter.tsx b/app/islands/counter.tsx
index 7e8c1e0..9a98835 100644
--- a/app/islands/counter.tsx
+++ b/app/islands/counter.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'hono/jsx'
+import { useState } from 'react'
 import { Button } from '@/components/ui/button'

 export default function Counter() {

これで動いた!!

babiebabie

react-rendererミドルウェアのソースコード見てたら、renderToReadableStream()に対応してたので、<Suspense>使いたいし、このプロジェクトも対応したくなった。やっていく。

babiebabie

renderToReadableStream()Web APIは、Node.JSは対応してないので、DenoかBunを使わなければならない。
Denoにするとパッケージマネジメント方法がガラッと変わってしまうので、Bunでやる。

Bunインストールは、多分推奨はcurl&bashだけど、Homebrewでやった。

$ brew install oven-sh/bun/bun

色々消す。

$ rm pnpm-lock.yaml
$ rm -rf node_modules
$ rm -rf dist

package.json書き換え。

package.json
diff --git a/package.json b/package.json
index cf1a391..adbd32f 100644
--- a/package.json
+++ b/package.json
@@ -5,21 +5,21 @@
     "clean": "rimraf dist",
     "prepare": "panda codegen",
     "dev": "vite",
-    "build": "pnpm run clean && vite build --mode client && vite build",
+    "build": "bun run clean && vite build --mode client && vite build",
     "preview": "wrangler pages dev",
-    "deploy": "$npm_execpath run build && wrangler pages deploy"
+    "deploy": "bun run build && wrangler pages deploy"
   },
   "private": true,
   "dependencies": {

お試し。

$ bun install
$ bun run dev
$ bun run build
$ bun run preview

全部正常に動いた。
PnpmからBunへの移行おわり。