tyhuynh-laya-cmd 1.0.5 → 1.0.7

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 ADDED
@@ -0,0 +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)
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"),{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,l=a||!t&&!a,c=t||!t&&!a,s=path.resolve(o);fs.existsSync(s)||(console.error(`[ERROR] Project directory not found: ${s}`.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: ${s.gray}`),console.log(`[UI] Mode: ${[l&&"CODE",c&&"ATLAS"].filter(Boolean).join(" + ").yellow}`),n&&console.log(`[UI] Cache: ${"CLEARED".red}`),console.log(""),await build(s,{doCode:l,doAtlas:c,clear:n}),r&&await startWatchMode(s,{doCode:l,doAtlas:c})}async function build(e,{doCode:o,doAtlas:t,clear:a,hint:n={}}){const r=Date.now();let l;try{l=loadProjectConfig(e)}catch(e){return void console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red)}const c=path.dirname(e),s=path.join(e,"pages"),i=path.join(c,l.codeExportPath,"layaMaxUI.ts"),d=path.join(e,".custom-cmd-cache"),p=path.join(d,"code-cache.json"),h=path.join(d,"atlas-cache.json"),g=o&&!n.atlasOnly,u=t&&!n.codeOnly,y=[],f=!!n&&Object.keys(n).length>0?()=>{}:e=>console.log(` ${e}`);g&&y.push(generateUICode({pagesDir:s,outputPath:i,cachePath:p,projectPath:c,resExportPath:path.join(c,l.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}`)})),u&&y.push((async()=>{try{const o=path.join(e,"styles.xml"),t=loadStyles(o);await generateAtlas({styleMap:t,projectConfig:l,assetDir:path.join(e,"assets"),outputDir:path.join(c,l.resExportPath),cachePath:h,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),j=(new Date).toLocaleTimeString();console.log(`[UI] ✅ ${j} — Build complete in ${m}s\n`)}async function startWatchMode(e,{doCode:o,doAtlas:t}){let a;try{a=require("chokidar")}catch{console.error("[WATCH] chokidar not installed. Run: npm install chokidar".red),process.exit(1)}const n=path.join(e,"pages"),r=path.join(e,"assets"),l=path.join(e,"styles.xml"),c=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 s=null,i=new Set,d=new Set,p=!1;function h(){clearTimeout(s),s=setTimeout(async()=>{const a=[...i],n=[...d],c=p;i.clear(),d.clear(),p=!1;const s=[...a,...n,...c?[l]:[]];if(0===s.length)return;if(console.log("\n[UI] 📝 Đã sửa:"),s.map(o=>path.relative(e,o).replace(/\\/g,"/")).forEach(e=>console.log(`[UI] • ${e}`)),c)return console.log("[UI] ↻ XML config changed — full rebuild..."),void await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{}});const h=o&&a.length>0,g=t&&n.length>0;if(!h&&!g)return;let u=null;g&&(u=new Set(n.map(e=>path.relative(r,path.dirname(e)).replace(/\\/g,"/"))));let y=null;h&&(y=new Set(a)),await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{codeOnly:h&&!g,atlasOnly:g&&!h,changedDirs:u,changedScenes:y}})},300)}function g(e){i.add(e),h()}function u(e){d.add(e),h()}function y(e){e===l&&invalidateStyles(),e===c&&invalidatePages(),p=!0,h()}const f={ignoreInitial:!0,usePolling:!1};o&&a.watch(path.join(n,"**","*.scene"),f).on("add",g).on("change",g).on("unlink",g).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),t&&a.watch([`${r}/**/*.png`,`${r}/**/*.jpg`],f).on("add",u).on("change",u).on("unlink",u).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),a.watch([l,c],f).on("add",y).on("change",y).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red))}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("-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.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"),{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,r=o;for(;r!==a;){const o=path.join(r,"laya");if(e(o))return o;a=r,r=path.dirname(r)}return null}async function runUI(e){const{project:o,atlas:t,code:a,clear:r,watch:n}=e,s=a||!t&&!a,l=t||!t&&!a,c=path.resolve(o);fs.existsSync(c)||(console.error(`[ERROR] Project directory not found: ${c}`.red),process.exit(1)),n?(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: ${c.gray}`),console.log(`[UI] Mode: ${[s&&"CODE",l&&"ATLAS"].filter(Boolean).join(" + ").yellow}`),r&&console.log(`[UI] Cache: ${"CLEARED".red}`),console.log(""),await build(c,{doCode:s,doAtlas:l,clear:r}),n&&await startWatchMode(c,{doCode:s,doAtlas:l})}async function build(e,{doCode:o,doAtlas:t,clear:a,hint:r={}}){const n=Date.now();let s;try{s=loadProjectConfig(e)}catch(e){return void console.error(`[ERROR] Cannot load .laya config: ${e.message}`.red)}const l=path.dirname(e),c=path.join(e,"pages"),i=path.join(l,s.codeExportPath,"layaMaxUI.ts"),d=path.join(e,".custom-cmd-cache"),p=path.join(d,"code-cache.json"),h=path.join(d,"atlas-cache.json"),u=o&&!r.atlasOnly,g=t&&!r.codeOnly,y=[],m=!!r&&Object.keys(r).length>0?()=>{}:e=>console.log(` ${e}`);u&&y.push(generateUICode({pagesDir:c,outputPath:i,cachePath:p,projectPath:l,resExportPath:path.join(l,s.resExportPath),clear:a,changedScenes:r.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}`)})),g&&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(l,s.resExportPath),cachePath:h,clear:a,changedDirs:r.changedDirs||null,log:m}),console.log(`[UI] 🖼 Atlas: ${"done".green}`)}catch(e){console.error(`[UI] ${"❌ ATLAS ERROR".red}: ${e.message}`)}})()),await Promise.all(y);const f=((Date.now()-n)/1e3).toFixed(2),j=(new Date).toLocaleTimeString();console.log(`[UI] ✅ ${j} — Build complete in ${f}s\n`)}async function startWatchMode(e,{doCode:o,doAtlas:t}){let a;try{a=require("chokidar")}catch{console.error("[WATCH] chokidar not installed. Run: npm install chokidar".red),process.exit(1)}const r=path.join(e,"pages"),n=path.join(e,"assets"),s=path.join(e,"styles.xml"),l=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 c=null,i=new Set,d=new Set,p=!1;function h(){clearTimeout(c),c=setTimeout(async()=>{const a=[...i],r=[...d],l=p;i.clear(),d.clear(),p=!1;const c=[...a,...r,...l?[s]:[]];if(0===c.length)return;if(console.log("\n[UI] 📝 Đã sửa:"),c.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 h=o&&a.length>0,u=t&&r.length>0;if(!h&&!u)return;let g=null;u&&(g=new Set(r.map(e=>path.relative(n,path.dirname(e)).replace(/\\/g,"/"))));let y=null;h&&(y=new Set(a)),await build(e,{doCode:o,doAtlas:t,clear:!1,hint:{codeOnly:h&&!u,atlasOnly:u&&!h,changedDirs:g,changedScenes:y}})},300)}function u(e){i.add(e),h()}function g(e){d.add(e),h()}function y(e){e===s&&invalidateStyles(),e===l&&invalidatePages(),p=!0,h()}const m={ignoreInitial:!0,usePolling:!1};o&&a.watch(path.join(r,"**","*.scene"),m).on("add",u).on("change",u).on("unlink",u).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),t&&a.watch([`${n}/**/*.png`,`${n}/**/*.jpg`],m).on("add",g).on("change",g).on("unlink",g).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red)),a.watch([s,l],m).on("add",y).on("change",y).on("error",e=>console.error(`[WATCH ERROR] ${e}`.red))}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("-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"),r=t(process.execPath,[a,...o],{stdio:"inherit"});process.exit(r.status??0)}),program.parse(process.argv);
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const path=require("path"),fs=require("fs-extra"),args=process.argv.slice(2),dryRun=args.includes("--dry-run"),pIdx=args.indexOf("-p"),layaDir=path.resolve(-1!==pIdx?args[pIdx+1]:"../../client/laya"),assetsDir=path.join(layaDir,"assets"),binDir=path.join(layaDir,"../bin"),stylesXml=path.join(layaDir,"styles.xml");console.log("\n🔍 Migrate stray assets"+(dryRun?" [DRY RUN]":"")),console.log(` laya : ${layaDir}`),console.log(` assets : ${assetsDir}`),console.log(` bin : ${binDir}\n`);const IMAGE_EXTS=new Set([".png",".jpg",".jpeg",".webp"]);function collectImages(e){const s=[];return function e(t){let n;try{n=fs.readdirSync(t,{withFileTypes:!0})}catch{return}for(const o of n){const n=path.join(t,o.name);o.isDirectory()?e(n):IMAGE_EXTS.has(path.extname(o.name).toLowerCase())&&s.push(n)}}(e),s}const binResDir=path.join(binDir,"res","image");fs.existsSync(binResDir)||(console.error(`❌ bin/res/image not found: ${binResDir}`),process.exit(1));const binImages=collectImages(binResDir),strayFiles=[],atlasBasenamesByDir=new Map;function collectAtlasInfo(e){let s;try{s=fs.readdirSync(e,{withFileTypes:!0})}catch{return}for(const t of s){const s=path.join(e,t.name);if(t.isDirectory())collectAtlasInfo(s);else if(".atlas"===path.extname(t.name).toLowerCase()){const s=path.basename(t.name,".atlas");atlasBasenamesByDir.has(e)||atlasBasenamesByDir.set(e,new Set),atlasBasenamesByDir.get(e).add(s)}}}function isAtlasPage(e){const s=path.dirname(e),t=atlasBasenamesByDir.get(s);if(!t)return!1;const n=path.basename(e,path.extname(e));for(const e of t){if(n===e)return!0;if(n.startsWith(e)&&/^\d+$/.test(n.slice(e.length)))return!0}return!1}collectAtlasInfo(binDir);for(const e of binImages){const s=path.relative(binDir,e).replace(/\\/g,"/"),t=path.join(assetsDir,s);fs.existsSync(t)||(isAtlasPage(e)||strayFiles.push({rel:s,binAbs:e,assetsAbs:t}))}0===strayFiles.length&&(console.log("✅ No stray files found — everything is already in assets/"),process.exit(0)),console.log(`Found ${strayFiles.length} stray file(s):\n`);for(const{rel:e}of strayFiles)console.log(` + ${e}`);console.log("");const dirGroups=new Map,preExistingAssetDirs=new Set;for(const{rel:e}of strayFiles){const s=e.substring(0,e.lastIndexOf("/"));dirGroups.has(s)||dirGroups.set(s,[]),dirGroups.get(s).push(e),fs.existsSync(path.join(assetsDir,s))&&preExistingAssetDirs.add(s)}if(dryRun)console.log("📦 (Dry run) Skipping moving files...");else{console.log("📦 Moving files to laya/assets/ ...");for(const{rel:e,binAbs:s,assetsAbs:t}of strayFiles)fs.ensureDirSync(path.dirname(t)),fs.copyFileSync(s,t),console.log(` ✓ ${e}`)}console.log("\n📝 "+(dryRun?"(Dry run preview) ":"")+'Updating styles.xml with pack="2" entries ...'),fs.existsSync(stylesXml)||(console.error(`❌ styles.xml not found: ${stylesXml}`),process.exit(1));let xmlContent=fs.readFileSync(stylesXml,"utf-8");const existingNames=new Set;for(const e of xmlContent.matchAll(/name="([^"]+)"/g))existingNames.add(e[1]);const newItems=[];function makeItem(e){return` <item name="${e}" type="Sprite" compress="0" pack="2" quality="80" props="" picType="0" scale="0"/>`}for(const[e,s]of dirGroups){if(!preExistingAssetDirs.has(e))existingNames.has(e)?console.log(` ℹ Skipped dir (already in styles.xml): ${e}`):(newItems.push(makeItem(e)),console.log(` + ${e}/ (new directory → dir rule)`));else for(const e of s)existingNames.has(e)?console.log(` ℹ Skipped (already in styles.xml): ${e}`):(newItems.push(makeItem(e)),console.log(` + ${e}`))}if(newItems.length>0){const e=xmlContent.lastIndexOf("</res>");-1===e&&(console.error("❌ Could not find </res> in styles.xml"),process.exit(1));const s=newItems.join("\n")+"\n";xmlContent=xmlContent.slice(0,e)+s+xmlContent.slice(e),dryRun&&(console.log(`\n✅ (Dry run preview) styles.xml WOULD BE updated with ${newItems.length} new entries.`),process.exit(0)),fs.writeFileSync(stylesXml,xmlContent,"utf-8"),console.log(`\n✅ styles.xml updated with ${newItems.length} new entries.`)}else console.log("\n✅ styles.xml already up-to-date (all entries existed)."),dryRun&&process.exit(0);console.log("\nDone! Next steps:\n 1. Run: npm run build:clean\n → AtlasGen will copy the new assets to bin/ going forward\n 2. Verify the game still works (files are already in bin/)\n 3. Commit laya/assets/ and styles.xml to version control\n");
2
+ "use strict";const path=require("path"),fs=require("fs-extra");function autoDetectLayaDir(){const e=e=>{const s=path.join(e,".laya");return fs.existsSync(s)&&fs.statSync(s).isFile()};let s=process.cwd();if(e(s))return s;const t=path.join(s,"laya");if(e(t))return t;let n=null,o=s;for(;o!==n;){const s=path.join(o,"laya");if(e(s))return s;n=o,o=path.dirname(o)}return null}const args=process.argv.slice(2),dryRun=args.includes("--dry-run"),pIdx=args.indexOf("-p");let layaDir;-1!==pIdx?layaDir=path.resolve(args[pIdx+1]):(layaDir=autoDetectLayaDir(),layaDir||(console.error("\n[migrate] ❌ Không tìm thấy thư mục laya/."),console.error("[migrate] Hãy chạy lệnh này từ thư mục dự án client/"),console.error("[migrate] hoặc dùng: tyhuynh-laya-cmd migrate -p <path/to/laya>\n"),process.exit(1)),console.log(`[migrate] Auto-detected: ${layaDir}`));const assetsDir=path.join(layaDir,"assets"),binDir=path.join(layaDir,"../bin"),stylesXml=path.join(layaDir,"styles.xml");console.log("\n🔍 Migrate stray assets"+(dryRun?" [DRY RUN]":"")),console.log(` laya : ${layaDir}`),console.log(` assets : ${assetsDir}`),console.log(` bin : ${binDir}\n`);const IMAGE_EXTS=new Set([".png",".jpg",".jpeg",".webp"]);function collectImages(e){const s=[];return function e(t){let n;try{n=fs.readdirSync(t,{withFileTypes:!0})}catch{return}for(const o of n){const n=path.join(t,o.name);o.isDirectory()?e(n):IMAGE_EXTS.has(path.extname(o.name).toLowerCase())&&s.push(n)}}(e),s}const binResDir=path.join(binDir,"res","image");fs.existsSync(binResDir)||(console.error(`❌ bin/res/image not found: ${binResDir}`),process.exit(1));const binImages=collectImages(binResDir),strayFiles=[],atlasBasenamesByDir=new Map;function collectAtlasInfo(e){let s;try{s=fs.readdirSync(e,{withFileTypes:!0})}catch{return}for(const t of s){const s=path.join(e,t.name);if(t.isDirectory())collectAtlasInfo(s);else if(".atlas"===path.extname(t.name).toLowerCase()){const s=path.basename(t.name,".atlas");atlasBasenamesByDir.has(e)||atlasBasenamesByDir.set(e,new Set),atlasBasenamesByDir.get(e).add(s)}}}function isAtlasPage(e){const s=path.dirname(e),t=atlasBasenamesByDir.get(s);if(!t)return!1;const n=path.basename(e,path.extname(e));for(const e of t){if(n===e)return!0;if(n.startsWith(e)&&/^\d+$/.test(n.slice(e.length)))return!0}return!1}collectAtlasInfo(binDir);for(const e of binImages){const s=path.relative(binDir,e).replace(/\\/g,"/"),t=path.join(assetsDir,s);fs.existsSync(t)||(isAtlasPage(e)||strayFiles.push({rel:s,binAbs:e,assetsAbs:t}))}0===strayFiles.length&&(console.log("✅ No stray files found — everything is already in assets/"),process.exit(0)),console.log(`Found ${strayFiles.length} stray file(s):\n`);for(const{rel:e}of strayFiles)console.log(` + ${e}`);console.log("");const dirGroups=new Map,preExistingAssetDirs=new Set;for(const{rel:e}of strayFiles){const s=e.substring(0,e.lastIndexOf("/"));dirGroups.has(s)||dirGroups.set(s,[]),dirGroups.get(s).push(e),fs.existsSync(path.join(assetsDir,s))&&preExistingAssetDirs.add(s)}if(dryRun)console.log("📦 (Dry run) Skipping moving files...");else{console.log("📦 Moving files to laya/assets/ ...");for(const{rel:e,binAbs:s,assetsAbs:t}of strayFiles)fs.ensureDirSync(path.dirname(t)),fs.copyFileSync(s,t),console.log(` ✓ ${e}`)}console.log("\n📝 "+(dryRun?"(Dry run preview) ":"")+'Updating styles.xml with pack="2" entries ...'),fs.existsSync(stylesXml)||(console.error(`❌ styles.xml not found: ${stylesXml}`),process.exit(1));let xmlContent=fs.readFileSync(stylesXml,"utf-8");const existingNames=new Set;for(const e of xmlContent.matchAll(/name="([^"]+)"/g))existingNames.add(e[1]);const newItems=[];function makeItem(e){return` <item name="${e}" type="Sprite" compress="0" pack="2" quality="80" props="" picType="0" scale="0"/>`}for(const[e,s]of dirGroups){if(!preExistingAssetDirs.has(e))existingNames.has(e)?console.log(` ℹ Skipped dir (already in styles.xml): ${e}`):(newItems.push(makeItem(e)),console.log(` + ${e}/ (new directory → dir rule)`));else for(const e of s)existingNames.has(e)?console.log(` ℹ Skipped (already in styles.xml): ${e}`):(newItems.push(makeItem(e)),console.log(` + ${e}`))}if(newItems.length>0){const e=xmlContent.lastIndexOf("</res>");-1===e&&(console.error("❌ Could not find </res> in styles.xml"),process.exit(1));const s=newItems.join("\n")+"\n";xmlContent=xmlContent.slice(0,e)+s+xmlContent.slice(e),dryRun&&(console.log(`\n✅ (Dry run preview) styles.xml WOULD BE updated with ${newItems.length} new entries.`),process.exit(0)),fs.writeFileSync(stylesXml,xmlContent,"utf-8"),console.log(`\n✅ styles.xml updated with ${newItems.length} new entries.`)}else console.log("\n✅ styles.xml already up-to-date (all entries existed)."),dryRun&&process.exit(0);console.log("\nDone! Next steps:\n 1. Run: npm run build:clean\n → AtlasGen will copy the new assets to bin/ going forward\n 2. Verify the game still works (files are already in bin/)\n 3. Commit laya/assets/ and styles.xml to version control\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyhuynh-laya-cmd",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {