📝

@vue/repl を元に react が動かせるものにした

2024/04/29に公開

はじめに

@vue/repl というライブラリがある。
これは Vue SFC Playground で使われているもので、フロントエンドで完結するコードの内容がリアルタイムで反映されるやつ。

これの良いと思っているところは、複数のファイル入力に対応していて、importmap と esm を使ってサードパーティライブラリなどを解決している点。

社内でプライベートに開発しているライブラリの Playground を作りたい時、適当な場所に esm 形式でデプロイし、 importmap でそのパスを設定して解決させるという使い方をしていた。

同様のことを React でやりたかったのだが、利用したいコンポーネントを Playground のビルド時に含める必要があったり、単一のスクリプトファイルしか書けないというものしか見つからなかった。
例えば以下など。

なので、@vue/repl を元に Vue 特有の部分を React に置き換えることを試みた。

https://github.com/sterashima78/react-repl

変更の内容など

基本的に @vue/repl はユーザーが入力したファイルに対して変換を行い、モジュール解決を行うためのコードを追加して importmap とともに iframe に突っ込むというもの。

そのため、https://github.com/vuejs/repl/blob/main/src/output/moduleCompiler.ts にあるモジュール解決のためのコード変換ロジックなど一般性が高いものはそのまま使えて、モジュール変換を行う部分やimportmap の初期状態の定義、アプリケーションの初期化ロジックなどを変更するだけでそれなりに動くものになる。

モジュール変換

変換は以下で行っている。
https://github.com/vuejs/repl/blob/main/src/transform.ts

以下を行えばいい

  • 今回は不要となる 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 に関するロジックは以下にある。
https://github.com/vuejs/repl/blob/main/src/import-map.ts

デフォルトで 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 の中にあるコードで行われる。

https://github.com/vuejs/repl/blob/e3d1b9f689399c4d0fa8214d835fdbcfadc8d610/src/output/Preview.vue#L251-L270

ここでは @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