
Quick.js と SWC を使って TypeScript のサンドボックス環境を Web Worker で実装する
この記事について
フロントエンドでサンドボックス環境というと <iframe sandbox />
が有名ですが、それを使わずともサンドボックスを構築することは可能です。
しかし、<iframe sandbox />
以外の方法は様々な理由からあまり使われておらず、参考となる情報も <iframe sandbox />
に比べると少ないため実装するのが難しい傾向にあります。
そのため今回は、サンドボックス環境を実装したい人の参考となれるように、そのサンドボックス環境を実装する時に一番選択肢から外されてしまう方法をあえて紹介していこうと思います。あえてね。
あと、普通に JavaScript のサンドボックス環境を実装するだけだと楽しさ半減してしまうので、TypeScript も実行できるサンドボックス環境を実装して行こうかなと思います。あえてね。
それではいってみよー💪
Vite + React で環境構築
まずは Vite + React の環境を整えます。create-vite
を使ってひな型を作成しましょう 👇
$ pnpm create vite
.../1957e42d429-4028 | +1 +
.../1957e42d429-4028 | Progress: resolved 1, reused 0, downloaded 1, added 1, done
│
◇ Project name:
│ quickjs-and-swc-sandbox
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ TypeScript + SWC
│
◇ Scaffolding project in /quickjs-and-swc-sandbox...
│
└ Done. Now run:
cd quickjs-and-swc-sandbox
pnpm install
pnpm run dev
ひな型が作成出来たら、次に ./src/App.tsx
を以下のように実装します 👇
import * as Comlink from "comlink";
import { useState } from "react";
import MyWorker from "./workers/mod.ts?worker";
interface WorkerApi {
eval(code: string): Promise<string>;
}
const myWorker = new MyWorker();
const workerApis = Comlink.wrap<WorkerApi>(myWorker);
function App() {
const [tsCode, setTsCode] = useState("");
const onEvalCode = async () => {
console.log(`実行するコード:\n${tsCode}`);
await workerApis
.eval(tsCode)
.then((result) => {
alert(`実行結果: ${result}`);
})
.catch((error) => {
alert(error.message);
});
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
maxWidth: 600,
}}
>
<textarea
placeholder="実行したいソースコードを入力"
value={tsCode}
onInput={(e) => setTsCode(e.currentTarget.value)}
style={{ minWidth: 450, minHeight: 300 }}
/>
<button
type="button"
onClick={onEvalCode}
>
実行する
</button>
</div>
)
}
export default App
実装内容は単純で <textarea />
に入力された TypeScript のコードを Web Worker に実装したサンドボックス環境で実行して結果を文字列として返します。
実装に Comlink というライブラリを使用していますが、これは Web Worker を簡単に扱えるようにしてくれるライブラリで、postMessage()
などの面倒な通信処理を普通の非同期関数として実装できるようになります。( comlink は pnpm add comlink
でインストールしておいて下さい! )
また、Vite には import 時に ?worker
をファイル名の後ろに付けることで、import 先のファイルを Worker として扱えるようにしてくれます。
フロントエンド側が実装できたら、次は Web Worker を実装して行きましょう 👉
Web Worker を実装する
まずは TypeScript から JavaScript に変換するための処理(トランスパイル)を書きます。
トランスパイルには SWC が Web 環境でも使えるように @swc/wasm-web
というパッケージを配信してくれているので、そちらを使って Web Worker の実装をします 👇
import initSwc, { transformSync } from "https://esm.sh/@swc/[email protected]";
export const transpile = async (tsCode: string): Promise<string> => {
await initSwc();
const result = transformSync(tsCode, {
jsc: {
parser: {
syntax: "typescript",
tsx: false,
},
},
});
return result.code;
};
これで TypeScript のコードを JavaScript にトランスパイルできます。
次は Quick.js を使って JavaScript を実行するための evalCode()
を実装しましょう 👇
import QuickJSVariant from "https://esm.sh/@jitl/[email protected]";
import { newQuickJSWASMModuleFromVariant } from "https://esm.sh/[email protected]";
const QuickJSPromise = newQuickJSWASMModuleFromVariant(QuickJSVariant);
export const evalCode = (jsCode: string): Promise<string> => {
const QuickJS = await QuickJSPromise;
using vm = QuickJS.newContext();
// ここでソースコードを実行する
using vmResult = vm.evalCode(result.code);
if (vmResult.error) {
const error = vm.dump(vmResult.error);
const errorObj = new Error(error.message);
errorObj.name = error.name;
errorObj.stack = error.stack;
throw errorObj;
}
// `globalThis.result`に格納されている値を文字列として取り出す
const result = vm.getProp(vm.global, "result").consume(vm.getString);
return result;
}
ここでは using 宣言を使って実装しています。これは TypeScript 5.2 で実装された機能で、メモリの解放処理などをスコープが外れた時に自動的に行ってくれる機能となっています。
Quick.js ではメモリ解放を自動的に行ってくれないので、using 宣言を使うことで .dispose()
を実行しなければいけない所を書かなくてもいいようにしています。( この辺りは実際に Quick.js を JS で実装してみるとその便利さが実感できると思います )
上記の実装ができたら、あとはフロントエンド側で使えるように Comlink を使って API を expose
します 👇
import * as Comlink from "https://esm.sh/[email protected]";
import { transpile } from "./swc-worker.ts";
import { evalCode } from "./quickjs-worker.ts";
const apis = {
eval: async (tsCode: string): Promise<string> => {
const jsCode = await transpile(tsCode);
const result = await evalCode(jsCode);
return result;
},
};
Comlink.expose(apis);
これで実装は完了です!次は実際に動作させてみましょう!
動作確認
<textarea />
に TypeScript のコードを入力して「実行する」ボタンを押すと、実行結果が alert()
で表示されていれば OK 👌 です。
あとがき
以上で Quick.js と SWC を活用して Web Worker 上に TypeScript のサンドボックス環境を実装する方法を紹介しました。
Quick.js を Web Worker で実行するという中々ニッチなアプローチですが、思っているよりも簡単に実装できるので、「 活用できそう! 」と思った方はぜひ参考にして見てください!
まあ、制約が多すぎて使いこなすの大変なんですけどね
はい、ここまで読んでくれてありがとうございました!
これが誰かの参考になれば幸いです。
記事に間違いなどがあれば、SNSなどで教えて頂けると嬉しいです。
それではまた 👋