t1k:cocos:base:dep-graph
| Field | Value |
|---|---|
| Module | base |
| Version | 1.14.0 |
| Effort | medium |
| Tools | — |
Keywords: acorn, ast, cocos, component-graph, dep-graph, export-form, js2ts, migration, require-graph
How to invoke
Section titled “How to invoke”/t1k:cocos:base:dep-graphWhen to use this skill
Section titled “When to use this skill”Activate when you need to:
- Know which files use
module.exports =(require-target) vs a barecc.Class(cc-component). - Produce the Export-Form Registry before running the
t1k-cocos-base-js2tscodemod. - Get the require-graph map: who requires whom, reverse deps, circular cycles.
- Decide
export =vsexport defaultper file when writing the TS version.
Do NOT use for: editing JS, generating TS, or cutover — this is a read-only scanner.
What the skill does
Section titled “What the skill does”Node.js CLI script scripts/dep-graph.cjs:
- Scans every
*.jsunder the target dir (defaultClient/assets/script/). - Parses the AST with acorn (ES5 →
latestscript →latestmodule fallback;latestis required to read class fields likex = Date.now()in ES2022 files). Never skips a file because of eval/with — it detects a globaleval()/with-statement via AST (accurate, never confused byobj.eval()such askit.eval()); only flagsscopeUnreliablefor the bare-global portion. - Classifies the exportForm of each file (see table below).
- Collects every
require('...')+ ESimport...from, classifying usage (assign/new/member/bare). - Resolves per the Cocos 2.x model: relative-path (
.js/.ts/index) FIRST, then a global-basename fallback (Cocos registers every script by its unique basename →require("pako"), or a relative path that fails to resolve, still resolves by name). A relative require that resolves to nothing is listed in_meta.danglingRequires(a pre-existing bug in the JS). - Reads the uuid from
<file>.meta(used to transplant uuids during cutover). - Builds the requiredBy reverse map + detects circular requires. Also folds in incoming edges from scripts OUTSIDE
assets/script(editor tools,preloadModule.js,Startup.js…) — Cocos compiles every script underassets/, and those can value-require into the set → affectingvalueRequired/export-form. Such edges are markedexternal:true. - Analyzes globals:
globalsProvided(window.X = ...defined by the file) +globalsUsed(window.X+ bare free-globals via scope analysis) → feedsdeclaregeneration when converting to TS. - Analyzes the component-name graph (4th dependency channel): collects
getComponent/addComponent/...("X")string refs (AST), the registeredcomponentNameper file (= cc.Classnameoption, else filename), reversecomponentRefBy,_meta.frozenComponentNames(names the codemod MUST preserve), and_meta.brokenComponentRefs(string refs that match no component → dead/orphan). - Emits JSON facts to stdout — the skill body / agent reasons over them.
- Collects the method call-arity map (
methodArity: NAME → max args any call passes, whole-program; over-approximated by name) sot1k-cocos-base-js2tsemits an arity-tolerance overload ONLY on over-called methods (a method called with more args than it declares), keeping non-over-called methods clean. - Computes transitive component-ness (
isComponentper file): walks each cc.Classextendschain to acc.*engine component base (ENGINE_COMPONENT_BASES) —extends cc.Component✓,extends require("./Proto")follows the chain, no-extends✗.t1k-cocos-base-js2tsuses it to emit@propertyONLY on real components (a non-component likeEntity extends Proto→ plain fields, else TS1240). - Writes the registry (map
file → {uuid, exportForm, tsExport, componentName}) + top-levelmethodArityconsumed byt1k-cocos-base-js2ts.
Export-Form → TS export form decision table
Section titled “Export-Form → TS export form decision table”| exportForm | Detection condition | TS export form | Notes |
|---|---|---|---|
commonjs-class | module.exports = cc.Class(...) or = <var assigned cc.Class> | export = | JS require() from other files keeps working unchanged |
commonjs-object | module.exports = { ... } or = function(){} | export = | Keeps require() compatibility |
commonjs-named | Only exports.foo = ... (no module.exports assignment) | named (re-export each field) | Uncommon, review manually |
cc-component | Top-level bare cc.Class({...}) (no module.exports), Cocos auto-registers | export = if valueRequired, otherwise export default | ⚠️ Cocos auto-export → this file CAN be required as a value |
none | No export at all (side-effect/patch: assigns window.X, or cc.X = cc.Class(...) patches the engine) | always export {} (even if valueRequired) | none has no value → require() always returns {}; export = would CHANGE require’s return value (e.g. net.js window.net) = behavior change. Internal cc.Class patches are preserved, NO @ccclass |
es-default | Already an ES module: has export default <expr> | export default | Module system ALREADY migrated; preserve. ES export wins over an in-file cc.Class (the module’s real value is the default binding). 7 files. |
es-named | Already an ES module: export class/const/function, export { a }, export * from (no default) | named | Preserve. NEVER export {} (would delete real exports → break import X from). 1 file (sdk.js). |
General principle (every row above follows it): the TS export form must preserve the value require() currently returns for real consumers + keep class-registration/side-effects. valueRequired only changes the form when the file HAS a value to export (cc-component); a none file has no value, so it is always export {}.
CORE gotcha (usage-aware): Cocos 2.x auto-exports the cc.Class() result → a cc-component file CAN still be require()’d as a VALUE (e.g. var X = require("./Avatar"); ...ctor: X). Therefore tsExport is NOT decided by exportForm alone: if ANY file requires it with usage assign/new/member (flag valueRequired=true) → it MUST be export = (keep module.exports = X), otherwise require() returns {default:X} and the untouched JS consumer breaks. Only when valueRequired=false (only bare/never required) is a cc-component safe as export default. Real example: Avatar.js is a cc-component but required by 12 files (10 as VALUE) → export =.
Global graph (window.X + bare global)
Section titled “Global graph (window.X + bare global)”dep-graph tracks globals as a 2nd dependency channel in parallel with require:
globalsProvided: files that assignwindow.X = ...(provider). ~361 global names in the project; 53/95nonefiles actually “export” via a global this way (the registry still records exportForm=none, but globalsProvided shows they provide a global).globalsUsed:window.Xreads + bare free-globals (via scope analysis) → the list needingdeclare const X: any/declare global { interface Window {...} }for the TS to compile._meta.globalProvidedBy: global → defining file._meta.externalGlobals: bare globals used but defined by NO file → SDK/runtime needing a declare (sdk44×,net,kit,MAX,HUMAN…).
JSON contract
Section titled “JSON contract”{ "version": 1, "scanned": 434, "elapsedMs": 4800, "results": [ { "file": "managers/LevelManager.js", // relative to target dir "uuid": "3f7a1c29-...", // from .meta, null if absent "exportForm": "cc-component", // see table above "requires": [ { "spec": "./Entity", "line": 3, "internal": true, "usage": "assign" // assign|new|member|bare } ], "requiredBy": ["managers/GameManager.js"], "requiredByDetail": [ // each require edge into this file { "file": "managers/GameManager.js", "usage": "assign", "line": 42 }, { "file": "../preloadModule.js", "usage": "assign", "line": 5, "external": true } // outside assets/script ], "valueRequired": true, // true if any usage is assign/new/member "circular": [], // array of cycle paths, if any "globalsProvided": ["TaskManager"], // window.X = ... defined by this file "globalsUsed": [ // need a `declare` when converting to TS { "name": "user", "kind": "window" }, { "name": "net", "kind": "bare" } ], "scopeUnreliable": false, // true if a global eval()/with is present → bare-global best-effort "componentName": "LevelManager", // registered name (= cc.Class name option, else filename); null if not a component "componentRefs": [ // getComponent("X") strings inside this file { "name": "Avatar", "line": 88, "method": "getComponent" } ], "componentRefBy": ["managers/GameSystem.js"] // who calls getComponent("LevelManager") → FROZEN name } ], "registry": { "managers/LevelManager.js": { "uuid": "3f7a1c29-...", "exportForm": "cc-component", "valueRequired": true, "tsExport": "export =", // usage-aware: valueRequired → export = "componentName": "LevelManager", // codemod MUST emit @ccclass("LevelManager") "isComponent": true // transitively extends cc.Component? → @property valid (else plain fields/accessors) } }, "methodArity": { // NAME → max args any call passes (whole-program); js2ts overload targeting "getConfig": 3, "continue": 2, "onExit": 0 // getConfig over-called (3 > 2 params) → overload; onExit never → clean }, "_meta": { "exportFormCounts": { "cc-component": 301, "commonjs-object": 27, ... }, "circularFiles": [...], "topRequired": [{ "file": "...", "requiredBy": 12 }], "globalProvidedBy": { "TaskManager": ["managers/TaskManager.js"] }, "externalGlobals": { "sdk": 44, "net": 30 }, // used but defined by no file → SDK/runtime "danglingRequires": [ // relative require that resolves to nothing = pre-existing JS bug { "file": "scripts/TalentDefine.js", "spec": "../components/combat/buffs/BulletOnShot", "line": 1507 } ], "frozenComponentNames": ["Item", "battleBottomUI", "LocalLabel"], // referenced via getComponent("X") → codemod MUST keep @ccclass name exact "brokenComponentRefs": [ // getComponent("X") matching no component = dead/orphan { "file": "scripts/TeamHead.js", "name": "GunModel", "line": 15 } ], "externalConsumers": [ // edges from scripts OUTSIDE assets/script requiring into the set (already folded) { "from": "../preloadModule.js", "target": "managers/EffectManager.js", "usage": "assign" } ] }}# First run (auto-installs acorn, ~5s):node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs
# Human-readable summary:node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs --pretty
# Write the registry to a file:node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs --out tmp/dep-registry.json
# Target a specific folder (STILL full-tree scans for accurate requiredBy, then filters output):node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs Client/assets/script/effects/visual/
# Detail for ONE file (full-tree scan → globally accurate requiredBy + valueRequired):node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs --pretty Client/assets/script/components/visual/Avatar.js
# Combined: pretty + write file:node .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs --pretty --out tmp/dep-registry.jsonGotchas
Section titled “Gotchas”-
Cocos auto-export + value-require: a bare
cc.Class({...})file →cc-component, BUT Cocos auto-exports it so it CAN still be required as a value → in that casetsExport=export =(see thevalueRequiredflag). Do NOT assume a cc-component is alwaysexport default— measured: 160/429 files needexport =. -
Sub-path always full-tree scans: passing a single file or sub-dir still scans all of
Client/assets/scriptand only then filters the output — sorequiredBy/valueRequired/global stay globally accurate (a folder-only scan gives wrong numbers, e.g. Avatar folder-scan=1 vs full-tree=12). -
Bare-global = scope analysis, may surface implicit-global leaks: an undeclared free identifier (e.g.
e,rnd_name) is reported inglobalsUsed— it may be an implicit-global bug (overlaps with thecocos2x-scope-auditsignal), NOT a real global. Review beforedeclare. The scope analysis leans toward over-reporting (safe: better to declare extra than to miss one → TS compile error).
9b. Builtin set is DATA-DRIVEN (not hardcoded): the builtin/ambient set used to filter globalsUsed is DERIVED at runtime, not a hand-maintained list (hand lists drift — we previously had to add eval/performance/self): (1) Object.getOwnPropertyNames(globalThis) → JS/ECMAScript globals; (2) parsing declare in creator.d.ts → engine globals (cc, sp, dragonBones, CC_*, Editor); (3) a small stable DOM/Web residual (DOM_WEB_GLOBALS: window/document/XMLHttpRequest/… — lib.dom types them downstream) + language-context names not on globalThis (require, module, exports, arguments, self). Mini-game/native platform globals (wx/tt/qq/swan/jsb/…) are deliberately NOT filtered — unlike DOM globals they are in no TS lib AND absent from creator.d.ts, so they genuinely need a declare; leaving them unfiltered surfaces them in externalGlobals (used-but-no-provider) so the js2ts globals.d.ts generator declares them from this registry (SSOT, no duplicated allowlist). creator.d.ts stays the authority: a platform global declared there is filtered via step (2); one that is not surfaces here. The final authority on “what needs a declare” is tsc (full lib + creator.d.ts) at the js2ts step.
-
eval/with no longer skips the whole file: AST distinguishes a global
eval(/with-statement (scope-dangerous →scopeUnreliableflag) from a methodobj.eval()/arr.with()(harmless, e.g.kit.eval()). A scope-unreliable file still has full require/exportForm/window data; only bare-globals are best-effort. (Previously the regex\beval\(wrongly skippedkit.eval()→ fixed.) -
Aliased require + new:
var $x = require("./X"); new $x()→ the script detects this pattern and marks the usage as"new". Ensuresexport =for file X. -
Incoming edges from OUTSIDE
assets/script(editor + preloadModule) — must be folded or export form is wrong: Cocos compiles EVERY script underassets/(not onlyassets/script). Measured: 24 VALUE edges from 7 out-of-tree files —preloadModule.js(a real build entry-point: value-requires kit/GameConstants/StatsTracker/EffectManager/…) + 6editor/scripts/*.js(extends: require("../../script/ui/core/Menu")…). Scanning onlyassets/scriptwould under-report requiredBy → a cc-component value-required ONLY by an external script → wrongly classifiedexport defaultinstead ofexport =→require()at the entry-point breaks. Fix: scan.jsoutside the tree (parent of scanRoot), fold VALUE edges into requiredBy/valueRequired, markexternal:true+_meta.externalConsumers. Only.jsis scanned (acorn can’t parse TS types; out-of-tree.tssuch asassets/Plugins/*don’t import into the set, so they’re safely skipped). In epic_dle_war 0 files flip today (targets are allnone/commonjs or already value in-scope), but the graph is now complete and no longer relies on luck. -
cc.find("path")/getChildByName("X")are NOT a dep-graph channel (negative result): Measured: 25cc.find(string)+ 614getChildByName(string)— all are NODE names in the scene hierarchy ("Canvas/mainMenu","ui/battle_bottom/money_bar"…), NOT script/class names. Node names are defined in.fire/.prefab(scene graph), do not map to a.jsfile → no script↔script edge. The migration doesn’t touch scenes/prefabs (keeps UUIDs) + the codemod doesn’t change string literals → paths are invariant, preserved for free. Contrast with gotcha #12:getComponent("X")= class-name (script-dep, FROZEN) ><cc.find/getChildByName= node-name (scene-graph, orthogonal). Detecting a brokencc.findrequires parsing.fire/.prefab= a different tool, out of dep-graph’s scope. Do NOT add this channel (YAGNI). -
properties: { x: { type: T } }is NOT a new dependency channel (verified by AST, negative result): Measured 1183 property type-refs: 1177 engine (cc.Node/cc.Label/cc.Integer/cc.macro/sp/dragonBones), 6 localcc.Enum(self-contained in the same file), 0type: <require'd class>cases. Why no tracking is needed: iftype: FoothenFooMUST already berequire("./Foo")/imported in scope → the require-graph ALREADY has that edge; a type-ref is merely a “usage” of the require, not a new edge. Deciding which identifier emits@property({type: X})is a transformation of thet1k-cocos-base-js2tscodemod (it has the per-file AST) — NOT dep-graph’s job. Do NOT add a type-ref channel (YAGNI: tracking something with 0 cross-file dependency). Codemod note: preserve the obfuscated localcc.Enum(i/n/ain LabelEffect.js) when converting. -
getComponent(“X”) = 4th dependency channel, component name FROZEN: Cocos references a component by its string class-name via
getComponent/addComponent/getComponentInChildren/...("X"). The registered name = cc.Classnameoption if present, else = filename (epic_dle_war: 0 files set a name option → name = filename everywhere). 286 refs / 64 frozen names;Itemis referenced by 25 files via getComponent. Codemod consequence (integrity): when converting it must emit@ccclass("<componentName>")with the EXACT original name + keep the file basename, or everygetComponent("X")returns null → crash._meta.frozenComponentNames= names that must not change;_meta.brokenComponentRefs= refs pointing at a non-existent component (e.g.GunModel/HumanModel/Light2— pre-existing dead/orphan, do NOT fix). Resolved via a name-index that includes.tstoo (e.g. the already-migratedSpritePulseLoop.ts). Note: this channel does NOT affecttsExport(component name is independent of export-form) — it only constrains the @ccclass name. -
Cocos 2.x require by BASENAME (not only path): Cocos Creator 2.x registers every script in a flat namespace keyed by basename (script names MUST be unique). So
require("pako")/require("ResponsiveUtils")(no./) AND a relativerequire("./Attribute")whose path doesn’t resolve → all resolve via the global basename. Resolver: relative-path (.js/.ts/index) first → basename fallback (unique index). NOT modeling this = missingrequiredByedges → may wrongly assignexport defaultto a cc-component that should beexport =(measured: pako/ResponsiveUtils/notif-debug/MAXDebug are required by basename;./Attributeactually points atcomponents/attributes/Attribute.js).internal=truemeans it resolves INSIDE the scan tree; a relative require to an out-of-tree.ts(e.g.assets/Plugins/*.ts) = external. Safety condition: 434 unique basenames (0 collisions) — on a collision the index drops that name (no guessing)._meta.danglingRequireslists relative requires that fail to resolve (e.g.TalentDefine → BulletOnShotwhich doesn’t exist — a pre-existing JS bug, do NOT fix per integrity). -
Circular timing: static cycle detection over the graph — not runtime circular detection. A circular require in CommonJS JS still runs (Cocos accepts it); just track it when writing TS to avoid TDZ.
-
UUID from .meta: read from
<file>.js.meta. Null if the file was never imported into the Cocos editor (i.e. an orphan file). Check for null before transplanting a UUID. -
Strictly read-only: the script NEVER writes/edits
.jsfiles. If edits are needed — that’s a different skill (t1k-cocos-base-js2ts,t1k-cocos-cutover). -
Parser fallback: ES5 →
latestscript →latestmodule. UsesecmaVersion: 'latest'(NOT 2020) because some files are ES2022 with class fields (x = Date.now()) —sdk.jsused to false-parse-error at 2020. ES modules are NOT rare: 64/434 files already useimport/export(hybrid: ES import + acc.Classbody, orexport default). dep-graph collectsimport ... from,export ... fromre-exports, and dynamicimport('...')as dependency edges (usage: default/namespace→assign, named/re-export→member, side-effect→bare) — just likerequire(), sorequiredBy/valueRequiredcount ES edges too. exportForm recognizes ES exports →es-default/es-named(see table), never misclassifying them asnone/export {}. -
Excluded dirs:
node_modules,temp,library,build,dist,quick-scripts,BackupAssets,.git— skipped automatically.
- Scan:
Bashrunsnode .claude/skills/t1k-cocos-base-dep-graph/scripts/dep-graph.cjs [options] [dir] - Registry analysis: the skill body reads the JSON output and reasons about exportForm, requiredBy, circular
- Do NOT use Grep to detect exports — too slow and inaccurate with aliased patterns
See also
Section titled “See also”plans/reports/2026-05-29-js-to-ts-migration-brainstorm.md— full migration context (§5.2).claude/skills/cocos2x-scope-audit/— same architecture, run before dep-graph to surface implicit globalst1k-cocos-base-js2ts(proof v0.1.0) — codemod that consumes this skill’s Export-Form Registry as input