Skip to content

t1k:cocos:base:js2ts

FieldValue
Modulebase
Version3.3.2
Efforthigh
Tools—

Keywords: codemod, commonjs, js2ts codemod, ts-morph

/t1k:cocos:base:js2ts

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.

Node.js CLI scripts/js2ts.cjs (ts-morph):

  1. Reads the registry entry for the target file (--registry dep-registry.json): uuid, exportForm, valueRequired, tsExport, componentName.
  2. Parses the .js as an AST (ts-morph), locates the single cc.Class({...}) call and classifies every top-level statement.
  3. Emits a decorator class matching the project idiom (const { ccclass, property } = cc._decorator; → @ccclass('<name>') → class).
  4. Surgical type-resolve pass: writes the file, runs the TS checker with creator.d.ts, collects TS2339/TS2551 (Property 'X' does not exist / same with a “did you mean” hint) errors whose access is on this, and declares each as X: 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.
  5. Writes <file>.cv.ts (with --write) or prints to stdout (dry-run).

The source .js is read-only — never written.

The TS export form is taken from registry[file].tsExport (computed usage-aware by dep-graph):

exportFormtsExportModeEmitted shapeStatus
cc-componentexport defaultA@ccclass('X_cv') export default class X_cv extends Base {}✅ tsc
cc-component / commonjs-classexport =A@ccclass('X_cv') class X_cv extends Base {} export = X_cv;✅ tsc
commonjs-objectexport =Bbody verbatim; module.exports = E → export = E✅ tsc
none (side-effect)export {}Bbody verbatim; requires→import; append export {}; + globals.d.ts + cc-ext.d.ts✅ tsc
es-default / es-namedexport default / namedCbody 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.

To let the dormant TS validate in parallel with the live JS (brainstorm §3/§4):

  • File: Foo.js → Foo.cv.ts (basename Foo.cv, so it never collides with the live Foo.js).
  • Class + @ccclass: @ccclass('Foo_cv') + class Foo_cv — a temp registered name so Cocos sees no duplicate of the live Foo. The class IDENTIFIER is sanitized (e.g. map-scroll-view → map_scroll_view_cv); the @ccclass STRING keeps the original chars + _cv.
  • Internal imports point at .cv: a relative require("./Dep") becomes import Dep = require("./Dep.cv") iff Dep.cv.ts already exists on disk. This makes the converted set a self-consistent, type-checkable module graph (all real export =/export default modules) while the live .js set runs the game. At cutover the .cv suffix 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).

properties: { ... } entries → @property fields:

JS formTS 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)@property
count: 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.

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.

  • 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, plus js2ts.cjs --cutover for 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 in t1k-cocos-base-uuid-verify. There is no separate cutover skill.