Movatterモバイル変換


[0]ホーム

URL:


Zenn
YutaUraYutaUra
🦆

duckdb-wasm を使った VSCode 拡張機能の作り方

に公開1件

この記事を必要とする人はあまりいないかもしれません。
VSCode の拡張機能を作る際に、 duckdb を利用するには癖があるためその解説を行いたいと思います。

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-node と duckdb-wasm の違い

DuckDB にはduckdb-node という Node.js 用のバインディングも存在します。

duckdb-node と duckdb-wasm の両方とも duckdb を利用するために使うことができるのですが、 duckdb-node を VSCode 拡張を使う際には2つの問題があります。

  1. duckdb-node を使う場合、 native モジュールを利用する必要があるため、ユーザーの環境に応じた native モジュールをバンドルさせる必要があります。
  2. native モジュールを利用するため、 VSCode Web Extension としては使えません。(多分)

特に 1. は、結構深刻ですよね。自分自身しか利用しないのであれば、特に問題はないと思うのですが、配布することを考えると、環境ごとの native モジュールをバンドルさせるのは少し現実的ではありません。

ということで、今回は duckdb-wasm を使って VSCode 拡張機能を作ることにしました。

結論

先に結論置いておきます。

  1. duckdb-wasm の worker も単体でバンドルする
  2. duckdb-wasm の wasm ファイルもアセットとしてバンドルする
  3. duckdb-wasm の初期化処理も単体でバンドルする
  4. web-worker を使って Node.js で duckdb-wasm の worker を利用する

基本的には以上だと思います。
詳しいバンドラの設定は最後に紹介しようと思いますが、上記の点を押さえておけばなんとかなると思います。

duckdb-wasm の利用方法の紹介

基本的な使い方は duckdb-wasm のexamples が参考になるのですが、 VSCode 拡張機能として使う場合は少し工夫する必要があります。

大前提として、多くの場合 VSCode 拡張機能として作成する js ファイルはバンドルされることが多いです。
通常の npm ライブラリであれば、ユーザーがライブラリをインストールする際に合わせて依存関係もインストールもされるため、ライブラリ本体のコードのみを提供すれば問題ありませんが、 VSCode 拡張機能の場合は、ライブラリ本体と依存関係のあるライブラリ全てを配布する必要があります。その際に大量のファイルを配布するのはあまり嬉しいことではありません。そのため、 VSCode 拡張機能を開発する際は、最小限のファイルになるようにバンドルしたり、minify したりすることが一般的だと思います。

先ほどのexample では Node.js での利用方法も紹介されていますが、バンドルされることまでは考慮されていません。バンドルされたとしても正しく動作するように構成する必要があります。

duckdb-wasm には以下の登場人物がいます。

  • duckdb-wasm の wasm ファイル
  • duckdb-wasm の worker 用の 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 拡張に使ったよ!という方がいれば、ぜひ教えてください。

GitHubで編集を提案
YutaUra

WebエンジニアPython・Typescriptが好き

バッジを贈って著者を応援しよう

バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。

notroguenotrogue

参考になる記事ありがとうございます!

記事を参考に最初期化だけ行うサンプルのリポジトリ作ってみました

(僕の環境依存かもしれませんが、何ヶ所か設定変えています)


[8]ページ先頭

©2009-2025 Movatter.jp