
Madge と ESLint を使用して JavaScript プロジェクトの循環依存関係を特定して修正するためのガイド。
プロジェクトを実行すると、参照された定数が未定義として出力されます。
例: utils.js からエクスポートされた FOO は、index.js にインポートされ、その値は未定義として出力されます。
// utils.js // import other modules… export const FOO = 'foo'; // ...
// index.js
import { FOO } from './utils.js';
// import other modules…
console.log(FOO); // `console.log` outputs `undefined`
// ...
経験に基づくと、この問題は、index.js と utils.js 間の循環依存関係が原因である可能性があります。
次のステップは、仮説を検証するために 2 つのモジュール間の循環依存関係パスを特定することです。
コミュニティには、循環依存関係を見つけるために利用できるツールが多数あります。ここでは例として Madge を使用します。
Madge は、モジュールの依存関係の視覚的なグラフの生成、循環依存関係の検索、その他の有用な情報の提供を行う開発者ツールです。
// madge.js
const madge = require("madge");
const path = require("path");
const fs = require("fs");
madge("./index.ts", {
tsConfig: {
compilerOptions: {
paths: {
// specify path aliases if using any
},
},
},
})
.then((res) => res.circular())
.then((circular) => {
if (circular.length > 0) {
console.log("Found circular dependencies: ", circular);
// save the result into a file
const outputPath = path.join(__dirname, "circular-dependencies.json");
fs.writeFileSync(outputPath, JSON.stringify(circular, null, 2), "utf-8");
console.log(`Saved to ${outputPath}`);
} else {
console.log("No circular dependencies found.");
}
})
.catch((error) => {
console.error(error);
});
node madge.js
スクリプトを実行すると、2D 配列が取得されます。
2D 配列には、プロジェクト内のすべての循環依存関係が保存されます。各サブ配列は特定の循環依存関係パスを表します。インデックス n のファイルはインデックス n + 1 のファイルを参照し、最後のファイルは最初のファイルを参照し、循環依存関係を形成します。
Madge は直接の循環依存関係のみを返すことができることに注意することが重要です。 2 つのファイルが 3 番目のファイルを通じて間接的な循環依存関係を形成している場合、そのファイルは Madge の出力には含まれません。
実際のプロジェクトの状況に基づいて、Madge は 6,000 行を超える結果ファイルを出力しました。結果ファイルは、2 つのファイル間の循環依存関係が直接参照されていないことを示しています。 2 つのターゲット ファイル間の間接的な依存関係を見つけることは、干し草の山から針を探すようなものでした。
次に、結果ファイルに基づいて 2 つのターゲット ファイル間の直接的または間接的な循環依存関係パスを見つけるスクリプトの作成を ChatGPT に依頼しました。
/**
* Check if there is a direct or indirect circular dependency between two files
* @param {Array<string>} targetFiles Array containing two file paths
* @param {Array<Array<string>>} references 2D array representing all file dependencies in the project
* @returns {Array<string>} Array representing the circular dependency path between the two target files
*/
function checkCircularDependency(targetFiles, references) {
// Build graph
const graph = buildGraph(references); // Store visited nodes to avoid revisiting
let visited = new Set(); // Store the current path to detect circular dependencies
let pathStack = [];
// Depth-First Search
function dfs(node, target, visited, pathStack) {
if (node === target) {
// Found target, return path
pathStack.push(node);
return true;
}
if (visited.has(node)) {
return false;
}
visited.add(node);
pathStack.push(node);
const neighbors = graph[node] || [];
for (let neighbor of neighbors) {
if (dfs(neighbor, target, visited, pathStack)) {
return true;
}
}
pathStack.pop();
return false;
}
// Build graph
function buildGraph(references) {
const graph = {};
references.forEach((ref) => {
for (let i = 0; i < ref.length; i++) {
const from = ref[i];
const to = ref[(i + 1) % ref.length]; // Circular reference to the first element
if (!graph[from]) {
graph[from] = [];
}
graph[from].push(to);
}
});
return graph;
}
// Try to find the path from the first file to the second file
if (dfs(targetFiles[0], targetFiles[1], new Set(), [])) {
// Clear visited records and path stack, try to find the path from the second file back to the first file
visited = new Set();
pathStack = [];
if (dfs(targetFiles[1], targetFiles[0], visited, pathStack)) {
return pathStack;
}
}
// If no circular dependency is found, return an empty array
return [];
}
// Example usage
const targetFiles = [
"scene/home/controller/home-controller/grocery-entry.ts",
"../../request/api/home.ts",
];
const references = require("./circular-dependencies");
const circularPath = checkCircularDependency(targetFiles, references);
console.log(circularPath);
Madge からの 2D 配列出力をスクリプト入力として使用すると、その結果、index.js と utils.js の間に確かに循環依存関係があり、26 個のファイルが関与するチェーンで構成されていることがわかりました。
問題を解決する前に、根本原因を理解する必要があります。循環依存関係により、参照される定数が未定義になるのはなぜですか?
問題をシミュレートして単純化するために、循環依存関係チェーンが次のようになっていると仮定します。
index.js→component-entry.js→request.js→utils.js→component-entry.js
プロジェクト コードは最終的に Webpack によってバンドルされ、Babel を使用して ES5 コードにコンパイルされるため、バンドルされたコードの構造を確認する必要があります。
(() => {
"use strict";
var e,
__modules__ = {
/* ===== component-entry.js starts ==== */
148: (_, exports, __webpack_require__) => {
// [2] define the getter of `exports` properties of `component-entry.js`
__webpack_require__.d(exports, { Cc: () => r, bg: () => c });
// [3] import `request.js`
var t = __webpack_require__(595);
// [9]
var r = function () {
return (
console.log("A function inside component-entry.js run, ", c)
);
},
c = "An constants which comes from component-entry.js";
},
/* ===== component-entry.js ends ==== */
/* ===== request.js starts ==== */
595: (_, exports, __webpack_require__) => {
// [4] import `utils.js`
var t = __webpack_require__(51);
// [8]
console.log("request.js run, two constants from utils.js are: ", t.R, ", and ", t.b);
},
/* ===== request.js ends ==== */
/* ===== utils.js starts ==== */
51: (_, exports, __webpack_require__) => {
// [5] define the getter of `exports` properties of `utils.js`
__webpack_require__.d(exports, { R: () => r, b: () => t.bg });
// [6] import `component-entry.js`, `component-entry.js` is already in `__webpack_module_cache__`
// so `__webpack_require__(148)` will return the `exports` object of `component-entry.js` immediately
var t = __webpack_require__(148);
var r = 1001;
// [7] print the value of `bg` exported by `component-entry.js`
console.log('utils.js,', t.bg); // output: 'utils, undefined'
},
/* ===== utils.js starts ==== */
},
__webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var e = __webpack_module_cache__[moduleId];
if (void 0 !== e) return e.exports;
var c = (__webpack_module_cache__[moduleId] = { exports: {} });
return __modules__[moduleId](c, c.exports, __webpack_require__), c.exports;
}
// Adds properties from the second object to the first object
__webpack_require__.d = (o, e) => {
for (var n in e)
Object.prototype.hasOwnProperty.call(e, n) &&
!Object.prototype.hasOwnProperty.call(o, n) &&
Object.defineProperty(o, n, { enumerable: !0, get: e[n] });
},
// [0]
// ======== index.js starts ========
// [1] import `component-entry.js`
(e = __webpack_require__(148/* (148 is the internal module id of `component-entry.js`) */)),
// [10] run `Cc` function exported by `component-entry.js`
(0, e.Cc)();
// ======== index.js ends ========
})();
例の [数字] はコードの実行順序を示します。
簡易バージョン:
function lazyCopy (target, source) {
for (var ele in source) {
if (Object.prototype.hasOwnProperty.call(source, ele)
&& !Object.prototype.hasOwnProperty.call(target, ele)
) {
Object.defineProperty(target, ele, { enumerable: true, get: source[ele] });
}
}
}
// Assuming module1 is the module being cyclically referenced (module1 is a webpack internal module, actually representing a file)
var module1 = {};
module1.exports = {};
lazyCopy(module1.exports, { foo: () => exportEleOfA, print: () => print, printButThrowError: () => printButThrowError });
// module1 is initially imported at this point
// Assume the intermediate process is omitted: module1 references other modules, and those modules reference module1
// When module1 is imported a second time and its `foo` variable is used, it is equivalent to executing:
console.log('Output during circular reference (undefined is expected): ', module1.exports.foo); // Output `undefined`
// Call `print` function, which can be executed normally due to function scope hoisting
module1.exports.print(); // 'print function executed'
// Call `printButThrowError` function, which will throw an error due to the way it is defined
try {
module1.exports.printButThrowError();
} catch (e) {
console.error('Expected error: ', e); // Error: module1.exports.printButThrowError is not a function
}
// Assume the intermediate process is omitted: all modules referenced by module1 are executed
// module1 returns to its own code and continues executing its remaining logic
var exportEleOfA = 'foo';
function print () {
console.log('print function executed');
}
var printButThrowError = function () {
console.log('printButThrowError function executed');
}
console.log('Normal output: ', module1.exports.foo); // 'foo'
module1.exports.print(); // 'print function executed'
module1.exports.printButThrowError(); // 'printButThrowError function executed'
AST 分析フェーズ中に、Webpack は ES6 のインポートおよびエクスポート ステートメントを探します。ファイルに次のステートメントが含まれている場合、Webpack はモジュールを「ハーモニー」タイプとしてマークし、エクスポート用に対応するコード変換を実行します。
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependency/HarmonyExportInitFragment.js#L161https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164
根本原因の概要
We can use ESLint to check for circular dependencies in the project. Install the eslint-plugin-import plugin and configure it:
// babel.config.js
import importPlugin from 'eslint-plugin-import';
export default [
{
plugins: {
import: importPlugin,
},
rules: {
'import/no-cycle': ['error', { maxDepth: Infinity }],
},
languageOptions: {
"parserOptions": {
"ecmaVersion": 6, // or use 6 for ES6
"sourceType": "module"
},
},
settings: {
// Need this to let 'import/no-cycle' to work
// reference: https://github.com/import-js/eslint-plugin-import/issues/2556#issuecomment-1419518561
"import/parsers": {
espree: [".js", ".cjs", ".mjs", ".jsx"],
}
},
},
];
以上がESrojects での循環依存関係の問題の解決の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。