t1k:cocos:base:js2ts
| Field | Value |
|---|---|
| Module | base |
| Version | 3.3.2 |
| Effort | high |
| Tools | — |
Keywords: codemod, commonjs, js2ts codemod, ts-morph
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 active pipeline dep-graph → scope-audit(read) → js2ts → tsc-validate → uuid-verify(attach). (The destructive cutover step left the default pipeline on 2026-05-31; see t1k-cocos-base-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 the cutover stage, owned by t1k-cocos-base-migrate), 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).
Generated .d.ts files (globals / cc-ext / node-ext / engine-ext)
Section titled “Generated .d.ts files (globals / cc-ext / node-ext / engine-ext)”Four helper scripts (gen-globals-dts.cjs, gen-cc-ext-dts.cjs, gen-node-ext-dts.cjs,
gen-engine-ext-dts.cjs) generate ambient declarations for dynamic runtime members (bare
window.X globals, cc.<id> engine patches, custom .node.<id> props, and nested
cc.sys/dragonBones prototype patches) that would otherwise raise TS2339. All are type-only,
0 runtime emit, and must be regenerated after any dep-graph re-run. Full commands, filter rules,
and per-project counts: references/codegen-spec.md.
Usage, Gotchas, and Scope
Section titled “Usage, Gotchas, and Scope”Full CLI flag reference (dry-run, --write, --types override, --no-resolve, and why
noImplicitAny: false is required not optional), the 13 numbered gotchas (dynamic this.X
declarations, bare-cc.Class module errors, verbatim method bodies, loud-fail on unhandled
options, _super→super rewrite, arity-overload targeting, …), and the v0.2.0
not-yet-implemented list: references/usage-and-gotchas.md.
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-base-migrate— owns the cutover stage (scripts/cutover.cjs, plusjs2ts.cjs --cutoverfor Mode B): strips.cv, transplants uuid, deletes.js. Off the default pipeline since the 2026-05-31 decision — the default is the non-destructive re-point int1k-cocos-base-uuid-verify. There is no separate cutover skill.