@vue/repl を元に react が動かせるものにした
はじめに
@vue/repl というライブラリがある。
これは Vue SFC Playground で使われているもので、フロントエンドで完結するコードの内容がリアルタイムで反映されるやつ。
これの良いと思っているところは、複数のファイル入力に対応していて、importmap と esm を使ってサードパーティライブラリなどを解決している点。
社内でプライベートに開発しているライブラリの Playground を作りたい時、適当な場所に esm 形式でデプロイし、 importmap でそのパスを設定して解決させるという使い方をしていた。
同様のことを React でやりたかったのだが、利用したいコンポーネントを Playground のビルド時に含める必要があったり、単一のスクリプトファイルしか書けないというものしか見つからなかった。
例えば以下など。
なので、@vue/repl
を元に Vue 特有の部分を React に置き換えることを試みた。
変更の内容など
基本的に @vue/repl
はユーザーが入力したファイルに対して変換を行い、モジュール解決を行うためのコードを追加して importmap とともに iframe に突っ込むというもの。
そのため、https://github.com/vuejs/repl/blob/main/src/output/moduleCompiler.ts にあるモジュール解決のためのコード変換ロジックなど一般性が高いものはそのまま使えて、モジュール変換を行う部分やimportmap の初期状態の定義、アプリケーションの初期化ロジックなどを変更するだけでそれなりに動くものになる。
モジュール変換
変換は以下で行っている。
以下を行えばいい
- 今回は不要となる Vue SFC の変換ロジックを削除
- jsx, tsx の変換ロジックを追加
- ファイル種別と変換ロジックの対応ロジックを上記の変更に合わせて変える
@vue/repl
ではもともと sucrase を ts の変換に用いているため、これをそのまま使って変換ロジックを実装できる。例えば以下。
async function transformTSX(src: string) {
return transform(src, {
transforms: ['jsx', 'typescript'],
jsxRuntime: 'automatic',
}).code
}
ソースを受け取って変換済みの js コードを Promise で包んで返すだけの関数であるため、必要なら esbuild などを wasm で使ったり Worker を使って別スレッドで行わせることなどもできる。
importmap
importmap に関するロジックは以下にある。
デフォルトで vue と vue/server-renderer が設定してあるので、これを適当なものに変えればいい。
今回は以下のようにした
{
"react": "https://esm.sh/react@18.2.0?dev",
"react/": "https://esm.sh/react@18.2.0&dev/",
"react-dom": "https://esm.sh/react-dom@18.2.0?external=react&dev",
"react-dom/": "https://esm.sh/*react-dom@18.2.0&external=react&dev/"
}
アプリケーションの初期化
アプリケーションの初期化は入力したコードを実際に実行する Preview.vue
の中にあるコードで行われる。
ここでは @vue/repl
で実装されているモジュール解決のためのオブジェクトから、エントリーポイントのファイルで default export されているオブジェクトを参照してマウントしている。
これを React の実装に置き換えればいい。例えば以下。
transform(
`import { createRoot } from 'react-dom/client'
${previewOptions?.customCode?.importCode || ''}
const _mount = () => {
const AppComponent = __modules__["${mainFile}"].default
const app = window.__app__ = createRoot(document.getElementById('app'))
${previewOptions?.customCode?.useCode || ''}
app.render(<AppComponent />)
}
_mount()`,
{
transforms: ['jsx', 'typescript'],
jsxRuntime: 'automatic',
},
).code
おわりに
とりあえず動かすか試したくて動いたという状態なので、ライブラリ特有の部分を抽象化して容易に他のライブラリに対応できるようにしたり、monaco editor で参照できる型を追加できるようにしたいと思った
Discussion