t1k:cocos:base:js2ts
| Field | Value |
|---|---|
| Module | base |
| Version | 1.14.0 |
| Effort | high |
| Tools | — |
Keywords: ccclass, cocos, codemod, commonjs, decorator, js2ts, migration, ts-morph, typescript
How to invoke
Section titled “How to invoke”/t1k:cocos:base:js2tsWhen to use this skill
Section titled “When to use this skill”Activate to convert a Cocos 2.4.15 .js component to a dormant .cv.ts during the JS→TS migration, AFTER t1k-cocos-base-dep-graph has produced the Export-Form Registry. This is the js2ts step in the pipeline dep-graph → scope-audit(read) → js2ts → tsc-validate → cutover → uuid-verify.
Mission (inherited, absolute): convert SYNTAX only, keep 100% behavior, app runs identically. The codemod copies method bodies verbatim and only adds type-level declarations needed to compile (behavior-preserving). It does NOT fix bugs, scope-bugs, or logic.
Do NOT use for: deleting/renaming the .js (that is t1k-cocos-cutover), uuid transplant, or scene/prefab edits.
What the skill does
Section titled “What the skill does”Node.js CLI scripts/js2ts.cjs (ts-morph):
- Reads the registry entry for the target file (
--registry dep-registry.json):uuid,exportForm,valueRequired,tsExport,componentName. - Parses the
.jsas an AST (ts-morph), locates the singlecc.Class({...})call and classifies every top-level statement. - Emits a decorator class matching the project idiom (
const { ccclass, property } = cc._decorator;→@ccclass('<name>')→ class). - Surgical type-resolve pass: writes the file, runs the TS checker with
creator.d.ts, collectsTS2339/TS2551(Property 'X' does not exist/ same with a “did you mean” hint) errors whose access is onthis, and declares each asX: any;(no initializer → no runtime emit at target es5). Base-provided members (cc.Component.node,getComponent, …) never raise the error, so they are never shadowed. - Writes
<file>.cv.ts(with--write) or prints to stdout (dry-run).
The source .js is read-only — never written.
Export-form decision (from the registry)
Section titled “Export-form decision (from the registry)”The TS export form is taken from registry[file].tsExport (computed usage-aware by dep-graph):
| exportForm | tsExport | Mode | Emitted shape | Status |
|---|---|---|---|---|
| cc-component | export default | A | @ccclass('X_cv') export default class X_cv extends Base {} | ✅ tsc |
| cc-component / commonjs-class | export = | A | @ccclass('X_cv') class X_cv extends Base {} export = X_cv; | ✅ tsc |
| commonjs-object | export = | B | body verbatim; module.exports = E → export = E | ✅ tsc |
| none (side-effect) | export {} | B | body verbatim; requires→import; append export {}; + globals.d.ts + cc-ext.d.ts | ✅ tsc |
| es-default / es-named | export default / named | C | body verbatim; relative import/export … from → .cv | ✅ tsc |
Three modes — routed by exportForm (the registry SSOT), NOT by cc.Class presence. Mode A (cc-component / commonjs-class) rebuilds a decorator class. Mode B (commonjs-object / none / commonjs-named) keeps the ENTIRE file verbatim (incl. comments) and only rewrites top-level var X = require() → import X = require(), module.exports = E → export = E, and appends export {}; for a none file. Mode C (es-default / es-named) is for a file that is ALREADY an ES module (import/export, often ES2022 class fields) — keep the body VERBATIM and only rewrite RELATIVE import/export … from specifiers to the .cv coexistence sibling (if it exists). No export conversion (ES exports ARE TS exports), no export {} append. Simplest of the three. Maximum integrity — no body restructuring. A none file may still CONTAIN a cc.Class — an engine PATCH like cc.Update = cc.Class({ name: "cc.Update", ... }) (e.g. hack.js) — which must stay VERBATIM (Mode B), NOT be rebuilt as a component. Routing by cc.Class-presence (the old bug) sent such files to Mode A → threw on the name/other options.
Mode A preserves side-effect top-level statements. A cc.Class file often has other top-level statements beside the class (window.BULLET_FADE_TYPE = {...}, var X;, …). These are kept VERBATIM in source order (before/after the class) — they run at module load exactly as before; the bare globals they assign rely on globals.d.ts. Structural coverage: 307/307 cc.Class files convert without throwing (0 holdout). Hai holdout cuối đã giải bằng cách KHÔNG-đụng-dep-graph: Item.js (wrapped properties + computed-key) → unwrap + resolve CONST+"Icon" qua constantsFile (xem Property transform); EffectManager.js (2 cc.Class patch+bare, dep-graph misclassify) → exportFormOverride ép none = Mode B verbatim (file thực ra là side-effect module). → mọi scene game (main 24 / login 26 / backpack 50) Gate 2 PASS.
cc.Class options (Mode A). Handled: extends → heritage clause (or none); properties → @property fields only when the class is a true component (registry[file].isComponent — transitively extends cc.Component; a no-extends cc.Class like Proto OR one extending a non-component like Entity extends Proto → plain fields/accessors, else TS1240 — see Gotcha 12), and computed {get,set} entries → TS accessors (see Property transform); statics: {X:v, fn(){}, get g(){}, set s(v){}} → static X = v; static fn(){} static get g(){} static set s(v){} (get/set accessor support; setter param KHÔNG optionalize, TS1051 cấm set x(v?)); editor: {...} → class decorators; ctor: function(){} → constructor(){ [super();] ... }; name: "X" → reserved CLASS-NAME key, DROPPED (@ccclass(componentName) từ registry SSOT đã mang tên); top-level NON-function data field (ngoài properties, vd data: null, itemList: void 0) → plain instance default name: any = value; (KHÔNG @property — behavior-preserving). mixins → throws (0 in this project). Destructuring require (const { default: X } = require("Y")) → giữ verbatim const {…} = require("Y.cv") (import=require KHÔNG destructure được; require runtime Cocos thật, type residual any-safe). Wrapped properties + computed-key (vd Item.js: properties: ((a={…}),(a[CONST+"Icon"]=v),…,a)) → unwrap sequence + resolve CONST+"Icon" thành tên literal (blueprintAtkIcon) qua constantsFile map → build synthetic object literal → convertProperties SẴN CÓ (verify: attach completeness 29/29 khớp serialized key trên 6 prefab). Alias rename: var i = cc.Class({...}) — obfuscated class-var i renamed (scope-correct) to TS class name.
Config override (cocos-migrate.json). exportFormOverride: { "<file>": "none" } — ép exportForm cho file dep-graph phân loại SAI, KHÔNG đụng dep-graph algorithm (rủi ro cả 434). Vd EffectManager.js: dep-graph thấy cc.Recycle = cc.Class(...) (engine-patch) → cc-component, nhưng file là side-effect module (bare anonymous cc.Class không name/extends/scene-ref) → ép none = Mode B verbatim. constantsFile: "<path>" — file global string-const (IDENT="lit" hoặc window.IDENT="lit") để resolve computed-key properties.
Coexistence naming (.cv marker)
Section titled “Coexistence naming (.cv marker)”To let the dormant TS validate in parallel with the live JS (brainstorm §3/§4):
- File:
Foo.js→Foo.cv.ts(basenameFoo.cv, so it never collides with the liveFoo.js). - Class +
@ccclass:@ccclass('Foo_cv')+class Foo_cv— a temp registered name so Cocos sees no duplicate of the liveFoo. The class IDENTIFIER is sanitized (e.g.map-scroll-view→map_scroll_view_cv); the@ccclassSTRING keeps the original chars +_cv. - Internal imports point at
.cv: a relativerequire("./Dep")becomesimport Dep = require("./Dep.cv")iffDep.cv.tsalready exists on disk. This makes the converted set a self-consistent, type-checkable module graph (all realexport =/export defaultmodules) while the live.jsset runs the game. At cutover the.cvsuffix is stripped (→Foo.ts,@ccclass('Foo'),require('./Dep')).
Convert in dependency order (base/dep before dependents) so the .cv sibling exists when a dependent is converted; otherwise the import stays ./Dep and won’t type-check against the bare-cc.Class .js (see Gotcha 2).
Property transform
Section titled “Property transform”properties: { ... } entries → @property fields:
| JS form | TS emitted |
|---|---|
content: cc.Sprite (shorthand type) | @property(cc.Sprite)content: cc.Sprite = null; |
list: [cc.Node] (array shorthand) | @property([cc.Node])list: cc.Node[] = []; |
{ type: cc.Integer, default: 5, tooltip: 't' } | @property({ type: cc.Integer, tooltip: 't' })name: number = 5; |
count: 5 / flag: false / label: "" (literal default) | @propertycount: number = 5; (type inferred) |
x: { get: function(){…}, set: function(e){…} } (computed) | get x() {…}set x(e) {…} (TS accessor, body VERBATIM — NOT @property) |
cc.Integer/cc.Float→number, cc.String→string, cc.Boolean→boolean, cc.X/sp.X/require’d-class→same, else any.
Computed (get/set) properties → TS accessors, not @property. A cc.Class { get, set } property IS an accessor; emitting @property({ get, set }) is invalid (TS2353 “‘get’ does not exist in the descriptor type”). The codemod emits a real get name(){…} / set name(e){…} (works on any class, component or not; behavior-preserving — accessor defined on the prototype exactly as cc.Class did). The @property vs plain-field/accessor decision is gated by isComponent (Gotcha 12).
Bare window-globals — generated globals.d.ts
Section titled “Bare window-globals — generated globals.d.ts”Every project global is a runtime window.X = ... (e.g. window.EFFECT = {} in EffectManager.js, window.ZORDER = {...} in define.js, window.kit = {} in kit.js) accessed bare (EFFECT.x, kit.clamp). Once a file becomes a TS module, a bare name needs an ambient declare var X: any; — a interface Window augmentation (the existing types/GameConfigTypes.d.ts pattern) only types window.X, NOT bare X.
scripts/gen-globals-dts.cjs generates an ambient globals.d.ts (SSOT = the dep-graph registry):
- declares every
_meta.globalProvidedByname + non-leak_meta.externalGlobals(SDK/UPPER_CASE/camelCase, now including mini-game platform globalstt/wx/… that dep-graph no longer filters) +declare function require. (No hardcoded platform allowlist — they arrive viaexternalGlobalsfrom the registry, SSOT.) - drops leak-like names (single lowercase letter
e/a/i/n/…, plusrnd_name/rnd_head) so they surface astscerrors for a later scope fix (policy: user decision; aligns withcocos2x-scope-audit). 2-char names are KEPT (many are real SDKs:qg=quickgame,ks=kuaishou,uc,bl=bilibili).
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-globals-dts.cjs --registry tmp/dep-registry.json --out Client/assets/script/types/globals.d.tsRegenerate after any dep-graph re-run. Include globals.d.ts in the validation tsconfig.
Engine patches — generated cc-ext.d.ts
Section titled “Engine patches — generated cc-ext.d.ts”The game patches the cc namespace at runtime (cc.recycle = ...) AND uses cc members absent from this project’s (partial) creator.d.ts (cc.vec4(...), cc.js, cc.gfx, cc.RotateBy …) → TS2339. scripts/gen-cc-ext-dts.cjs scans the source for every cc.<id> member access (assignments + read/call usage), filters out members already declared in creator.d.ts (avoids duplicate-identifier; legit cc.Node/cc.Sprite/cc.log never re-declared), and emits a declare namespace cc { let <id>: any; } augmentation (type-only, 0 runtime emit → behavior-preserving). This project: 12 custom members (recycle, msgbox, dump, vec4, js, gfx, RotateBy/RotateTo, …); cc.update is engine-owned so it is skipped.
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-cc-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/cc-ext.d.tslet (not const/function) so both the patch assignment cc.recycle = fn and every consumer cc.recycle() type-check. Caveat: a file that reassigns an engine-owned member (cc.update = ..., which creator.d.ts declares as a function) still raises an assignment error — that is a genuine override the codemod surfaces, not something cc-ext.d.ts masks.
Custom node members — generated node-ext.d.ts
Section titled “Custom node members — generated node-ext.d.ts”The game stores custom props on node INSTANCES (this.node.onTouch = fn, then var e = this.node.onTouch). this.node is cc.Node, which has no onTouch → TS2339 (4th dynamic category, distinct from bare globals / cc-namespace patches / this.X). scripts/gen-node-ext-dts.cjs scans the source for .node.<id> accesses, filters out every real cc.Node member (incl. inherited from _BaseNode, read via the ts-morph type checker — avoids the interface/class merge error on active/x/on/…), and emits declare namespace cc { interface Node { <id>: any; } } (interface-merges with the engine’s cc.Node class; type-only, 0 runtime emit).
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-node-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/node-ext.d.tsThis project: 18 custom members (onTouch, runActionVS, recycleFunc, isDestroyed, …). Scope: only the .node.<id> receiver is detected; custom members reached via an aliased node var or event.target.<id> are not, and surface as TS2339 for a follow-up.
Nested / prototype engine patches — generated engine-ext.d.ts
Section titled “Nested / prototype engine patches — generated engine-ext.d.ts”cc-ext (cc.<id>) and node-ext (this.node.<id>) each own ONE receiver shape. An engine-bootstrap file (hack.js) patches engine internals at OTHER receivers that neither covers: cc.sys.<id> (custom langs), cc.director.<id> (_scheduler), cc.Node/Component/Action.prototype.<id> (incl. var h = cc.Node.prototype; h.<id> = …), and the foreign dragonBones.<Class>.prototype.<id> / dragonBones.WorldClock.<staticId>. Each maps to a different augmentation (namespace member vs class interface-merge). scripts/gen-engine-ext-dts.cjs is config-driven — the RECEIVERS table maps each { receiver, ns, target, kind: namespace|interface, alias }:
- scans each receiver (direct
<recv>.<id>+ aliasvar h = <recv>; h.<id> = …for prototype patches); - filters against creator.d.ts — namespace vars/functions/enums + class STATIC and INSTANCE members incl. inherited (so real members are never re-declared → no duplicate-identifier).
cc.sys/dragonBones.WorldClockare classes accessed astypeof X, so their statics (OS_IOS, …) are filtered via the static side. A namespace declared across MULTIPLEdeclare namespace <ns> { … }blocks (dragonBones has ~10) is searched across ALL blocks (getModules().filter), else Slot/WorldClock are missed; - filters against
node-ext.d.ts(so cc.Node members aren’t double-declared); - emits grouped by namespace:
declare namespace cc { namespace sys { let X } interface Director/Node/Component/Action { X } }+declare namespace dragonBones { interface Slot/ArmatureDisplay { X } namespace WorldClock { let X } }.let(notconst) for namespace members — the game ASSIGNS them (cc.sys.LANGUAGE_VIETNAMESE = "vi"→ aconstraises TS2540).
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-engine-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/engine-ext.d.ts --node-ext Client/assets/script/types/node-ext.d.tsThis project: 29 members across cc.{sys,Director,Node,Component,Action} + dragonBones.{Slot,ArmatureDisplay,WorldClock}. Cleared 36/51 hack.cv.ts errors. Remaining 15 are NOT receiver-typing: 6 function-object property patches (var f = …; f.<id> = … — the receiver is a function value; intractable) + 9 verbatim obfuscated-code quirks (!1 & bool TS2447, arity TS2554, non-callable TS2349, … — the same accepted-residual category as every file, behavior-preserving). Include engine-ext.d.ts in the validation tsconfig alongside the other three.
Config — .claude/cocos-migrate.json (SSOT, KHÔNG hardcode)
Section titled “Config — .claude/cocos-migrate.json (SSOT, KHÔNG hardcode)”--registry (key registry) và --types (key creatorDts) mặc định lấy từ .claude/cocos-migrate.json (dùng chung với t1k-cocos-base-uuid-verify/t1k-cocos-base-tsc-validate/t1k-cocos-base-migrate). Có config → gọi js2ts KHÔNG cần truyền 2 flag đó. Thứ tự: CLI flag > config > fallback (registry=null bắt buộc, types=auto walk-up). Xem t1k-cocos-base-migrate § Config.
# Có .claude/cocos-migrate.json → không cần --registry/--types:node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> # dry → stdoutnode .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --write # ghi <file>.cv.ts
# Dry-run (prints TS to stdout; surgical pass uses a temp sibling, then deletes it):node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --registry tmp/dep-registry.json
# Write <file>.cv.ts:node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --registry tmp/dep-registry.json --write
# Override creator.d.ts location (else auto-located by walking up):... --types Client/creator.d.ts
# Skip the surgical type-resolve pass (faster; may leave TS2339):... --no-resolveValidate the produced .cv.ts set with tsc --noEmit against creator.d.ts (module commonjs, target es5, experimentalDecorators, skipLibCheck, allowJs, noImplicitAny off — matches Client/tsconfig.json, the real Cocos 2.4.15 project config which sets NO strict/noImplicitAny → defaults to off).
noImplicitAny: false is REQUIRED, not optional. Params are emitted as bare e? (optional, type implicitly any); the codemod does NOT write : any (it would be redundant under the project config). Under noImplicitAny: true / strict (an IDE-only setting — the project tsconfig does NOT enable it), every un-annotated param raises TS7006. On LevelManager.cv.ts: 0 under the project config vs 141 under strict — of which 108 are emitted method params (fixable with e?: any) but 33 are params of top-level helpers / nested callbacks inside VERBATIM bodies, unfixable without editing the body (Gotcha 4). So strict-clean is unreachable for obfuscated code; the migration targets noImplicitAny: false (decision, user). If an IDE shows TS7006, point it at Client/tsconfig.json rather than annotating.
Gotchas
Section titled “Gotchas”-
Dynamic
this.X(base→subclass method, prototype patch) needs a declaration, NOT a body edit. Real example:Transition.js(base) callsthis.enterTransition()which onlyTransitionGo.js(a sibling subclass) defines — a valid polymorphic pattern, not a bug. The surgical pass declaresenterTransition: any;on the base class. Decision (user): surgical per-memberX: any;, chosen over a blanket[key:string]: anyindex signature (keeps type-checking everywhere else) and over// @ts-nocheck(which kills all checking). A bareX: any;(no initializer) emits no runtime code at es5 → behavior-preserving. -
A bare-cc.Class
.jsis “not a module” to TS (TS2306). A cc-component.jshas nomodule.exports(Cocos auto-registers it), soimport X = require("./Dep")against the liveDep.jsfails type-check. Fix = the.cvcoexistence import (point at the convertedDep.cv.ts, a real module). Consequence: convert in dependency order, and a not-yet-converted relative dep will not type-check until it is converted. -
this.nodefalsely declared if the typed base is lost. If the import resolves to a non-module/anybase, everythis.memberraises 2339 and gets a spuriousanydecl (observed:nodedeclared on TransitionCover before the.cvimport fix). Always resolve the import to the typed.cvbase so inherited members (node, etc.) stay typed and are not redeclared. -
Method bodies are copied VERBATIM (
getBody().getText()), only re-indented as a block. No statement is rewritten — integrity. Obfuscated single-letter params (e,t,i) are preserved untyped (tsconfig hasnoImplicitAnyoff → compiles). -
Loud-fail on unhandled cc.Class options.
ctor,editor,statics,mixins, non-function data fields, multiplecc.Class()calls, and unrecognized top-level statements throw rather than convert lossily. Add explicit support before converting such files (integrity > silent drop). -
export =+ decorator class compiles undermodule: commonjs+experimentalDecorators(verified). The class is declared thenexport = X;(decorator on the non-exported declaration). -
cc.Xengine patches need adeclare namespace ccaugmentation — done via the generatedcc-ext.d.ts(see section above). The game patchescc.recycle/cc.msgbox/cc.dump/cc.Recycle/cc.Update;globals.d.ts(bare globals) does NOT cover these (they’recc.members, not bare). Withcc-ext.d.tsincluded,bonusEffect.cv.tsvalidates EXIT=0. -
this._super→super.<methodName>transform (DONE, on by default). cc.Class injectsthis._superto call the same-named superclass method (70 files / 105 refs in this project); an ESextendsclass has no_super. The codemod replaces everythis._superPropertyAccess withsuper.<enclosing-method-name>, which handles all call shapes uniformly (this._super(a)→super.foo(a),this._super.apply(this,arguments)→super.foo.apply(this,arguments)). This is the ONE body-editing transform — a behavior-PRESERVING syntax conversion (without itthis._superis undefined → runtime crash). Proven:RewardBtn.onLoadthis._super()→super.onLoad()resolves against the converted base. Disable with--no-super-rewrite(then_superstays verbatim + gets declared — compiles but runtime-wrong). Caveat:super.foo()requires the typed base (.cv) to declarefoo; convert in dependency order. Athis._superinside a nested non-arrowfunction(){}(wherethisis rebound — a pre-existing bug) would become an invalidsuper— rare; tsc flags it.
9b. No-extends cc.Class must NOT default to extends cc.Component. A bare cc.Class({...}) with no extends key is a plain base class (e.g. Proto), NOT a component — the codemod emits class X {} with no heritage clause. (Bug fixed: previously injected a spurious cc.Component base.)
-
Dynamic members on
cc.Nodeinstances — done via the generatednode-ext.d.ts(see section above). The game stores custom props on nodes (this.node.onTouch);cc.Nodehas noonTouch→ TS2339 (4th dynamic category, distinct from bare-globals / cc-namespace patches /this.X).node-ext.d.tsinterface-merges the custom members ontocc.Node. With it included,ClickButton.cv.tsvalidates EXIT=0. Remaining gap: custom members reached via an aliased node var orevent.target.<id>(not.node.<id>) are still uncovered. -
Obfuscated comma-wrapper arg — handled (Bullet/Merchant).
cc.Class( ((i = {OBJ}).onHitWall = fn, i.remove = fn, i.statics = {…}, i) ): the config is assigned to a locali(the basei = {OBJ}is often NESTED in the first attachment’s LHS(i = {OBJ}).key), members are attached asi.<key> = <val>(functions → instance methods,statics→ static block), andiis returned (often via a result varvar a = cc.Class(...)). The codemod: unwraps the comma-sequence (unwrapWrapperArg), finds the nested base object, collects attachments, renames BOTH the config aliasiAND the result vara→ the TS class (statics/methods reference the class through them:a.shot(…)→ClassName.shot(…)), and dedupes an attachment method that overrides an inline method of the same name (i.remove = fnwins → no TS2393 duplicate). Validated:Bullet.cv.ts— all residual errors are the #7 method-variance category + intentional single-letter-leakTS2304, none are wrapper bugs. Still NOT handled:Item.js(twocc.Class()calls + aproperties: ((a = {…}), …)wrapped-properties form). -
@propertyonly on a TRUE component (transitiveisComponent).@propertyis valid ONLY when the class transitively extendscc.Component(proven by isolation test:@propertyon a class extendingcc.Componentcompiles; on a plain class → TS1240 “Unable to resolve signature of property decorator”). The codemod readsregistry[file].isComponent(dep-graph computes it by walking theextendschain to acc.*engine component base inENGINE_COMPONENT_BASES); non-components emitpropertiesas plain fields / accessors. This REPLACES the old “has a base → @property” heuristic, which was wrong:Entity extends Proto(Proto = a no-extendsplain base, NOT a component) → Entity is NOT a component, but the old heuristic decorated it → 9× TS1240. NowisComponent(Entity)=false(Entity→Proto→∅) → plain fields, 0 TS1240. Impact: the wholeProto/Entitysubtree (≈36 files: AI behaviors, Hero, Vehicle, Drop, …) is non-component and correctly un-decorated. A real component (extends cc.Component/cc.Sprite/…) still gets@property(e.g.buffBannerEffect). -
Method arity variance — handled via a TARGETED type-only OVERLOAD (NOT a rest param). Obfuscated JS treats every parameter as optional and ignores extra args; TS enforces arity → over-call (TS2554, called with MORE args than declared) and under-call / derived-adds-param (TS2554/TS2416). Under-call & derived-adds-param are resolved by
optionalizeParam(each simple param → optional). For over-call, the codemod emits a type-only overload signaturename(...args: any[]): any;immediately above the method/static (overloadLine); it accepts any argument count, is ERASED at emit (0 runtime code), and the impl keeps its original params. Targeting (full call-graph): the overload is emitted ONLY when the method is actually over-called somewhere —dep-graphnow ships a whole-programmethodAritymap (NAME → max args any call passes; keyed by name, a safe over-approximation) andoverloadLineemits the overload iffmethodArity[name] > declaredParamCount. SogetConfig/continue/send(over-called) get one;onExit()/clear()(never over-called) stay clean. OnLevelManager.jsthis cut overloads 73 → 7. Constructors are always overloaded (called via import-aliasednew Alias(...), so arity-by-name is unreliable; only a handful project-wide). Safety: a missed over-call (e.g. via dynamicobj[x](...)dispatch the map can’t see) degrades to a visible TS2554 residual at validation — NEVER a runtime change — because the overload is purely a type-level aid. Why not a rest param on the impl: at target es5 a rest param emits anarguments-gather loop (var args = []; for (var _i …)) EVEN WHEN UNUSED — a real per-call array allocation, NOT behavior-preserving. This corrects the prior0-param → ...argsstub approach, which DID inject that loop (thenoEmit: truevalidation config hid it). Proven onLevelManager.js(1747 lines): every class/static over-call TS2554 cleared; the emitted JS contains 0 gather-loops and 0 leaked overloads, impls emit with original params (LevelManager_cv.getConfig = function (e, t) {…}). Validated:Buff/Proto(override chain) + async/generator/static/ctor edge cases EXIT=0. Residual NOT covered (by design): a LOCAL function-expression inside a verbatim method body (var n = function (t, i){…}thenn(x)) — its arity mismatch surfaces as TS2554, but fixing it would require editing the verbatim body (forbidden by Gotcha 4), so it is left as a classified residual (behavior-irrelevant: the source JS ignored the arg-count too). Also residual: a fewTS2353(object-literal excess-property, narrowly-inferred param shape). -
Surgical pass over-declares when the base dep is unresolved. If
extends Baseresolves to a not-yet-converted bare-cc.Class.js(anany/non-module base), EVERYthis.memberraises 2339 and gets a spuriousanyfield. Convert in dependency order so the typed.cvbase exists first; then only genuinely-undeclared members are declared.
Scope / not-yet-implemented (v0.2.0)
Section titled “Scope / not-yet-implemented (v0.2.0)”tsc-validated: cc-component (export default + export =: buffBannerEffect, Transition, TransitionCover), commonjs-object (VirtualAvatar, LeaderboardConstants), none/side-effect (bonusEffect — modulo cc.X patches). commonjs-class (BulletLaser) converts structurally (Mode A).
tsc-validated additions: _super→super transform (RewardBtn/ClickButton chain) + no-extends fix.
tsc-validated additions: editor→decorators (FullScreenWidget), ctor→constructor (ColorMatrix), statics+alias-rename (Buff), @property-only-on-component fix (Proto).
tsc-validated additions: uniform type-only overload for arity tolerance (LevelManager — 1747-line cc-component, no-extends base; every class/static over-call TS2554 cleared, emit verified 0 gather-loops). Replaces the rest-param stub (fixes the latent es5 arguments-gather emit). Remaining LevelManager residuals are all classified: dep-order new $drop() (TS2351, clears when Drop is converted), local-function-expr arity (TS2554, Gotcha 13 residual), scope-hazard single-letter shadow-mutation (TS2367/TS2322 → cocos2x-scope-audit).
Not yet built:
TS2353object-literal excess-property (narrowly-inferred param shape) — minor, unrelated to param-variance.- ĐÃ FIX HẾT 5 holdout → 307/307 convert, 0 throw:
LeaderboardData: statics get/set accessor + destructuring require (const {default:X}=require()verbatim) + setter param không-optionalize (TS1051).ItemAdvanced/DraftMenu: data field → plain instance field;name= reserved class-name key → drop.Item: wrapped-properties + computed-key → unwrap + resolveCONST+"Icon"quaconstantsFile(attach completeness 29/29 trên 6 prefab — tên resolved khớp serialized key).EffectManager:exportFormOverride→none(Mode B verbatim — file là side-effect module, dep-graph misclassify cc-component). → mọi scene game qua Gate 2: main (24) / login (26) / backpack (50). Full-434 prerequisite #0 DONE.
- (Tương lai, KHÔNG block) dep-graph misclassify
EffectManager(componentName=cc.Recycle, exportForm=cc-component) còn trong registry — đã workaround bằngexportFormOverride. Fix tận gốc (dep-graph phân biệtcc.X=cc.Classpatch vs export) là cải thiện sạch hơn nhưng rủi ro cả 434 → để sau. - Aliased-node /
event.target.<id>custom members (Gotcha 10 residual — only.node.<id>is covered bynode-ext.d.ts). already-ES modules (— DONE (Mode C): verbatim +es-default7 /es-named1).cvimport rewrite.sdk.cv.ts(es-named, ES2022) converts EXIT=0. Residual is NOT conversion-related: dominant is narrow type-inference on a big verbatim object literal (var sdk = { …40 methods… }→ dynamicsdk.Xmembers, unfixable via d.ts — it’s a local literal, same class as the surgicalthis.Xgap but on an object), plus a DOM-receiver category (HTMLDivElement.show/hide,window.X) the engine-ext generator could be extended to cover, plus verbatim quirks. Transitive plugin-.tserrors (Plugins/applovin/MAX.ts…) are PRE-EXISTING in those files, surfaced only because the import resolved.mixinscc.Class option (0 in this project; throws if seen).commonjs-named(exports.foo = ...) — none in this project; Mode B throws if seen.- Basename (Cocos-global) require coexistence rewrite (only relative
.//../handled). .cv.ts.metageneration (new uuid) — cutover/t1k-cocos-base-uuid-verifyconcern.
See also
Section titled “See also”t1k-cocos-base-dep-graph— produces the Export-Form Registry this codemod consumes (run it first).plans/reports/2026-05-29-js-to-ts-migration-brainstorm.md— full migration design (§4 cutover, §5.2 skill stack, §7 target template)..claude/handoffs/260530-dep-graph-hardening.md— session context.t1k-cocos-cutover(not yet built) — strips.cv, transplants uuid, deletes.js.