blog.yuzu441.com

esbuildでビルドして配信されてるパッケージがinstanceofできない問題の対応

ある日「別パッケージから読み込んだ同一のパッケージのクラスをinstanceofすると常にfalseになる」という問題がどうにかならないかと相談されて、自分はハマった事がなかったのでそんな事があるのかと気になったので調べた

もしより良い方法があれば教えて欲しい。

問題概要

PackageCからPackageAとPackageBを読み込んでいて、PackageAのクラスをPackageBでinstanceofできない

Mermaidで書くとこんな感じ

また各パッケージではesbuildでビルドしていて、PackageAとPackageBはesbuildでビルドされたパッケージを配信している

調べてみると同じような問題を抱えている人がいた

原因

esbuildで以下のコードをesbuild --bundle --platform=node --format=esm --outfile=out.js index.tsでビルドすると以下のようになる

元のコード

export class Hoge {
  private x: number

  constructor(x: number = 1) {
    this.x = x
  }

  getValue(): number {
    return this.x
  }
}

esbuild実行後

var Hoge = class {
  x
  constructor(x = 1) {
    this.x = x
  }
  getValue() {
    return this.x
  }
}
export { Hoge }

このような結果になる場合に、PackageAとBでそれぞれesbuildでビルドすると複数のHogeクラスが定義されてしまうのでCで以下のような処理を書いた時に、B内部のinstanceofによるを比較は常にfalseになる

// package-bのコード
import { Hoge } from 'package-a';
// テスト用に値を返す関数
export const getHogeValue = (v: unknown): number {
  return v instanceof Hoge ? v.getValue() : -1;
}


// package-cのコード
import { Hoge } from 'package-a';
import {getHogeValue} from 'package-b';

const hoge = new Hoge(5);
console.log(getHogeValue(hoge)); // -1になる。理想は5が返ってきて欲しい

解決方法

解決方法としてはvar Hoge = classの部分を共通化して同じものを見る必要がある

どこまでパッケージに手を入れれるのかで変わってくるが自分が考えたのは以下の3つ
可能であれば3のそもそもライブラリ側でtranspileをしないようにするのが一番良い気がする

1. esbuildのexternalオプションを使う

PackageBのビルド時にesbuildのexternalオプションを使うと、PackageAをバンドルしなくなる

esbuild - API

# package-bのビルド
esbuild --bundle --platform=node --format=esm --outfile=out.js --external:package-a index.ts

ただそもそもPackageAのようなライブラリ側でバンドルしないようにする(--bundleを無くす)のが一番良い気がする

2. PackageBからAをexportする

PackageB以外でAをimportしてinstanceofしたりしないという制約ができてしまうが、PackageBからAをexportすることで解決することもできる

// package-bのコード
import { Hoge } from 'package-a';

export * from 'package-a';

// テスト用に値を返す関数
export const getHogeValue = (v: unknown): number {
  return v instanceof Hoge ? v.getValue() : -1;
}

3. ライブラリ側でtranspileしないようにする

そもそもesbuildでのビルドをやめtscでビルドしたものを使用するようにする(ファイルの指定・targetなどは省略)

tsc --outDir dist --declaration

これでほぼ型注釈が外れただけのコードが出てくるはずなので、B, Cで同じAを見ることができる

まとめ

今回はトランスパイルした結果をライブラリとして配信していた結果、同じクラスでも別のクラスとして扱われてしまい、instanceofができない問題が発生した。個人的にはアプリケーション側(例だとPackageC)でminifyなりトランスパイルするので、ライブラリ側では型注釈を外すだけで良いのではないかと思う

以下テストに使用したコード yuzu-sandbox/node-import-sandbox: import package test repository