UI5 モダナイゼーション(古いコード技術を最新に更新する作業)中に発生した循環依存関係(複数のモジュールが互いに呼び出す問題)を検出して解決します。 次のような場合に使用: - ユーザーが「cyclic dependency」「circular import」「undefined module at runtime」「lazy require」「sap.ui.require sync」「import cycle」などの用語を使った場合 - あるいはモジュールが正しいインポートパスなのに実行時に undefined になる場合 主な対応内容: **2つのモジュール間の循環依存** — 使用頻度の低い方の依存を遅延読み込み(sap.ui.require による動的インポート)に自動変換します。 **3つ以上のモジュールチェーン** — ハブ方式(中心となるモジュール経由)で自動修正します。修正できない場合は MODERNIZATION-ISSUES.md に記録します。 フェーズ 3 の最終ステップ(ステップ 3.3)として実行され、他のすべての処理の後に動作します。
Detect and resolve cyclic module dependencies introduced during UI5 modernization. Trigger when user mentions: "cyclic dependency", "circular import", "undefined module at runtime", "lazy require", "sap.ui.require sync", "import cycle", or when a module returns undefined despite a correct import path. Classic symptom: a module is `undefined` at runtime despite a correct sap.ui.define import path. Auto-fixes 2-node cycles (A↔B) by converting the lesser-used edge to lazy sap.ui.require(). Auto-fixes longer chains (3+ nodes) via hub-based approach. Reports unfixable chains to MODERNIZATION-ISSUES.md. Runs as Phase 3, Step 3.3 (final step) after all other Phase 3 steps.
This skill detects and resolves cyclic module dependencies that arise during UI5 modernization. When modernization converts global namespace access to sap.ui.define imports, new dependency edges can create circular imports (A imports B, B imports A), causing the UI5 AMD loader to return undefined for the back-edge module.
The fix: replace the back-edge sap.ui.define dependency with a lazy sap.ui.require("path/to/Module") (synchronous form) at each call site, retrieving the already-loaded module from the loader cache without creating a dependency edge.
| Rule ID | Message Pattern | This Skill's Action |
|---|---|---|
| (none — structural) | Runtime: module is undefined despite correct import path |
Detect cycle in dependency graph, convert back-edge to lazy sap.ui.require() |
This skill is NOT triggered by a UI5 linter rule. It addresses a structural problem in the module dependency graph. It is triggered as the final fix phase in the modernization workflow, or standalone when a developer encounters undefined modules at runtime.
sap.ui.define edges that can create cycles — running once at the end operates on the final dependency graph.undefined at runtime despite a correct import path.sap.ui.define dependency and gets an unexpected undefined.Three modernization operations add new sap.ui.define dependency edges that can introduce cycles:
Converting var Helper = com.example.app.utils.Helper; to a sap.ui.define dependency. If Helper already imports the current module, adding the reverse edge creates a 2-node cycle.
Wrapping legacy jQuery.sap.declare / jQuery.sap.require code in sap.ui.define. The jQuery.sap.require calls become dependencies, potentially creating cycles the legacy synchronous loader handled differently.
Test files that previously accessed modules via the global namespace chain now get proper sap.ui.define imports. Test utility files that reference each other can create cycles.
The UI5 AMD loader resolves sap.ui.define dependencies via depth-first traversal. When it encounters a cycle:
undefined for A's factory result (the back-edge)undefined where it expected A's exports2-node cycles (A↔B) are guaranteed to break. Longer chains (A→B→C→A) may or may not break at runtime depending on load order — they are latent bugs.
sap.ui.require (synchronous) Breaks the CycleThe synchronous form sap.ui.require("path/to/Module") does NOT create a loader dependency edge. It reads from the module cache without triggering a load.
Critical caveat: sap.ui.require(path) returns undefined if the target module's factory has not yet executed. So it is safe only when the target module is reachable via a static-only path from the active entry point that fires before the lazy call site.
When you remove a static edge A → B and replace it with var B = sap.ui.require("B") inside a function in A, you must verify B remains reachable from every entry-point controller that loads A.
Three forms of sap.ui.require — know the difference:
sap.ui.require("path/to/B") — sync cache read. No load trigger. Returns undefined if not in cache.sap.ui.require(["path/to/B"], function(B) {...}) — async load + callback. Triggers load.sap.ui.requireSync("path/to/B") — sync load + return. Triggers load. Deprecated.Key distinction:
sap.ui.define(["path/to/B"], ...) — creates a loader edge A→B (cycle risk)var B = sap.ui.require("path/to/B") inside a function body — no loader edge (safe if B is in cache)A bundled script automates detection (graph building, verification, cycle detection, usage counting, hub identification):
node <skill-dir>/scripts/detect-cycles.js <project-root>
The script:
manifest.json.js files (app + tests), strips comments, parses sap.ui.define arraysOutput is JSON to stdout. Use this to drive the fix phase. If unavailable, the manual procedure is below and in references/dependency-graph-analysis.md.
Read manifest.json → sap.app.id → e.g. "com.example.myapp" → convert to slash notation: "com/example/myapp". This identifies internal project modules vs. sap/* framework deps.
Parse all .js files (app source AND tests). For each file:
Strip comments (// and /* */) while preserving string contents. Prevents matching sap.ui.define in commented-out code.
Find sap.ui.define using a regex allowing arbitrary whitespace between tokens:
/sap\s*\.\s*ui\s*\.\s*define\s*\(/
Critical: The simpler sap\.ui\.define\s*\( MISSES files where sap.ui.define is split across lines. These patterns exist in real codebases.
Extract the dependency array: From ( after define, find [ ... ] respecting bracket depth. Extract string literals.
Filter to internal project modules only (matching namespace). Include test-resources/-prefixed paths.
Build directed graph: node = module path (without .js), edge = dependency.
Exclusions: Framework deps (sap/*), sap.ui.require calls in function bodies, string literals (extend names, fragment paths).
After building the graph, verify every referenced dependency was also parsed. A missing module can hide entire cycle chains.
For every module referenced as a dependency but having no graph node entry:
[...] arrays. If found but primary parser extracted nothing, merge into graph.sap.ui.define was found but first argument was not an array. Run same fallback.For every edge A→B, check if B→A also exists. Collect unique pairs (deduplicated).
Run Tarjan's SCC algorithm. Any SCC with 3+ nodes is a longer cycle chain. See references/dependency-graph-analysis.md for full algorithm pseudocode.
When both sides are equally safe:
sap.ui.define array).To make B lazy in A (A no longer statically imports B):
Step 1 — Remove B from A's dependency array and corresponding parameter:
// Before:
sap.ui.define(["path/to/B", "path/to/C"], function(B, C) {
// After:
sap.ui.define(["path/to/C"], function(C) {
Step 2 — Add lazy require at each usage site:
// Before:
someMethod: function() {
B.doSomething();
B.doSomethingElse();
}
// After:
someMethod: function() {
var B = sap.ui.require("path/to/B");
B.doSomething();
B.doSomethingElse();
}
One sap.ui.require per function — each function using B needs its own declaration.
Declaration keyword: Match surrounding style (const for modern, var for legacy).
Step 3 — Clean up: Remove var X = X; self-assignments. If B had zero code references, just remove from deps — no lazy require needed.
If A already has sap.ui.require("path/to/B") calls AND B in its sap.ui.define array, remove B from sap.ui.define and ensure all remaining usage sites have lazy requires. Don't duplicate existing calls.
Longer chains are auto-fixed using a hub-based approach. Identify the hub module within each SCC and make its cycle-creating dependencies lazy. One hub fix can eliminate many chains simultaneously.
A hub module sits at the center of multiple cycle paths. Rather than fixing edges in many files, converting a few deps in the hub breaks all cycles at once.
For each SCC with 3+ nodes:
sap.ui.define deps that are SCC members.Make all SCC-internal deps of the hub lazy. This guarantees all cycles through the hub are broken. If a hub's SCC-internal dep has zero usages, just remove it.
If a module references the cycle-creating dep at top level outside any function body (where lazy sap.ui.require would return undefined), report:
### Cyclic Dependency Chain (unfixable automatically)
- **SCC nodes**: A, B, C, D
- **Hub identified**: A (3 outgoing SCC edges, 4 incoming)
- **Blocking reason**: A references B at module top level (line 15), outside any function body.
- **Suggested manual fix**: Restructure A to defer the B reference into a function body, or extract the top-level initialization into a separate non-cyclic module.
ModuleA imports ModuleB, ModuleB imports ModuleA. ModuleB only uses ModuleA at 1 call site.
Before (broken):
// ModuleB.js
sap.ui.define([
"com/example/myapp/utils/ModuleA",
"sap/ui/thirdparty/jquery"
], function(ModuleA, jQuery) {
var ModuleB = {
handleStatus: function(aSelectedItems, sId) {
ModuleA.processStatus(aSelectedItems, sId); // undefined!
}
};
return ModuleB;
});
After (fixed):
// ModuleB.js
sap.ui.define([
"sap/ui/thirdparty/jquery"
], function(jQuery) {
var ModuleB = {
handleStatus: function(aSelectedItems, sId) {
var ModuleA = sap.ui.require("com/example/myapp/utils/ModuleA");
ModuleA.processStatus(aSelectedItems, sId);
}
};
return ModuleB;
});
Orchestrator imports Helper (5 usages), Helper imports Orchestrator (25 usages). Decision: make Helper lazy in Orchestrator (fewer sites to patch).
Before (broken):
// Orchestrator.js
sap.ui.define([
"com/example/myapp/utils/Helper",
"com/example/myapp/utils/Validator"
], function(Helper, Validator) {
var Orchestrator = {
openDialog: function() { Helper.openDialog(); },
refreshAll: function() { Helper.refreshAll(); }
// ... 3 more Helper usage sites
};
return Orchestrator;
});
After (fixed):
// Orchestrator.js
sap.ui.define([
"com/example/myapp/utils/Validator"
], function(Validator) {
var Orchestrator = {
openDialog: function() {
var Helper = sap.ui.require("com/example/myapp/utils/Helper");
Helper.openDialog();
},
refreshAll: function() {
var Helper = sap.ui.require("com/example/myapp/utils/Helper");
Helper.refreshAll();
}
// ... each function gets its own lazy require
};
return Orchestrator;
});
ModuleX imports ModuleY but never references it in code — just remove the dead import.
Before:
sap.ui.define([
"com/example/myapp/utils/ModuleY",
"com/example/myapp/utils/ModuleZ"
], function(ModuleY, ModuleZ) {
// ModuleY never used
After:
sap.ui.define([
"com/example/myapp/utils/ModuleZ"
], function(ModuleZ) {
ModelManager participates in 7 cycle chains through deps FilterHelper and ChartHelper. Hub analysis: ModelManager has 2 outgoing SCC edges — it is the hub.
Before (broken):
// ModelManager.js
sap.ui.define(["sap/ui/thirdparty/jquery",
"com/example/myapp/utils/FilterHelper",
"com/example/myapp/utils/Payload",
"com/example/myapp/utils/ChartHelper",
"sap/base/Log"
], function(jQuery, FilterHelper, oPayload, ChartHelper, Log) {
var ModelManager = {
getFilterConfig: function() {
var oFilterConfig = FilterHelper; // undefined due to cycle!
},
getChartType: function() {
var oChartType = ChartHelper.getAnnotationType(); // undefined!
}
};
return ModelManager;
});
After (fixed — both SCC-internal deps made lazy):
// ModelManager.js
sap.ui.define(["sap/ui/thirdparty/jquery",
"com/example/myapp/utils/Payload",
"sap/base/Log"
], function(jQuery, oPayload, Log) {
var ModelManager = {
getFilterConfig: function() {
var FilterHelper = sap.ui.require("com/example/myapp/utils/FilterHelper");
var oFilterConfig = FilterHelper;
},
getChartType: function() {
var ChartHelper = sap.ui.require("com/example/myapp/utils/ChartHelper");
var oChartType = ChartHelper.getAnnotationType();
}
};
return ModelManager;
});
Removing 2 deps from 1 file broke all 7 cycle chains. Note: var oFilterConfig = FilterHelper; is a legacy aliasing pattern — the skill preserves existing code, only transforming the import mechanism.
Run detection script to build the dependency graph and detect all cycles:
node <skill-dir>/scripts/detect-cycles.js <project-root>
JSON output contains twoNodeCycles (with usage counts and lazy-side decisions) and longerChains (with hub identification). If unavailable, follow the manual procedure above.
Review output: Check warnings and errors arrays for graph completeness issues. Address errors before proceeding.
Fix each 2-node cycle (from twoNodeCycles[]):
lazySide field identifies which module to patchsap.ui.define array + function parametervar Module = sap.ui.require("path/to/Module") at each usage siteFix longer chains via hub (from longerChains[]):
hub and hubInternalDeps fields identify module and deps to make lazyVerify — re-run cycle detection:
node <skill-dir>/scripts/detect-cycles.js <project-root>
Output should show twoNodeCycles: [] and longerChains: []. Then run npx @ui5/linter --details for regression check.
Verify static coverage:
node <skill-dir>/scripts/detect-unsafe-lazy.js <project-root>
Checks every sap.ui.require("M") has M reachable via static chain from every entry-point controller. If findings exist, apply "Append-to-Controller Remedy" below. Re-run until unsafeCount: 0. Skill is not done until BOTH scripts report 0 issues.
When detect-unsafe-lazy.js reports uncovered entry points, append the lazy target to the controller's sap.ui.define array as a load-only side-effect import:
// Before — controller does NOT statically import DialogHelper
sap.ui.define([
"com/example/myapp/utils/ActionHandler",
"sap/ui/core/mvc/Controller"
], function(ActionHandler, Controller) {
// ActionHandler has lazy require to DialogHelper
// → sap.ui.require("DialogHelper") returns undefined
// After — append as load-only dep (no factory param)
sap.ui.define([
"com/example/myapp/utils/ActionHandler",
"sap/ui/core/mvc/Controller",
"com/example/myapp/utils/DialogHelper"
], function(ActionHandler, Controller /* no DialogHelper param */) {
// DialogHelper now in cache → lazy require works
Rules:
staticUncovered lists 3+ controllers, add dep to BaseController.js or Component.js insteaddetect-unsafe-lazy.js after each fix to confirm resolutionLazy require INSIDE function body: Place var B = sap.ui.require("path/to/B") inside the function that uses B, not at module top level. At define-time, B may not be in cache yet.
One require per function: Each function body using the module needs its own declaration. The variable is function-scoped.
Only internal project modules: Framework deps (sap/*) never cause project-level cycles. Only process deps matching the project namespace.
String literals are NOT usages: Log messages, .extend() class names, fragment paths are strings, not code references. Do not count or modify them.
Commented-out code is NOT a usage: Lines inside // or /* */ do not count as usages.
Parameter alignment: Removing dep at index N → remove function parameter at index N. Adjust only if the removed dep had a corresponding parameter.
Existing lazy requires are NOT dependency edges: sap.ui.require("path/to/X") in function bodies does NOT create loader edges. Only sap.ui.define array entries are edges.
Idempotent: Running twice is safe. On second run, cycles are already broken — no changes made.
Test files included: Dependency graph must include test files. They can participate in cycles after modernize-test-starter adds sap.ui.define deps.
Do not touch async sap.ui.require: The async form sap.ui.require(["path/to/B"], function(B) {...}) creates a loader edge. This skill only uses the synchronous single-string form.
Multi-line sap.ui.define patterns: Regex MUST allow arbitrary whitespace/newlines between sap, ., ui, ., define, (. Simpler regexes miss split patterns, hiding cycle chains.
Strip comments before parsing: Always remove comments (respecting string literals) before searching for sap.ui.define. Files may contain it in comments as section markers.
Verify graph completeness: Check every referenced dependency was parsed. Run fallback analysis on files where primary parser found nothing. Merge discovered deps before cycle detection.
fix-cyclic-deps AFTER fix-js-globals completes.sap.ui.define dependencies that can create cycles between test utilities or between test and app modules.原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。