tyhuynh-laya-cmd 1.0.12 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,171 +1,171 @@
1
- # tyhuynh-laya-cmd
2
-
3
- > Fast, incremental UI build tool for **LayaAir 2** projects — replaces `layaair2-cmd`.
4
-
5
- Thay thế `layaair2-cmd` với build nhanh hơn, hỗ trợ incremental build, watch mode thông minh và migrate assets tự động.
6
-
7
- ---
8
-
9
- ## Tính năng
10
-
11
- - ⚡ **Incremental build** — chỉ rebuild những file thay đổi, không rebuild toàn bộ
12
- - 🎨 **Atlas generation** — đóng gói sprite atlas tối ưu với `maxrects-packer` + `sharp`
13
- - 📄 **UI code generation** — tạo `layaMaxUI.ts` từ `.scene` files
14
- - 👀 **Watch mode thông minh** — phân biệt `.scene` (code only) vs ảnh (atlas only) vs XML (full rebuild)
15
- - 🔍 **Auto-detect** — tự tìm thư mục `laya/` từ thư mục hiện tại, không cần chỉ định `-p`
16
- - 📦 **Migrate assets** — phát hiện và di chuyển ảnh lạc loài trong `bin/` vào `laya/assets/`
17
-
18
- ---
19
-
20
- ## Cài đặt
21
-
22
- ```bash
23
- npm install --save-dev tyhuynh-laya-cmd
24
- ```
25
-
26
- ---
27
-
28
- ## Sử dụng
29
-
30
- Chạy từ thư mục **gốc dự án** (nơi chứa thư mục `laya/`). Tool sẽ tự tìm `laya/`.
31
-
32
- ### Build UI (atlas + code)
33
-
34
- ```bash
35
- # Build một lần
36
- tyhuynh-laya-cmd ui -a -d
37
-
38
- # Build + clear cache (full rebuild)
39
- tyhuynh-laya-cmd ui -a -d -c
40
-
41
- # Chỉ build atlas
42
- tyhuynh-laya-cmd ui -a
43
-
44
- # Chỉ build UI code (layaMaxUI.ts)
45
- tyhuynh-laya-cmd ui -d
46
-
47
- # Watch mode — tự rebuild khi có thay đổi
48
- tyhuynh-laya-cmd ui -a -d -w
49
- ```
50
-
51
- | Flag | Tác dụng |
52
- |------|----------|
53
- | `-a` | Generate atlas sprite sheets |
54
- | `-d` | Generate TypeScript UI code (`layaMaxUI.ts`) |
55
- | `-c` | Clear cache, force full rebuild |
56
- | `-w` | Watch mode |
57
- | `-p <dir>` | Chỉ định thủ công đường dẫn `laya/` (mặc định: auto-detect) |
58
-
59
- ### Migrate stray assets
60
-
61
- Dùng khi có ảnh nằm trực tiếp trong `bin/res/image/` nhưng chưa được quản lý bởi `laya/assets/`.
62
-
63
- ```bash
64
- # Xem trước — không thay đổi gì
65
- tyhuynh-laya-cmd migrate --dry-run
66
-
67
- # Thực hiện migrate
68
- tyhuynh-laya-cmd migrate
69
- ```
70
-
71
- Script sẽ:
72
- 1. Tìm ảnh trong `bin/res/image/` không có tương ứng trong `laya/assets/`
73
- 2. Copy chúng sang `laya/assets/`
74
- 3. Thêm entry vào `styles.xml`:
75
- - Thư mục **mới hoàn toàn** → `pack="2"` (dir rule, raw copy)
76
- - Thư mục **đã tồn tại** → từng file lẻ (giữ nguyên cấu hình atlas cũ)
77
-
78
- ---
79
-
80
- ## Tích hợp vào `package.json`
81
-
82
- ```json
83
- {
84
- "scripts": {
85
- "exportui": "tyhuynh-laya-cmd ui -a -d && npm run compile",
86
- "exportuic": "tyhuynh-laya-cmd ui -c -a -d",
87
- "exportui:watch": "tyhuynh-laya-cmd ui -a -d -w",
88
- "export": "tyhuynh-laya-cmd ui -c -a -d && npm run compile",
89
- "migrate": "tyhuynh-laya-cmd migrate",
90
- "migrate:dry": "tyhuynh-laya-cmd migrate --dry-run",
91
- "dev": "npm run nginx && concurrently \"npm run watch\" \"npm run exportui:watch\""
92
- }
93
- }
94
- ```
95
-
96
- ---
97
-
98
- ## Watch mode — Log output
99
-
100
- Khi chạy watch mode, output được phân loại rõ ràng với prefix `[UI]`:
101
-
102
- ```
103
- ╔════════════════════════════════════════╗
104
- ║ 🎨 UI Build Tool (tyhuynh-laya-cmd) ║
105
- ╚════════════════════════════════════════╝
106
- [UI] Project: D:\project\client\laya
107
- [UI] ✅ 12:00:00 — Build complete in 0.18s
108
- [UI] 👀 Watching pages/ & assets/ — Ctrl+C để dừng
109
- [UI] .scene → code | image → atlas | .xml → full rebuild
110
-
111
- # Khi sửa file .scene:
112
- [UI] 📝 Đã sửa:
113
- [UI] • pages/res/image/com/config/copy/CopyDoor.scene
114
- [UI] 📄 UI Code: 1 built, 823 skipped
115
- [UI] ✅ 12:01:30 — Build complete in 0.16s
116
-
117
- # Khi sửa ảnh:
118
- [UI] 📝 Đã sửa:
119
- [UI] • assets/res/image/com/bag/icon_gold.png
120
- [UI] 🖼 Atlas: done
121
- [UI] ✅ 12:02:00 — Build complete in 0.45s
122
- ```
123
-
124
- ---
125
-
126
- ## Cấu trúc dự án
127
-
128
- ```
129
- custom-cmd/
130
- ├── index.js # CLI entry point
131
- ├── build-dist.js # Build script (minify → dist/)
132
- ├── scripts/
133
- │ └── migrate-stray-assets.js # Migrate tool
134
- └── src/
135
- ├── config/
136
- │ └── ProjectConfig.js # Đọc .laya config
137
- ├── generators/
138
- │ ├── AtlasGen.js # Atlas generation
139
- │ ├── UICodeGen.js # layaMaxUI.ts generation
140
- │ └── SceneJsonGen.js # Scene JSON generation
141
- ├── parsers/
142
- │ ├── StylesParser.js # Parse styles.xml
143
- │ ├── PageParser.js # Parse pageStyles.xml
144
- │ └── SceneParser.js # Parse .scene files
145
- └── utils/
146
- ├── BuildCache.js # Incremental build cache
147
- ├── FileUtils.js
148
- └── XmlUtils.js
149
- ```
150
-
151
- ---
152
-
153
- ## Publish lên npm
154
-
155
- ```bash
156
- # Bump version
157
- npm version patch # hoặc minor / major
158
-
159
- # Build (minify) + publish tự động
160
- npm publish --access public
161
- ```
162
-
163
- > Script `prepublishOnly` sẽ tự chạy `build-dist.js` để minify code vào `dist/` trước khi publish.
164
- > Source gốc (`src/`, `scripts/`, `index.js`) **không được upload** lên npm.
165
-
166
- ---
167
-
168
- ## Yêu cầu
169
-
170
- - Node.js >= 16.0.0
171
- - LayaAir 2 project với cấu trúc `laya/` tiêu chuẩn (có file `.laya` config)
1
+ # tyhuynh-laya-cmd
2
+
3
+ > Fast, incremental UI build tool for **LayaAir 2** projects — replaces `layaair2-cmd`.
4
+
5
+ Thay thế `layaair2-cmd` với build nhanh hơn, hỗ trợ incremental build, watch mode thông minh và migrate assets tự động.
6
+
7
+ ---
8
+
9
+ ## Tính năng
10
+
11
+ - ⚡ **Incremental build** — chỉ rebuild những file thay đổi, không rebuild toàn bộ
12
+ - 🎨 **Atlas generation** — đóng gói sprite atlas tối ưu với `maxrects-packer` + `sharp`
13
+ - 📄 **UI code generation** — tạo `layaMaxUI.ts` từ `.scene` files
14
+ - 👀 **Watch mode thông minh** — phân biệt `.scene` (code only) vs ảnh (atlas only) vs XML (full rebuild)
15
+ - 🔍 **Auto-detect** — tự tìm thư mục `laya/` từ thư mục hiện tại, không cần chỉ định `-p`
16
+ - 📦 **Migrate assets** — phát hiện và di chuyển ảnh lạc loài trong `bin/` vào `laya/assets/`
17
+
18
+ ---
19
+
20
+ ## Cài đặt
21
+
22
+ ```bash
23
+ npm install --save-dev tyhuynh-laya-cmd
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Sử dụng
29
+
30
+ Chạy từ thư mục **gốc dự án** (nơi chứa thư mục `laya/`). Tool sẽ tự tìm `laya/`.
31
+
32
+ ### Build UI (atlas + code)
33
+
34
+ ```bash
35
+ # Build một lần
36
+ tyhuynh-laya-cmd ui -a -d
37
+
38
+ # Build + clear cache (full rebuild)
39
+ tyhuynh-laya-cmd ui -a -d -c
40
+
41
+ # Chỉ build atlas
42
+ tyhuynh-laya-cmd ui -a
43
+
44
+ # Chỉ build UI code (layaMaxUI.ts)
45
+ tyhuynh-laya-cmd ui -d
46
+
47
+ # Watch mode — tự rebuild khi có thay đổi
48
+ tyhuynh-laya-cmd ui -a -d -w
49
+ ```
50
+
51
+ | Flag | Tác dụng |
52
+ |------|----------|
53
+ | `-a` | Generate atlas sprite sheets |
54
+ | `-d` | Generate TypeScript UI code (`layaMaxUI.ts`) |
55
+ | `-c` | Clear cache, force full rebuild |
56
+ | `-w` | Watch mode |
57
+ | `-p <dir>` | Chỉ định thủ công đường dẫn `laya/` (mặc định: auto-detect) |
58
+
59
+ ### Migrate stray assets
60
+
61
+ Dùng khi có ảnh nằm trực tiếp trong `bin/res/image/` nhưng chưa được quản lý bởi `laya/assets/`.
62
+
63
+ ```bash
64
+ # Xem trước — không thay đổi gì
65
+ tyhuynh-laya-cmd migrate --dry-run
66
+
67
+ # Thực hiện migrate
68
+ tyhuynh-laya-cmd migrate
69
+ ```
70
+
71
+ Script sẽ:
72
+ 1. Tìm ảnh trong `bin/res/image/` không có tương ứng trong `laya/assets/`
73
+ 2. Copy chúng sang `laya/assets/`
74
+ 3. Thêm entry vào `styles.xml`:
75
+ - Thư mục **mới hoàn toàn** → `pack="2"` (dir rule, raw copy)
76
+ - Thư mục **đã tồn tại** → từng file lẻ (giữ nguyên cấu hình atlas cũ)
77
+
78
+ ---
79
+
80
+ ## Tích hợp vào `package.json`
81
+
82
+ ```json
83
+ {
84
+ "scripts": {
85
+ "exportui": "tyhuynh-laya-cmd ui -a -d && npm run compile",
86
+ "exportuic": "tyhuynh-laya-cmd ui -c -a -d",
87
+ "exportui:watch": "tyhuynh-laya-cmd ui -a -d -w",
88
+ "export": "tyhuynh-laya-cmd ui -c -a -d && npm run compile",
89
+ "migrate": "tyhuynh-laya-cmd migrate",
90
+ "migrate:dry": "tyhuynh-laya-cmd migrate --dry-run",
91
+ "dev": "npm run nginx && concurrently \"npm run watch\" \"npm run exportui:watch\""
92
+ }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Watch mode — Log output
99
+
100
+ Khi chạy watch mode, output được phân loại rõ ràng với prefix `[UI]`:
101
+
102
+ ```
103
+ ╔════════════════════════════════════════╗
104
+ ║ 🎨 UI Build Tool (tyhuynh-laya-cmd) ║
105
+ ╚════════════════════════════════════════╝
106
+ [UI] Project: D:\project\client\laya
107
+ [UI] ✅ 12:00:00 — Build complete in 0.18s
108
+ [UI] 👀 Watching pages/ & assets/ — Ctrl+C để dừng
109
+ [UI] .scene → code | image → atlas | .xml → full rebuild
110
+
111
+ # Khi sửa file .scene:
112
+ [UI] 📝 Đã sửa:
113
+ [UI] • pages/res/image/com/config/copy/CopyDoor.scene
114
+ [UI] 📄 UI Code: 1 built, 823 skipped
115
+ [UI] ✅ 12:01:30 — Build complete in 0.16s
116
+
117
+ # Khi sửa ảnh:
118
+ [UI] 📝 Đã sửa:
119
+ [UI] • assets/res/image/com/bag/icon_gold.png
120
+ [UI] 🖼 Atlas: done
121
+ [UI] ✅ 12:02:00 — Build complete in 0.45s
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Cấu trúc dự án
127
+
128
+ ```
129
+ custom-cmd/
130
+ ├── index.js # CLI entry point
131
+ ├── build-dist.js # Build script (minify → dist/)
132
+ ├── scripts/
133
+ │ └── migrate-stray-assets.js # Migrate tool
134
+ └── src/
135
+ ├── config/
136
+ │ └── ProjectConfig.js # Đọc .laya config
137
+ ├── generators/
138
+ │ ├── AtlasGen.js # Atlas generation
139
+ │ ├── UICodeGen.js # layaMaxUI.ts generation
140
+ │ └── SceneJsonGen.js # Scene JSON generation
141
+ ├── parsers/
142
+ │ ├── StylesParser.js # Parse styles.xml
143
+ │ ├── PageParser.js # Parse pageStyles.xml
144
+ │ └── SceneParser.js # Parse .scene files
145
+ └── utils/
146
+ ├── BuildCache.js # Incremental build cache
147
+ ├── FileUtils.js
148
+ └── XmlUtils.js
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Publish lên npm
154
+
155
+ ```bash
156
+ # Bump version
157
+ npm version patch # hoặc minor / major
158
+
159
+ # Build (minify) + publish tự động
160
+ npm publish --access public
161
+ ```
162
+
163
+ > Script `prepublishOnly` sẽ tự chạy `build-dist.js` để minify code vào `dist/` trước khi publish.
164
+ > Source gốc (`src/`, `scripts/`, `index.js`) **không được upload** lên npm.
165
+
166
+ ---
167
+
168
+ ## Yêu cầu
169
+
170
+ - Node.js >= 16.0.0
171
+ - LayaAir 2 project với cấu trúc `laya/` tiêu chuẩn (có file `.laya` config)
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const path=require("path"),fs=require("fs-extra"),{program:program}=require("commander");require("colors");const{loadProjectConfig:loadProjectConfig}=require("./src/config/ProjectConfig"),{loadStyles:loadStyles,invalidateCache:invalidateStyles}=require("./src/parsers/StylesParser"),{loadPageStyles:loadPageStyles,invalidateCache:invalidatePages}=require("./src/parsers/PageParser"),{generateUICode:generateUICode}=require("./src/generators/UICodeGen"),{generateAtlas:generateAtlas}=require("./src/generators/AtlasGen"),{generateGameConfig:generateGameConfig}=require("./src/generators/GameConfigGen"),{collectFiles:collectFiles}=require("./src/utils/FileUtils");function autoDetectLayaDir(){const e=e=>{const o=path.join(e,".laya");return fs.existsSync(o)&&fs.statSync(o).isFile()};let o=process.cwd();if(e(o))return o;const t=path.join(o,"laya");if(e(t))return t;let a=null,n=o;for(;n!==a;){const o=path.join(n,"laya");if(e(o))return o;a=n,n=path.dirname(n)}return null}async function runUI(e){const{project:o,atlas:t,code:a,clear:n,watch:r}=e,s=!!e.gameConfig,c=a||!t&&!a,i=t||!t&&!a,l=path.resolve(o);fs.existsSync(l)||(console.error(`[ERROR] Project directory not found: ${l}`.red),process.exit(1)),r?(console.log("\n╔════════════════════════════════════════╗"),console.log("║ 🎨 UI Build Tool (tyhuynh-laya-cmd) ║"),console.log("╚════════════════════════════════════════╝")):console.log(`\n${"⚡ Custom LayaAir2 Build Tool".cyan.bold}`),console.log(`[UI] Project: ${l.gray}`);const d=[c&&"CODE",i&&"ATLAS",s&&"GAME-CONFIG"].filter(Boolean).join(" + ");console.log(`[UI] Mode: ${d.yellow}`),n&&console.log(`[UI] Cache: ${"CLEARED".red}`),console.log(""),await build(l,{doCode:c,doAtlas:i,clear:n}),s&&await buildGameConfig(l),r&&await startWatchMode(l,{doCode:c,doAtlas:i,doGameConfig:s})}async function build(e,{doCode:o,doAtlas:t,clear:a,hint:n={}}){const r=Date.now();let s;try{s=loadProjectConfig(e)}catch(e){return void console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red)}const c=path.dirname(e),i=path.join(e,"pages"),l=path.join(c,s.codeExportPath,"layaMaxUI.ts"),d=path.join(e,".custom-cmd-cache"),g=path.join(d,"code-cache.json"),p=path.join(d,"atlas-cache.json"),u=o&&!n.atlasOnly,h=t&&!n.codeOnly,y=[],f=!!n&&Object.keys(n).length>0?()=>{}:e=>console.log(` ${e}`);u&&y.push(generateUICode({pagesDir:i,outputPath:l,cachePath:g,projectPath:c,resExportPath:path.join(c,s.resExportPath),clear:a,changedScenes:n.changedScenes||null,log:f}).then(e=>{console.log(`[UI] 📄 UI Code: ${String(e.built).green} built, ${e.skipped} skipped`)}).catch(e=>{console.error(`[UI] ${"❌ CODE ERROR".red}: ${e.message}`)})),h&&y.push((async()=>{try{const o=path.join(e,"styles.xml"),t=loadStyles(o);await generateAtlas({styleMap:t,projectConfig:s,assetDir:path.join(e,"assets"),outputDir:path.join(c,s.resExportPath),cachePath:p,clear:a,changedDirs:n.changedDirs||null,log:f}),console.log(`[UI] 🖼 Atlas: ${"done".green}`)}catch(e){console.error(`[UI] ${"❌ ATLAS ERROR".red}: ${e.message}`)}})()),await Promise.all(y);const m=((Date.now()-r)/1e3).toFixed(2),C=(new Date).toLocaleTimeString();console.log(`[UI] ✅ ${C} — Build complete in ${m}s\n`)}async function buildGameConfig(e){let o;try{o=loadProjectConfig(e)}catch(e){return void console.error(`[GameConfig] ${"❌ ERROR loading .laya".red}: ${e.message}`)}const t=path.dirname(e),a=path.join(e,"pages"),n=path.join(t,"src","GameConfig.ts");try{const e=await generateGameConfig({pagesDir:a,outputPath:n,projectConfig:o,log:()=>{}});e.written&&console.log(`[UI] ⚙ GameConfig: ${"Updated".green} (${e.total} scripts)`)}catch(e){console.error(`[UI] ${"❌ GAME-CONFIG ERROR".red}: ${e.message}`)}}async function startWatchMode(e,{doCode:o,doAtlas:t,doGameConfig:a}){let n;try{n=require("chokidar")}catch{console.error("[WATCH] chokidar not installed. Run: npm install chokidar".red),process.exit(1)}const r=path.join(e,"pages"),s=path.join(e,"assets"),c=path.join(e,"styles.xml"),i=path.join(e,"pageStyles.xml");console.log("[UI] 👀 Watching pages/ & assets/ — Ctrl+C để dừng"),console.log("[UI] .scene → code | image → atlas | .xml → full rebuild\n");let l=null,d=new Set,g=new Set,p=new Set,u=!1;function h(){clearTimeout(l),l=setTimeout(async()=>{const n=[...d],r=[...g],i=[...p],l=u;d.clear(),g.clear(),p.clear(),u=!1;const h=[...n,...r,...i,...l?[c]:[]];if(0===h.length)return;if(console.log("\n[UI] 📝 Đã sửa:"),h.map(o=>path.relative(e,o).replace(/\\/g,"/")).forEach(e=>console.log(`[UI] • ${e}`)),l)return console.log("[UI] ↻ XML config changed — full rebuild..."),void await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{}});const y=o&&n.length>0,f=t&&i.length>0;if(!y&&!f)return;let m=null;f&&(m=new Set(i.map(e=>path.relative(s,path.dirname(e)).replace(/\\/g,"/"))));let C=null;y&&(C=new Set(n)),await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{codeOnly:y&&!f,atlasOnly:f&&!y,changedDirs:m,changedScenes:C}}),a&&(n.length>0||r.length>0)&&await buildGameConfig(e)},300)}function y(e){d.add(e),h()}function f(e){g.add(e),h()}function m(e){p.add(e),h()}function C(e){e===c&&invalidateStyles(),e===i&&invalidatePages(),u=!0,h()}const j={ignoreInitial:!0,usePolling:!1};o&&n.watch(path.join(r,"**","*.scene"),j).on("add",y).on("change",y).on("unlink",y).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),a&&n.watch([path.join(r,"**","*.scene"),path.join(r,"**","*.prefab")],j).on("add",e=>e.endsWith(".prefab")?f(e):y(e)).on("change",e=>e.endsWith(".prefab")?f(e):y(e)).on("unlink",e=>e.endsWith(".prefab")?f(e):y(e)).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),t&&n.watch([`${s}/**/*.png`,`${s}/**/*.jpg`],j).on("add",m).on("change",m).on("unlink",m).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),n.watch([c,i],j).on("add",C).on("change",C).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red))}async function runGameConfig(e){const o=path.resolve(e.project);let t;fs.existsSync(o)||(console.error(`[ERROR] Project directory not found: ${o}`.red),process.exit(1));try{t=loadProjectConfig(o)}catch(e){console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red),process.exit(1)}const a=path.dirname(o),n=path.join(o,"pages"),r=e.output?path.resolve(e.output):path.join(a,"src","GameConfig.ts");console.log(`\n${"⚡ Custom LayaAir2 Build Tool".cyan.bold}`),console.log(`[GameConfig] Project: ${o.gray}`),console.log(`[GameConfig] Output: ${r.gray}`),console.log("");const s=Date.now();try{const e=await generateGameConfig({pagesDir:n,outputPath:r,projectConfig:t,log:e=>console.log(` ${e}`)}),o=((Date.now()-s)/1e3).toFixed(2),a=(new Date).toLocaleTimeString(),c=e.written?"Updated".green:"No changes".gray;console.log(`[GameConfig] ✅ ${a} — ${c} (${e.total} scripts) in ${o}s\n`)}catch(e){console.error(`[GameConfig] ${"❌ ERROR".red}: ${e.message}`),process.exit(1)}}program.name("tyhuynh-laya-cmd").description("Custom LayaAir 2 fast incremental UI build tool").version("1.0.1"),program.command("ui").description("Export UI code and/or atlas").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("-a, --atlas","Generate atlas sprite sheets",!1).option("-d, --code","Generate TypeScript UI code",!1).option("-g, --game-config","Generate GameConfig.ts",!1).option("-c, --clear","Clear cache (force full rebuild)",!1).option("-w, --watch","Watch mode",!1).action(async e=>{e.project||(e.project=autoDetectLayaDir(),e.project||(console.error("[ERROR] Cannot find laya/ directory. Use -p <dir> to specify manually.".red),process.exit(1)),console.log(` Auto-detected: ${e.project.gray}`)),await runUI(e)}),program.command("migrate").description("Migrate stray assets from bin/ into laya/assets/ and update styles.xml").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("--dry-run","Preview changes without writing anything",!1).action(e=>{const o=[];e.dryRun&&o.push("--dry-run"),e.project&&o.push("-p",e.project);const{spawnSync:t}=require("child_process"),a=require.resolve("./scripts/migrate-stray-assets.js"),n=t(process.execPath,[a,...o],{stdio:"inherit"});process.exit(n.status||0)}),program.command("game-config").description("Generate src/GameConfig.ts by scanning all .scene files for Script components").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("-o, --output <file>","Output path for GameConfig.ts (default: <projectRoot>/src/GameConfig.ts)",null).action(async e=>{e.project||(e.project=autoDetectLayaDir(),e.project||(console.error("[ERROR] Cannot find laya/ directory. Use -p <dir> to specify manually.".red),process.exit(1)),console.log(` Auto-detected: ${e.project.gray}`)),await runGameConfig(e)}),program.parse(process.argv);
2
+ "use strict";const path=require("path"),fs=require("fs-extra"),{program:program}=require("commander");require("colors");const{loadProjectConfig:loadProjectConfig}=require("./src/config/ProjectConfig"),{loadStyles:loadStyles,invalidateCache:invalidateStyles}=require("./src/parsers/StylesParser"),{loadPageStyles:loadPageStyles,invalidateCache:invalidatePages}=require("./src/parsers/PageParser"),{generateUICode:generateUICode}=require("./src/generators/UICodeGen"),{generateAtlas:generateAtlas}=require("./src/generators/AtlasGen"),{generateGameConfig:generateGameConfig}=require("./src/generators/GameConfigGen"),{generateAni:generateAni}=require("./src/generators/AniGen"),{collectFiles:collectFiles}=require("./src/utils/FileUtils");function autoDetectLayaDir(){const e=e=>{const o=path.join(e,".laya");return fs.existsSync(o)&&fs.statSync(o).isFile()};let o=process.cwd();if(e(o))return o;const t=path.join(o,"laya");if(e(t))return t;let a=null,n=o;for(;n!==a;){const o=path.join(n,"laya");if(e(o))return o;a=n,n=path.dirname(n)}return null}async function runUI(e){const{project:o,atlas:t,code:a,clear:n,watch:r}=e,s=!!e.gameConfig,c=a||!t&&!a,i=t||!t&&!a,l=path.resolve(o);fs.existsSync(l)||(console.error(`[ERROR] Project directory not found: ${l}`.red),process.exit(1)),r?(console.log("\n╔════════════════════════════════════════╗"),console.log("║ 🎨 UI Build Tool (tyhuynh-laya-cmd) ║"),console.log("╚════════════════════════════════════════╝")):console.log(`\n${"⚡ Custom LayaAir2 Build Tool".cyan.bold}`),console.log(`[UI] Project: ${l.gray}`);const d=[c&&"CODE",i&&"ATLAS",s&&"GAME-CONFIG"].filter(Boolean).join(" + ");console.log(`[UI] Mode: ${d.yellow}`),n&&console.log(`[UI] Cache: ${"CLEARED".red}`),console.log(""),await build(l,{doCode:c,doAtlas:i,clear:n}),s&&await buildGameConfig(l),r&&await startWatchMode(l,{doCode:c,doAtlas:i,doGameConfig:s})}async function build(e,{doCode:o,doAtlas:t,clear:a,hint:n={}}){const r=Date.now();let s;try{s=loadProjectConfig(e)}catch(e){return void console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red)}const c=path.dirname(e),i=path.join(e,"pages"),l=path.join(c,s.codeExportPath,"layaMaxUI.ts"),d=path.join(e,".custom-cmd-cache"),g=path.join(d,"code-cache.json"),p=path.join(d,"atlas-cache.json"),h=path.join(d,"ani-cache.json"),u=o&&!n.atlasOnly,y=t&&!n.codeOnly,f=[],m=!!n&&Object.keys(n).length>0?()=>{}:e=>console.log(` ${e}`);u&&f.push(generateUICode({pagesDir:i,outputPath:l,cachePath:g,projectPath:c,resExportPath:path.join(c,s.resExportPath),clear:a,changedScenes:n.changedScenes||null,log:m}).then(e=>{console.log(`[UI] 📄 UI Code: ${String(e.built).green} built, ${e.skipped} skipped`)}).catch(e=>{console.error(`[UI] ${"❌ CODE ERROR".red}: ${e.message}`)})),y&&f.push((async()=>{try{const o=path.join(e,"styles.xml"),t=loadStyles(o);await generateAtlas({styleMap:t,projectConfig:s,assetDir:path.join(e,"assets"),outputDir:path.join(c,s.resExportPath),cachePath:p,clear:a,changedDirs:n.changedDirs||null,log:m}),console.log(`[UI] 🖼 Atlas: ${"done".green}`)}catch(e){console.error(`[UI] ${"❌ ATLAS ERROR".red}: ${e.message}`)}})()),f.push(generateAni({pagesDir:i,outputDir:path.join(c,s.resExportPath),cachePath:h,clear:a,log:m}).then(e=>{e.copied>0&&console.log(`[UI] 🎞 Ani: ${String(e.copied).green} copied, ${e.skipped} skipped`)}).catch(e=>{console.error(`[UI] ${"❌ ANI ERROR".red}: ${e.message}`)})),await Promise.all(f);const C=((Date.now()-r)/1e3).toFixed(2),j=(new Date).toLocaleTimeString();console.log(`[UI] ✅ ${j} — Build complete in ${C}s\n`)}async function buildGameConfig(e){let o;try{o=loadProjectConfig(e)}catch(e){return void console.error(`[GameConfig] ${"❌ ERROR loading .laya".red}: ${e.message}`)}const t=path.dirname(e),a=path.join(e,"pages"),n=path.join(t,"src","GameConfig.ts");try{const e=await generateGameConfig({pagesDir:a,outputPath:n,projectConfig:o,log:()=>{}});e.written&&console.log(`[UI] ⚙ GameConfig: ${"Updated".green} (${e.total} scripts)`)}catch(e){console.error(`[UI] ${"❌ GAME-CONFIG ERROR".red}: ${e.message}`)}}async function startWatchMode(e,{doCode:o,doAtlas:t,doGameConfig:a}){let n;try{n=require("chokidar")}catch{console.error("[WATCH] chokidar not installed. Run: npm install chokidar".red),process.exit(1)}const r=path.join(e,"pages"),s=path.join(e,"assets"),c=path.join(e,"styles.xml"),i=path.join(e,"pageStyles.xml");console.log("[UI] 👀 Watching pages/ & assets/ — Ctrl+C để dừng"),console.log("[UI] .scene → code | image → atlas | .xml → full rebuild\n");let l=null,d=new Set,g=new Set,p=new Set,h=!1;function u(){clearTimeout(l),l=setTimeout(async()=>{const n=[...d],r=[...g],i=[...p],l=h;d.clear(),g.clear(),p.clear(),h=!1;const u=[...n,...r,...i,...l?[c]:[]];if(0===u.length)return;if(console.log("\n[UI] 📝 Đã sửa:"),u.map(o=>path.relative(e,o).replace(/\\/g,"/")).forEach(e=>console.log(`[UI] • ${e}`)),l)return console.log("[UI] ↻ XML config changed — full rebuild..."),void await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{}});const y=o&&n.length>0,f=t&&i.length>0;if(!y&&!f)return;let m=null;f&&(m=new Set(i.map(e=>path.relative(s,path.dirname(e)).replace(/\\/g,"/"))));let C=null;y&&(C=new Set(n)),await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{codeOnly:y&&!f,atlasOnly:f&&!y,changedDirs:m,changedScenes:C}}),a&&(n.length>0||r.length>0)&&await buildGameConfig(e)},300)}function y(e){d.add(e),u()}function f(e){g.add(e),u()}function m(e){p.add(e),u()}function C(e){e===c&&invalidateStyles(),e===i&&invalidatePages(),h=!0,u()}const j={ignoreInitial:!0,usePolling:!1};o&&n.watch(path.join(r,"**","*.scene"),j).on("add",y).on("change",y).on("unlink",y).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),a&&n.watch([path.join(r,"**","*.scene"),path.join(r,"**","*.prefab")],j).on("add",e=>e.endsWith(".prefab")?f(e):y(e)).on("change",e=>e.endsWith(".prefab")?f(e):y(e)).on("unlink",e=>e.endsWith(".prefab")?f(e):y(e)).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),t&&n.watch([`${s}/**/*.png`,`${s}/**/*.jpg`],j).on("add",m).on("change",m).on("unlink",m).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),n.watch(`${r}/**/*.ani`,j).on("add",e=>{p.add(e),u()}).on("change",e=>{p.add(e),u()}).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),n.watch([c,i],j).on("add",C).on("change",C).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red))}async function runGameConfig(e){const o=path.resolve(e.project);let t;fs.existsSync(o)||(console.error(`[ERROR] Project directory not found: ${o}`.red),process.exit(1));try{t=loadProjectConfig(o)}catch(e){console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red),process.exit(1)}const a=path.dirname(o),n=path.join(o,"pages"),r=e.output?path.resolve(e.output):path.join(a,"src","GameConfig.ts");console.log(`\n${"⚡ Custom LayaAir2 Build Tool".cyan.bold}`),console.log(`[GameConfig] Project: ${o.gray}`),console.log(`[GameConfig] Output: ${r.gray}`),console.log("");const s=Date.now();try{const e=await generateGameConfig({pagesDir:n,outputPath:r,projectConfig:t,log:e=>console.log(` ${e}`)}),o=((Date.now()-s)/1e3).toFixed(2),a=(new Date).toLocaleTimeString(),c=e.written?"Updated".green:"No changes".gray;console.log(`[GameConfig] ✅ ${a} — ${c} (${e.total} scripts) in ${o}s\n`)}catch(e){console.error(`[GameConfig] ${"❌ ERROR".red}: ${e.message}`),process.exit(1)}}program.name("tyhuynh-laya-cmd").description("Custom LayaAir 2 fast incremental UI build tool").version("1.0.1"),program.command("ui").description("Export UI code and/or atlas").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("-a, --atlas","Generate atlas sprite sheets",!1).option("-d, --code","Generate TypeScript UI code",!1).option("-g, --game-config","Generate GameConfig.ts",!1).option("-c, --clear","Clear cache (force full rebuild)",!1).option("-w, --watch","Watch mode",!1).action(async e=>{e.project||(e.project=autoDetectLayaDir(),e.project||(console.error("[ERROR] Cannot find laya/ directory. Use -p <dir> to specify manually.".red),process.exit(1)),console.log(` Auto-detected: ${e.project.gray}`)),await runUI(e)}),program.command("migrate").description("Migrate stray assets from bin/ into laya/assets/ and update styles.xml").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("--dry-run","Preview changes without writing anything",!1).action(e=>{const o=[];e.dryRun&&o.push("--dry-run"),e.project&&o.push("-p",e.project);const{spawnSync:t}=require("child_process"),a=require.resolve("./scripts/migrate-stray-assets.js"),n=t(process.execPath,[a,...o],{stdio:"inherit"});process.exit(n.status||0)}),program.command("game-config").description("Generate src/GameConfig.ts by scanning all .scene files for Script components").option("-p, --project <dir>","Path to laya/ directory (auto-detected if omitted)",null).option("-o, --output <file>","Output path for GameConfig.ts (default: <projectRoot>/src/GameConfig.ts)",null).action(async e=>{e.project||(e.project=autoDetectLayaDir(),e.project||(console.error("[ERROR] Cannot find laya/ directory. Use -p <dir> to specify manually.".red),process.exit(1)),console.log(` Auto-detected: ${e.project.gray}`)),await runGameConfig(e)}),program.parse(process.argv);
@@ -0,0 +1 @@
1
+ "use strict";const path=require("path"),fs=require("fs-extra"),{getMtime:getMtime,collectFiles:collectFiles,writeIfChanged:writeIfChanged}=require("../utils/FileUtils");async function generateAni({pagesDir:e,outputDir:t,cachePath:i,clear:n=!1,log:r=console.log}){const s=collectFiles(e,".ani");if(0===s.length)return{copied:0,skipped:0};let c={};if(!n)try{c=fs.readJsonSync(i)}catch{}let a=0,l=0;for(const i of s){const s=path.relative(e,i).replace(/\\/g,"/"),o=path.join(t,s),p=getMtime(i);n||!c[s]||c[s]!==p||!fs.existsSync(o)?(fs.ensureDirSync(path.dirname(o)),fs.copyFileSync(i,o),c[s]=p,a++,r(`[AniGen] ✓ ${s}`)):l++}return fs.ensureDirSync(path.dirname(i)),writeIfChanged(i,JSON.stringify(c,null,2)),{copied:a,skipped:l}}module.exports={generateAni:generateAni};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyhuynh-laya-cmd",
3
- "version": "1.0.12",
3
+ "version": "1.0.17",
4
4
  "description": "Custom LayaAir 2 UI build tool - fast incremental atlas and UI code generation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -43,4 +43,4 @@
43
43
  "devDependencies": {
44
44
  "terser": "^5.46.1"
45
45
  }
46
- }
46
+ }