この記事を必要とする人はあまりいないかもしれません。
VSCode の拡張機能を作る際に、 duckdb を利用するには癖があるためその解説を行いたいと思います。
DuckDB とは、データ分析に特化した列指向データベースです。DuckDB は SQLite のように、埋め込み可能なデータベースエンジンとして設計されており、オフラインでの利用などを行う際に便利です。
DuckDB にはduckdb-wasm という WebAssembly 版が存在し、ブラウザを含む様々な環境で DuckDB を利用することができます。
VSCode の拡張機能として、SQL を interface とした構造的なファイル検索システムを作りたいと考えました。
イメージとしては、例えば github actions のワークフローが大量に存在していて、特定の条件に満たすワークフローを検索するときに、こんな SQL で検索できたら便利だなと思いませんか?
SELECT filenameFROM filesWHERE filenameLIKE".github/workflows/%.yml"AND( JSON_EXTRACT(content,"$.on.push.branches")ISNOTNULLOR JSON_EXTRACT(content,"$.on.pull_request.branches")ISNOTNULL)
なんか適当な感じですけど、何がいいたいかというと、 VSCode に組み込まれた検索機能では、このような複雑な検索は難しいです。
正規表現のエキスパートであれば達成可能かもしれませんが、 SQL で検索できたら、より柔軟な検索が可能になるのではないかと考えました。
そこで DuckDB を使って、ファイルの情報をデータベースとして保存することで、 SQL で検索することができる仕組みを作れないかと考えました。
一応他の手段としては、 SQLite を使うことも考えましたが、 DuckDB の方がイイ感じ(重要)な気がしたので、 DuckDB を頑張って使えるようにしました。
DuckDB にはduckdb-node という Node.js 用のバインディングも存在します。
duckdb-node と duckdb-wasm の両方とも duckdb を利用するために使うことができるのですが、 duckdb-node を VSCode 拡張を使う際には2つの問題があります。
特に 1. は、結構深刻ですよね。自分自身しか利用しないのであれば、特に問題はないと思うのですが、配布することを考えると、環境ごとの native モジュールをバンドルさせるのは少し現実的ではありません。
ということで、今回は duckdb-wasm を使って VSCode 拡張機能を作ることにしました。
先に結論置いておきます。
web-worker
を使って Node.js で duckdb-wasm の worker を利用する基本的には以上だと思います。
詳しいバンドラの設定は最後に紹介しようと思いますが、上記の点を押さえておけばなんとかなると思います。
基本的な使い方は duckdb-wasm のexamples が参考になるのですが、 VSCode 拡張機能として使う場合は少し工夫する必要があります。
大前提として、多くの場合 VSCode 拡張機能として作成する js ファイルはバンドルされることが多いです。
通常の npm ライブラリであれば、ユーザーがライブラリをインストールする際に合わせて依存関係もインストールもされるため、ライブラリ本体のコードのみを提供すれば問題ありませんが、 VSCode 拡張機能の場合は、ライブラリ本体と依存関係のあるライブラリ全てを配布する必要があります。その際に大量のファイルを配布するのはあまり嬉しいことではありません。そのため、 VSCode 拡張機能を開発する際は、最小限のファイルになるようにバンドルしたり、minify したりすることが一般的だと思います。
先ほどのexample
では Node.js での利用方法も紹介されていますが、バンドルされることまでは考慮されていません。バンドルされたとしても正しく動作するように構成する必要があります。
duckdb-wasm には以下の登場人物がいます。
これらを組み合わせることで duckdb-wasm を利用することができます。具体的には以下のような手順で利用します。
importpathfrom'node:path'importduckdbfrom'@duckdb/duckdb-wasm'importWorkerfrom'web-worker'constDUCKDB_DIST= path.dirname(require.resolve('@duckdb/duckdb-wasm'));const logger=newduckdb.ConsoleLogger();const worker=newWorker(path.join(DUCKDB_DIST,"dist/duckdb-node-eh.worker.cjs"));const db=newduckdb.AsyncDuckDB(logger, worker);await db.instantiate(path.join(DUCKDB_DIST,"dist/duckdb-eh.wasm"));const conn=await db.connect();await conn.query(`SELECT count(*)::INTEGER as v FROM generate_series(0, 100) t(v)`);
通常であれば、上記のようにすることで期待通りに動作するはずです。しかし、バンドルをして、 VSCode 拡張機能として利用する場合は、以下のような問題が発生します。(厳密なエラーメッセージは忘れたので雰囲気です)
module not found @duckdb/duckdb-wasm
とかmodule not found duckdb-node-eh.worker.cjs
エラーが発生するmodule not found xxxx
エラーが発生するapache-arrow
などwasm file not found
エラーが発生するmodule not found vscode
エラーが発生する1つずつ原因と対処方法を説明していきます。
module not found duckdb-node-eh.worker.cjs
エラーが発生するこれはバンドルされた後をイメージしてもらえたらわかりやすいと思います。
バンドル前のソースコードが以下のような構成になっているとします。
.|-- src| |-- extension.ts| |-- duckdb.ts
これを愚直にバンドルすると、以下のような構成になります。
.|-- dist| |-- extension.js
extension.ts と duckdb.ts がバンドルされて extension.js になっていますね。
この extension.js の中でrequire.resolve('@duckdb/duckdb-wasm')
を実行するとどうなるでしょうか?
@duckdb/duckdb-wasm
は node_modules にインストールされているライブラリですが、dist
フォルダにはnode_modules
が存在せず、結果的に extension.js から@duckdb/duckdb-wasm
を見つけることができません。
なので、 worker ファイルも合わせてdist
フォルダに追加されるように設定する必要があります。
.|-- dist| |-- extension.js| |-- duckdb-node-eh.worker.cjs
importpathfrom'node:path'importduckdbfrom'@duckdb/duckdb-wasm'importWorkerfrom'web-worker'const logger=newduckdb.ConsoleLogger();// dist フォルダにどのように配置されるのかイメージしつつパスを指定するconst worker=newWorker(path.join(__dirname,"duckdb-node-eh.worker.cjs"));
module not found xxxx
エラーが発生する実は、単純に worker ファイルを dist フォルダにコピーするだけでは解決しません。
なぜなら、 worker ファイルではapache-arrow
などのライブラリが使用されており、それらも含めて worker ファイルにバンドルしてあげる必要あるからです。
なので、単純にコピーするのではなく、 entry point として@duckdb/duckdb-wasm/dist/duckdb-node-eh.worker.cjs
を指定してあげてください。
wasm file not found
エラーが発生するここまで来ていれば、このエラーも解決できるはずです。
想像通り、.wasm
ファイルも dist フォルダーにコピーする必要があります。
バンドラの設定を用いて、.wasm
ファイルを dist フォルダにコピーするように設定してあげてください。
module not found vscode
エラーが発生することがあるそして、ここまで設定しても、なぜかmodule not found vscode
エラーが発生することがあります。
そもそもvscode
というモジュールは VSCode 拡張でしか利用できないモジュールで、裏を返せばmodule not found vscode
エラーが発生することはないはずなので、非常に不思議です。
さて、かなり限られた情報しか提供していませんが、自信のある方は少し原因を考えてみましょう
はい。それでは正解の発表です。
正解は Worker(正確にはweb-worker
による Node.js polyfill) として起動する際は Node.js のworker_threads
が利用されます。子スレッドの実行コンテキストはメインスレッドとは異なるため、vscode
モジュールが利用できないということです。(詳しくは知りません。詳しい人いたら教えてください)
先述までのコードではvscode
モジュールは利用していませんが、実際の VSCode 拡張機能では以下のようなコードを書くことになります。
import*as vscodefrom'vscode'import{ initDB}from'./duckdb'// 仮に duckdb の初期化処理を別ファイルに切り出しているexportasyncfunctionactivate(context: vscode.ExtensionContext){const db=awaitinitDB() vscode.window.showInformationMessage('duckdb-wasm is activated!')}
そうすると、バンドルされたextension.js
はどうなるでしょうか?
const vscode=require('vscode')// @duckdb/duckdb-wasm// ~~~~// @duckdb/duckdb-wasm/dist/duckdb-node-eh.worker.cjs// ~~~~// web-worker// ~~~~// src/duckdb.tsconstinitDB=async()=>{// ...const worker=newWorker(path.join(__dirname,"duckdb-node-eh.worker.cjs"));// ...}// src/extension.tsexports.activate=asyncfunctionactivate(context){const db=awaitinitDB() vscode.window.showInformationMessage('duckdb-wasm is activated!')}
イメージですが、こんな感じになります。
すると worker_threads で起動される子スレッドはextension.js
がベースとなってしまいます。extension.js
ではvscode
が読み込まれていますが、子スレッドではvscode
を利用することはできないため、module not found vscode
エラーが発生するというわけです。
そのため先述のinitDB
の処理はextension.js
にバンドルさせずに、別ファイルに切り出す必要があります。
私自信 Node.js の worker_threads については、詳しくないため、別の方法で解決することができるかもしれませんが、私の知識ではこの方法しか思いつきませんでした。
ぐだぐだと書いてしまいましたが、私なりの結論をまとめたいと思います。
ファイル構成
.|-- dist| |-- extension.js| |-- duckdb.js| |-- duckdb-node-eh.worker.cjs| |-- duckdb-eh.wasm|-- src| |-- extension.ts| |-- duckdb.ts
// duckdb.tsimport{ join}from"node:path";import*as duckdbfrom"@duckdb/duckdb-wasm";// bundler の loader で wasm ファイルを解決できるので、 import するだけで OKimport duckdb_wasmfrom"@duckdb/duckdb-wasm/dist/duckdb-eh.wasm";import Workerfrom"web-worker";exportconstinitDb=async()=>{const logger=newduckdb.ConsoleLogger();const worker=newWorker(newURL(`file://${join(__dirname,"./duckdb-node-eh.worker.cjs")}`),);const db=newduckdb.AsyncDuckDB(logger, worker);await db.instantiate(join(__dirname, duckdb_wasm));return db;};
// extension.tsimport*as vscodefrom"vscode";// 動的読み込みするためのおまじない。(他にいい方法があれば教えてください)const duckdb=require(`${"./duckdb"}`)astypeofimport("./duckdb");// webpack の場合はこちら// const duckdb = __non_webpack_require__("./duckdb");const dbPromise= duckdb.initDb();exportconstactivate=async(context: vscode.ExtensionContext)=>{ vscode.window.showInformationMessage("duckdb-wasm is activating...");const db=await dbPromise; vscode.window.showInformationMessage("duckdb-wasm is activated!");};exportconstdeactivate=()=>{const db=await dbPromise;await db.terminate();};
続いてバンドラの設定です。
esbuild を使う場合
{"entryPoints":{"extension":"src/extension.ts","duckdb":"src/duckdb.ts","duckdb-node-eh.worker":"node_modules/@duckdb/duckdb-wasm/dist/duckdb-node-eh.worker.cjs",},"outdir":"dist","bundle":true,"format":"cjs","platform":"node","external":["vscode"],"loader":{".wasm":"file",}}
なんとなくこんな感じの設定をすれば、動くのではないかと思います。
webpack を使う場合は、私の webpack 力が低くて嫌な感じになってしまうのですが、web-worker/node.js
と@duckdb/duckdb-wasm/dist/duckdb-node.cjs
で利用されている dynamic require を dynamic require のまま処理させる方法がわからず、無理やり__non_webpack_require__
に置き換えています。
そのために、 webpack や ts-loader などに加えてstring-replace-loader
も install してあげてください。
const path=require('node:path');/**@type{import('webpack').Configuration}*/const config={target:"node",entry:{extension:'./src/extension.ts',_duckdb:'./src/_duckdb.ts',duckdb_worker:'./node_modules/@duckdb/duckdb-wasm/dist/duckdb-node-eh.worker.cjs'},output:{path: path.resolve(__dirname,'dist'),libraryTarget:'commonjs2',devtoolModuleFilenameTemplate:'../[resource-path]',},devtool:'source-map',externals:{vscode:'commonjs vscode'},resolve:{extensions:[".ts",".js"],},module:{rules:[{// dynamic require のままにしたいtest:[/node_modules\/web-worker\/node.js$/,/node_modules\/@duckdb\/duckdb-wasm\/dist\/duckdb-node.cjs$/],loader:'string-replace-loader',options:{search:/require\((mod|s)\)/g,replace:'__non_webpack_require__($1)',},},{test:/\.ts$/,exclude:/node_modules/,use:[{loader:'ts-loader'}]},{test:/\.wasm$/,type:'asset/resource'}]},};module.exports= config;
あとは vite とか、そういうのでもいい感じにできるんじゃないでしょうか。
また、 VSCode Web Extension の場合は、 target の指定とか、 worker や wasm がまた変わってくるため、その辺りもいい感じに設定してみてください。
webpack で dynamic require をするとファイルが解決できない場合にwebpackEmptyContext
とかに変換されてエラーになっちゃうんですよね。runtime でファイルが存在していても関係ないって感じなので、いろいろ調べてみましたけど今回の方法しか見つけられませんでした。
esbuild の方がよっぽど素直なので、新規プロジェクトであれば esbuild を使うのがいいかもしれません。
というわけで、 duckdb-wasm を使った VSCode 拡張機能の作り方でした。
もし duckdb-wasm を VSCode 拡張に使ったよ!という方がいれば、ぜひ教えてください。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。