CommonJSにおいて循環参照時にモジュールがundefinedになる挙動

作成日:2024/12/04

概要

CommonJSにビルドしたTypeScriptのモジュールを使用している際に、どれをimportしてもundefinedになる現象に遭遇したので、その挙動を再現してみる。 調査の結果、CommonJSのモジュール読み込みの仕組みと循環参照に原因がありそうだと考えられるようになった。

検証

ビルドしたCommonJSのモジュールが循環参照によってundefinedになるか検証してみる。

main.js -> moduleA.js -> moduleB.js -> moduleA.js... という循環参照を作成する。

// main.js
const module1 = require("./module1");

const module2Class = module1.createModule2Class();
module2Class.callModule1Func();
// module1.js
const module2 = require("./module2");

const module1Func = () => {
    console.log("call module1Func");
};

const createModule2Class = () => {
    return new module2.Module2Class();
};

module.exports = {
    createModule2Class,
    module1Func
};
// module2.js
const module1 = require("./module1");

class Module2Class {
    constructor() { }
    callModule1Func() {
        module1.module1Func(); // ここでundefinedになる
    }
}

module.exports = {
    Module2Class
};

main.jsを実行すると、module1Funcが呼び出される前にmodule1がundefinedになる。

$ node main.js
/workspace/temp/module2.js:9
        module1.module1Func();
TypeError: module1.module1Func is not a function
    at Module2Class.callModule1Func (/module2.js:9:17)

考察

モジュールの読み込みタイミングによってmodule.exportsの値が異なるような動作になっている。 これを確認するために、各モジュールのrequireの次の行にモジュールに対してconsole.logを追加してみる。

// main.js
const module1 = require("./module1");
console.log("main.js", module1);
// module1.js
const module2 = require("./module2");
console.log("module1.js", module2);
// module2.js
const module1 = require("./module1");
console.log("module2.js", module1);

main.jsの実行結果から、module2.jsを読み込んだタイミングでmodule1のmodule.exportsが空のオブジェクトになっていることがわかる。 おそらく、逐次実行的にmodule.exportsを評価していて、module2.jsが評価されるタイミングではmodule1.jsは最後まで評価されていないのだと考えている。

$ node main.js
module2.js {}
module1.js { Module2Class: [class Module2Class] }
main.js {
  createModule2Class: [Function: createModule2Class],
  module1Func: [Function: module1Func]
}

まとめ

一次情報まで調査したわけでないので考察が間違っている可能性もあるが、TypeScriptからビルドしたJavaScriptファイルでモジュール全体がundefinedになる現象があるということについて知ることができた。 そもそも循環参照になっているモジュールを作るのを避けるべきなのでこれ以上は追わないが、同様な事象が発生した場合の調査方法の一つの選択肢として覚えておきたい。