10 个 TDD 任务:工具层脚手架、paths/junction/scaffold 库、 setup-links/new-game/check-cocos-version/bump-cocos 脚本、种子工程、端到端冒烟。 纯 Node ESM + node:test,无第三方依赖;遵循 spec §2/§7/§8/§0.1。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1179 lines
40 KiB
Markdown
1179 lines
40 KiB
Markdown
# Monorepo 骨架与工具链 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 搭建 Cocos Creator 子游戏 monorepo 的目录骨架与工具链脚本,做到「一键 `new-game` 生成挂好 framework junction、共享框架真源的子游戏工程」,消灭旧架构「拷贝整份模板」的痛点。
|
||
|
||
**Architecture:** 框架真源唯一存放于 `cocoscreator_projects/YouleNexus/assets/framework`;各子游戏工程在 `cocoscreator_projects/games/<name>/` 下,通过 Windows junction 把 `assets/framework` 链接到真源实现即时共享。工具链是一组纯 Node ESM 脚本(`setup-links`/`new-game`/`check-cocos-version`/`bump-cocos`),核心逻辑抽到可单测的 `lib/` 纯函数,CLI 入口仅做参数解析与输出。
|
||
|
||
**Tech Stack:** Node.js 20(ESM `.mjs`)、`node:test` + `node:assert` 内置测试、`node:fs`/`node:crypto`/`node:url` 标准库;无第三方依赖。
|
||
|
||
> 本计划遵循 spec `docs/superpowers/specs/2026-06-28-cocos-framework-design.md` 的 §2、§7、§8 与 §0.1(协议 SSOT:脚手架产物只引用 `docs/protocol`,不内联协议细节)。
|
||
|
||
---
|
||
|
||
## 目录与文件结构(本计划建立)
|
||
|
||
```
|
||
cocoscreator_projects/
|
||
├─ package.json # monorepo 工具层(type:module + npm scripts)
|
||
├─ YouleNexus/ # 已存在(框架宿主工程)
|
||
│ └─ assets/framework/ # 框架真源占位结构(本计划建 6 层空目录 + README)
|
||
├─ games/ # 子游戏工程根(本计划建,含 .gitkeep)
|
||
├─ templates/game-seed/ # 种子工程(Task 7 从 YouleNexus 派生)
|
||
└─ scripts/
|
||
├─ lib/
|
||
│ ├─ paths.mjs # 路径常量 + 列工程 + 读 creator.version
|
||
│ ├─ junction.mjs # junction 创建/检测(跨平台)
|
||
│ └─ scaffold.mjs # 复制种子 / 改身份 / 铺 game 骨架
|
||
├─ setup-links.mjs # 遍历 games/* 重建 framework junction
|
||
├─ new-game.mjs # 一键新建子游戏工程
|
||
├─ check-cocos-version.mjs # 校验各工程 creator.version 一致
|
||
├─ bump-cocos.mjs # 批量改 creator.version
|
||
├─ README.md # 工具链使用说明
|
||
└─ test/
|
||
├─ helpers.mjs # 临时 monorepo fixture 构造
|
||
├─ paths.test.mjs
|
||
├─ junction.test.mjs
|
||
├─ setup-links.test.mjs
|
||
├─ scaffold.test.mjs
|
||
├─ new-game.test.mjs
|
||
├─ check-cocos-version.test.mjs
|
||
└─ bump-cocos.test.mjs
|
||
```
|
||
|
||
**所有命令的工作目录均为 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects`**(下称 monorepo 根),除非另行说明。
|
||
|
||
---
|
||
|
||
### Task 1: monorepo 工具层脚手架
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/package.json`
|
||
- Create: `cocoscreator_projects/games/.gitkeep`
|
||
- Create: `cocoscreator_projects/YouleNexus/assets/framework/README.md`
|
||
- Create: `cocoscreator_projects/YouleNexus/assets/framework/{core,net,protocol,platform,ui,sdk}/.gitkeep`
|
||
- Modify: `.gitignore`(仓库根,修正 games 路径前缀)
|
||
|
||
- [ ] **Step 1: 建立工具层 package.json**
|
||
|
||
创建 `cocoscreator_projects/package.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "youle-monorepo-tools",
|
||
"private": true,
|
||
"type": "module",
|
||
"version": "0.1.0",
|
||
"scripts": {
|
||
"test": "node --test scripts/test/",
|
||
"setup-links": "node scripts/setup-links.mjs",
|
||
"new-game": "node scripts/new-game.mjs",
|
||
"check-cocos": "node scripts/check-cocos-version.mjs",
|
||
"bump-cocos": "node scripts/bump-cocos.mjs"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 建立框架真源占位结构**
|
||
|
||
创建 `cocoscreator_projects/YouleNexus/assets/framework/README.md`:
|
||
|
||
```markdown
|
||
# framework — 框架真源(唯一权威副本)
|
||
|
||
各子游戏工程的 `assets/framework` 是指向**本目录**的 junction(见 `scripts/setup-links.mjs`)。
|
||
六层结构(依赖严格单向,详见 spec §3):core / net / protocol / platform / ui / sdk。
|
||
|
||
协议相关实现一律以 `docs/protocol/` 为唯一信息源(spec §0.1),不在代码或注释中内联复制协议字段。
|
||
```
|
||
|
||
为六层各建一个 `.gitkeep` 空文件占位:
|
||
`core/.gitkeep`、`net/.gitkeep`、`protocol/.gitkeep`、`platform/.gitkeep`、`ui/.gitkeep`、`sdk/.gitkeep`。
|
||
|
||
创建 `cocoscreator_projects/games/.gitkeep`(空文件,使空目录可入库)。
|
||
|
||
- [ ] **Step 3: 修正仓库根 .gitignore 的 games 路径**
|
||
|
||
`games/` 实际位于 `cocoscreator_projects/games/`,修正 `.gitignore` 中的链接忽略规则。
|
||
|
||
将 `.gitignore` 中这两行:
|
||
|
||
```
|
||
games/*/assets/framework
|
||
games/*/assets/framework.meta
|
||
```
|
||
|
||
替换为:
|
||
|
||
```
|
||
cocoscreator_projects/games/*/assets/framework
|
||
cocoscreator_projects/games/*/assets/framework.meta
|
||
```
|
||
|
||
- [ ] **Step 4: 验证 .gitignore 行为**
|
||
|
||
Run: `cd G:/Works/YouleGamesCocosCreator && git check-ignore cocoscreator_projects/games/demo/assets/framework`
|
||
Expected: 输出 `cocoscreator_projects/games/demo/assets/framework`(命中规则即正确)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/package.json cocoscreator_projects/games/.gitkeep cocoscreator_projects/YouleNexus/assets/framework .gitignore
|
||
git commit -m "chore(monorepo): 工具层脚手架 + 框架真源占位结构
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: `lib/paths.mjs` — 路径解析与工程发现
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/helpers.mjs`
|
||
- Create: `cocoscreator_projects/scripts/test/paths.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/lib/paths.mjs`
|
||
|
||
- [ ] **Step 1: 写测试 fixture 辅助 + 失败测试**
|
||
|
||
创建 `scripts/test/helpers.mjs`:
|
||
|
||
```js
|
||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
|
||
// 构造一个临时 monorepo:YouleNexus(host) + framework 真源 + 空 games/
|
||
export function makeTempMonorepo() {
|
||
const root = mkdtempSync(join(tmpdir(), 'youle-mono-'));
|
||
const host = join(root, 'YouleNexus');
|
||
const framework = join(host, 'assets', 'framework');
|
||
mkdirSync(framework, { recursive: true });
|
||
writeFileSync(
|
||
join(host, 'package.json'),
|
||
JSON.stringify({ name: 'YouleNexus', uuid: 'host-uuid', creator: { version: '3.8.8' } }, null, 2)
|
||
);
|
||
writeFileSync(join(framework, 'README.md'), '# framework src\n');
|
||
const games = join(root, 'games');
|
||
mkdirSync(games, { recursive: true });
|
||
return { root, host, framework, games };
|
||
}
|
||
|
||
// 在 gamesDir 下造一个子游戏工程(含 package.json)
|
||
export function makeGameProject(gamesDir, name, version = '3.8.8') {
|
||
const dir = join(gamesDir, name);
|
||
mkdirSync(join(dir, 'assets'), { recursive: true });
|
||
writeFileSync(
|
||
join(dir, 'package.json'),
|
||
JSON.stringify({ name, uuid: `${name}-uuid`, creator: { version } }, null, 2)
|
||
);
|
||
return dir;
|
||
}
|
||
|
||
// 造一个最小种子工程
|
||
export function makeSeed(root) {
|
||
const seed = join(root, 'templates', 'game-seed');
|
||
mkdirSync(join(seed, 'assets'), { recursive: true });
|
||
mkdirSync(join(seed, 'settings'), { recursive: true });
|
||
writeFileSync(
|
||
join(seed, 'package.json'),
|
||
JSON.stringify({ name: 'game-seed', uuid: 'seed-uuid', creator: { version: '3.8.8' } }, null, 2)
|
||
);
|
||
writeFileSync(join(seed, 'settings', 'v2.json'), '{}\n');
|
||
return seed;
|
||
}
|
||
|
||
export function cleanup(root) {
|
||
rmSync(root, { recursive: true, force: true });
|
||
}
|
||
```
|
||
|
||
创建 `scripts/test/paths.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||
import { makeTempMonorepo, makeGameProject, cleanup } from './helpers.mjs';
|
||
import { readCreatorVersion, listGameProjects } from '../lib/paths.mjs';
|
||
|
||
test('readCreatorVersion 读取 package.json 的 creator.version', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
assert.equal(readCreatorVersion(mono.host), '3.8.8');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('readCreatorVersion 缺字段返回 null', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const dir = makeGameProject(mono.games, 'nover');
|
||
// 覆写成无 creator 字段
|
||
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'nover' }));
|
||
assert.equal(readCreatorVersion(dir), null);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('listGameProjects 只列出含 package.json 的子目录', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
makeGameProject(mono.games, 'doudizhu');
|
||
makeGameProject(mono.games, 'niuniu');
|
||
mkdirSync(join(mono.games, 'not-a-project')); // 无 package.json,应被忽略
|
||
const found = listGameProjects(mono.games).map((p) => p.split(/[\\/]/).pop()).sort();
|
||
assert.deepEqual(found, ['doudizhu', 'niuniu']);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('listGameProjects 对不存在的目录返回空数组', () => {
|
||
assert.deepEqual(listGameProjects('/no/such/dir'), []);
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/paths.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../lib/paths.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `lib/paths.mjs`**
|
||
|
||
创建 `scripts/lib/paths.mjs`:
|
||
|
||
```js
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join, resolve } from 'node:path';
|
||
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||
|
||
// 本文件位于 <monorepo>/scripts/lib/paths.mjs → 上溯两级为 monorepo 根
|
||
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||
export const HOST_PROJECT = join(ROOT, 'YouleNexus');
|
||
export const FRAMEWORK_SRC = join(HOST_PROJECT, 'assets', 'framework');
|
||
export const GAMES_DIR = join(ROOT, 'games');
|
||
export const SEED_DIR = join(ROOT, 'templates', 'game-seed');
|
||
|
||
export function readCreatorVersion(projectDir) {
|
||
try {
|
||
const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'));
|
||
return pkg?.creator?.version ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function listGameProjects(gamesDir = GAMES_DIR) {
|
||
if (!existsSync(gamesDir)) return [];
|
||
return readdirSync(gamesDir, { withFileTypes: true })
|
||
.filter((d) => d.isDirectory())
|
||
.map((d) => join(gamesDir, d.name))
|
||
.filter((dir) => existsSync(join(dir, 'package.json')));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/paths.test.mjs`
|
||
Expected: PASS — 4 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/lib/paths.mjs cocoscreator_projects/scripts/test/helpers.mjs cocoscreator_projects/scripts/test/paths.test.mjs
|
||
git commit -m "feat(tools): paths.mjs 路径解析与工程发现
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: `lib/junction.mjs` — junction 创建与检测
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/junction.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/lib/junction.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/junction.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||
import { makeTempMonorepo, cleanup } from './helpers.mjs';
|
||
import { ensureJunction, isLink } from '../lib/junction.mjs';
|
||
|
||
test('ensureJunction 创建链接后可经链接读到真源文件', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const link = join(mono.games, 'demo-framework');
|
||
const status = ensureJunction(link, mono.framework);
|
||
assert.equal(status, 'created');
|
||
assert.ok(isLink(link));
|
||
assert.equal(readFileSync(join(link, 'README.md'), 'utf8'), '# framework src\n');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('ensureJunction 对已存在链接返回 exists 且不抛错', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const link = join(mono.games, 'demo-framework');
|
||
ensureJunction(link, mono.framework);
|
||
assert.equal(ensureJunction(link, mono.framework), 'exists');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('ensureJunction 目标不存在时抛错', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
assert.throws(() => ensureJunction(join(mono.games, 'x'), join(mono.root, 'no-such')), /target 不存在/);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('ensureJunction 拒绝覆盖已存在的真实目录/文件', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const link = join(mono.games, 'realfile');
|
||
writeFileSync(link, 'x');
|
||
assert.throws(() => ensureJunction(link, mono.framework), /非链接/);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/junction.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../lib/junction.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `lib/junction.mjs`**
|
||
|
||
创建 `scripts/lib/junction.mjs`:
|
||
|
||
```js
|
||
import { symlinkSync, lstatSync, existsSync } from 'node:fs';
|
||
import { resolve } from 'node:path';
|
||
import { platform } from 'node:process';
|
||
|
||
// Windows 用 junction(免管理员、仅限目录);其它平台用 dir 符号链接
|
||
const LINK_TYPE = platform === 'win32' ? 'junction' : 'dir';
|
||
|
||
export function isLink(p) {
|
||
try {
|
||
return lstatSync(p).isSymbolicLink();
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 在 linkPath 建一个指向 targetPath 的目录链接。
|
||
// 返回 'created' | 'exists';目标不存在或 linkPath 已被真实文件占用则抛错。
|
||
export function ensureJunction(linkPath, targetPath) {
|
||
const target = resolve(targetPath); // junction 要求绝对目标
|
||
if (!existsSync(target)) {
|
||
throw new Error(`junction target 不存在: ${target}`);
|
||
}
|
||
if (isLink(linkPath)) return 'exists';
|
||
if (existsSync(linkPath)) {
|
||
throw new Error(`路径已存在且非链接,拒绝覆盖: ${linkPath}`);
|
||
}
|
||
symlinkSync(target, linkPath, LINK_TYPE);
|
||
return 'created';
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/junction.test.mjs`
|
||
Expected: PASS — 4 tests passed
|
||
|
||
> 注:若在非 Windows CI 上 `dir` symlink 需权限,本仓库目标平台为 Windows(junction 免管理员),开发机直接通过。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/lib/junction.mjs cocoscreator_projects/scripts/test/junction.test.mjs
|
||
git commit -m "feat(tools): junction.mjs 跨平台目录链接创建/检测
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: `setup-links.mjs` — 批量重建 framework junction
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/setup-links.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/setup-links.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/setup-links.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { readFileSync } from 'node:fs';
|
||
import { makeTempMonorepo, makeGameProject, cleanup } from './helpers.mjs';
|
||
import { setupLinks } from '../setup-links.mjs';
|
||
import { isLink } from '../lib/junction.mjs';
|
||
|
||
test('setupLinks 为每个子游戏工程建立 assets/framework junction', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
makeGameProject(mono.games, 'doudizhu');
|
||
makeGameProject(mono.games, 'niuniu');
|
||
const results = setupLinks(mono.games, mono.framework);
|
||
assert.equal(results.length, 2);
|
||
for (const r of results) {
|
||
assert.equal(r.status, 'created');
|
||
assert.ok(isLink(r.link));
|
||
assert.equal(readFileSync(join(r.link, 'README.md'), 'utf8'), '# framework src\n');
|
||
}
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('setupLinks 二次运行幂等(已存在则 exists)', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
makeGameProject(mono.games, 'doudizhu');
|
||
setupLinks(mono.games, mono.framework);
|
||
const second = setupLinks(mono.games, mono.framework);
|
||
assert.equal(second[0].status, 'exists');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/setup-links.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../setup-links.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `setup-links.mjs`**
|
||
|
||
创建 `scripts/setup-links.mjs`:
|
||
|
||
```js
|
||
#!/usr/bin/env node
|
||
import { join } from 'node:path';
|
||
import { mkdirSync, existsSync } from 'node:fs';
|
||
import { pathToFileURL } from 'node:url';
|
||
import { GAMES_DIR, FRAMEWORK_SRC, listGameProjects } from './lib/paths.mjs';
|
||
import { ensureJunction } from './lib/junction.mjs';
|
||
|
||
// 为 gamesDir 下每个工程在 assets/framework 建指向 frameworkSrc 的 junction
|
||
export function setupLinks(gamesDir, frameworkSrc) {
|
||
const results = [];
|
||
for (const proj of listGameProjects(gamesDir)) {
|
||
const assetsDir = join(proj, 'assets');
|
||
if (!existsSync(assetsDir)) mkdirSync(assetsDir, { recursive: true });
|
||
const link = join(assetsDir, 'framework');
|
||
const status = ensureJunction(link, frameworkSrc);
|
||
results.push({ project: proj, link, status });
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// CLI 入口
|
||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||
const results = setupLinks(GAMES_DIR, FRAMEWORK_SRC);
|
||
if (results.length === 0) {
|
||
console.log('games/ 下暂无子游戏工程。');
|
||
}
|
||
for (const r of results) {
|
||
console.log(`[${r.status}] ${r.link}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/setup-links.test.mjs`
|
||
Expected: PASS — 2 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/setup-links.mjs cocoscreator_projects/scripts/test/setup-links.test.mjs
|
||
git commit -m "feat(tools): setup-links 批量重建 framework junction
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: `lib/scaffold.mjs` — 复制种子 / 改身份 / 铺 game 骨架
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/scaffold.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/lib/scaffold.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/scaffold.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||
import { makeTempMonorepo, makeSeed, cleanup } from './helpers.mjs';
|
||
import { copySeed, setProjectIdentity, scaffoldGameAssets } from '../lib/scaffold.mjs';
|
||
|
||
test('copySeed 复制种子并清理缓存目录', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
mkdirSync(join(seed, 'library'), { recursive: true }); // 模拟缓存
|
||
writeFileSync(join(seed, 'library', 'x'), '1');
|
||
const dest = join(mono.games, 'demo');
|
||
copySeed(seed, dest);
|
||
assert.ok(existsSync(join(dest, 'package.json')));
|
||
assert.ok(existsSync(join(dest, 'settings', 'v2.json')));
|
||
assert.ok(!existsSync(join(dest, 'library')), 'library 应被清理');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('copySeed 拒绝覆盖已存在目标', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const dest = join(mono.games, 'demo');
|
||
copySeed(seed, dest);
|
||
assert.throws(() => copySeed(seed, dest), /已存在/);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('setProjectIdentity 改 name 并生成新 uuid', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const dest = join(mono.games, 'demo');
|
||
copySeed(seed, dest);
|
||
const pkg = setProjectIdentity(dest, 'doudizhu');
|
||
assert.equal(pkg.name, 'doudizhu');
|
||
assert.notEqual(pkg.uuid, 'seed-uuid');
|
||
assert.match(pkg.uuid, /^[0-9a-f-]{36}$/);
|
||
const onDisk = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'));
|
||
assert.equal(onDisk.name, 'doudizhu');
|
||
assert.equal(onDisk.creator.version, '3.8.8'); // 引擎版本保留
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('scaffoldGameAssets 铺出 game 骨架(GameModule/theme/override)', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const dest = join(mono.games, 'demo');
|
||
copySeed(seed, dest);
|
||
scaffoldGameAssets(dest, 'doudizhu');
|
||
const gm = readFileSync(join(dest, 'assets', 'game', 'GameModule.ts'), 'utf8');
|
||
assert.match(gm, /doudizhu/);
|
||
assert.match(gm, /docs\/protocol/); // 提醒协议 SSOT
|
||
assert.ok(existsSync(join(dest, 'assets', 'game', 'theme.ts')));
|
||
assert.ok(existsSync(join(dest, 'assets', 'game', 'override', '.gitkeep')));
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/scaffold.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../lib/scaffold.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `lib/scaffold.mjs`**
|
||
|
||
创建 `scripts/lib/scaffold.mjs`:
|
||
|
||
```js
|
||
import { cpSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { randomUUID } from 'node:crypto';
|
||
|
||
const CACHE_DIRS = ['library', 'temp', 'local', 'build'];
|
||
|
||
// 复制种子工程到 destDir,并清理可能残留的缓存目录
|
||
export function copySeed(seedDir, destDir) {
|
||
if (existsSync(destDir)) throw new Error(`目标已存在: ${destDir}`);
|
||
cpSync(seedDir, destDir, { recursive: true });
|
||
for (const c of CACHE_DIRS) {
|
||
rmSync(join(destDir, c), { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
// 修改工程 package.json 的 name 并分配全新 uuid(保留 creator.version 等其它字段)
|
||
export function setProjectIdentity(projectDir, name) {
|
||
const pkgPath = join(projectDir, 'package.json');
|
||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
||
pkg.name = name;
|
||
pkg.uuid = randomUUID();
|
||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||
return pkg;
|
||
}
|
||
|
||
// 铺出子游戏私有 game/ 骨架:IGameModule 占位 + theme 占位 + override 目录
|
||
export function scaffoldGameAssets(projectDir, name) {
|
||
const gameDir = join(projectDir, 'assets', 'game');
|
||
mkdirSync(join(gameDir, 'override'), { recursive: true });
|
||
writeFileSync(join(gameDir, 'override', '.gitkeep'), '');
|
||
writeFileSync(join(gameDir, 'GameModule.ts'), gameModuleStub(name));
|
||
writeFileSync(join(gameDir, 'theme.ts'), themeStub(name));
|
||
}
|
||
|
||
function gameModuleStub(name) {
|
||
return `/**
|
||
* ${name} 子游戏模块骨架。
|
||
* TODO(Plan 5): 待 framework/sdk 的 IGameModule / GameContext 接口定稿后实现
|
||
* (route / onEnter / onReceive / onReconnect 等)。
|
||
* 协议字段一律参见 docs/protocol/(spec §0.1:只引用、不内联复制)。
|
||
*/
|
||
export const GAME_ROUTE = '${name}';
|
||
`;
|
||
}
|
||
|
||
function themeStub(name) {
|
||
return `/**
|
||
* ${name} 主题覆盖骨架。
|
||
* TODO(Plan 4): 待 framework/ui 的皮肤变量清单定稿后填写主色/字体/图集映射等。
|
||
*/
|
||
export const theme = {};
|
||
`;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/scaffold.test.mjs`
|
||
Expected: PASS — 4 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/lib/scaffold.mjs cocoscreator_projects/scripts/test/scaffold.test.mjs
|
||
git commit -m "feat(tools): scaffold.mjs 种子复制/改身份/铺 game 骨架
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: `new-game.mjs` — 一键新建子游戏工程
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/new-game.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/new-game.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/new-game.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { existsSync, readFileSync } from 'node:fs';
|
||
import { makeTempMonorepo, makeSeed, cleanup } from './helpers.mjs';
|
||
import { newGame } from '../new-game.mjs';
|
||
import { isLink } from '../lib/junction.mjs';
|
||
|
||
test('newGame 生成工程:改身份 + 挂 framework junction + 铺 game 骨架', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const result = newGame('doudizhu', { seedDir: seed, gamesDir: mono.games, frameworkSrc: mono.framework });
|
||
const dest = join(mono.games, 'doudizhu');
|
||
assert.equal(result.projectDir, dest);
|
||
// 身份
|
||
const pkg = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'));
|
||
assert.equal(pkg.name, 'doudizhu');
|
||
assert.notEqual(pkg.uuid, 'seed-uuid');
|
||
// junction 指向真源
|
||
const link = join(dest, 'assets', 'framework');
|
||
assert.ok(isLink(link));
|
||
assert.equal(readFileSync(join(link, 'README.md'), 'utf8'), '# framework src\n');
|
||
// game 骨架
|
||
assert.ok(existsSync(join(dest, 'assets', 'game', 'GameModule.ts')));
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('newGame 拒绝重名工程', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const opts = { seedDir: seed, gamesDir: mono.games, frameworkSrc: mono.framework };
|
||
newGame('doudizhu', opts);
|
||
assert.throws(() => newGame('doudizhu', opts), /已存在/);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('newGame 校验名称非空且合法', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const seed = makeSeed(mono.root);
|
||
const opts = { seedDir: seed, gamesDir: mono.games, frameworkSrc: mono.framework };
|
||
assert.throws(() => newGame('', opts), /名称/);
|
||
assert.throws(() => newGame('Bad Name!', opts), /名称/);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/new-game.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../new-game.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `new-game.mjs`**
|
||
|
||
创建 `scripts/new-game.mjs`:
|
||
|
||
```js
|
||
#!/usr/bin/env node
|
||
import { join } from 'node:path';
|
||
import { pathToFileURL } from 'node:url';
|
||
import { SEED_DIR, GAMES_DIR, FRAMEWORK_SRC } from './lib/paths.mjs';
|
||
import { copySeed, setProjectIdentity, scaffoldGameAssets } from './lib/scaffold.mjs';
|
||
import { ensureJunction } from './lib/junction.mjs';
|
||
|
||
const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
||
|
||
// 新建一款子游戏工程。opts 可注入路径用于测试。
|
||
export function newGame(name, opts = {}) {
|
||
const seedDir = opts.seedDir ?? SEED_DIR;
|
||
const gamesDir = opts.gamesDir ?? GAMES_DIR;
|
||
const frameworkSrc = opts.frameworkSrc ?? FRAMEWORK_SRC;
|
||
|
||
if (!name || !NAME_RE.test(name)) {
|
||
throw new Error(`非法子游戏名称: "${name}"(仅限小写字母/数字/-/_,且以字母数字开头)`);
|
||
}
|
||
const projectDir = join(gamesDir, name);
|
||
|
||
copySeed(seedDir, projectDir); // 目标已存在会抛错
|
||
setProjectIdentity(projectDir, name);
|
||
scaffoldGameAssets(projectDir, name);
|
||
const link = join(projectDir, 'assets', 'framework');
|
||
ensureJunction(link, frameworkSrc);
|
||
|
||
return { projectDir, link };
|
||
}
|
||
|
||
// CLI 入口
|
||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||
const name = process.argv[2];
|
||
if (!name) {
|
||
console.error('用法: npm run new-game <name>');
|
||
process.exit(1);
|
||
}
|
||
const { projectDir } = newGame(name);
|
||
console.log(`已创建子游戏工程: ${projectDir}`);
|
||
console.log(`下一步: 用 Cocos Creator 打开该目录`);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/new-game.test.mjs`
|
||
Expected: PASS — 3 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/new-game.mjs cocoscreator_projects/scripts/test/new-game.test.mjs
|
||
git commit -m "feat(tools): new-game 一键新建子游戏工程
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: 建立真实种子工程 `templates/game-seed`
|
||
|
||
> 数据准备任务(非 TDD):从 YouleNexus 派生一个最小可用的空 Cocos 工程作为种子。new-game 的逻辑已在 Task 6 用 fixture 种子验证;本任务产出运行时使用的真实种子。
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/templates/game-seed/`(从 YouleNexus 派生)
|
||
|
||
- [ ] **Step 1: 从 YouleNexus 复制工程级文件(排除业务与缓存)**
|
||
|
||
Run(在 monorepo 根执行;只复制工程配置,排除 assets 业务内容、缓存、扩展、framework):
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||
mkdir -p templates/game-seed
|
||
cp -r YouleNexus/.creator templates/game-seed/ 2>/dev/null || true
|
||
cp -r YouleNexus/settings templates/game-seed/ 2>/dev/null || true
|
||
cp YouleNexus/package.json templates/game-seed/package.json
|
||
mkdir -p templates/game-seed/assets
|
||
```
|
||
|
||
- [ ] **Step 2: 把种子 package.json 的 name 改为 game-seed**
|
||
|
||
编辑 `templates/game-seed/package.json`,将 `"name": "YouleNexus"` 改为 `"name": "game-seed"`,保留 `creator.version`(`3.8.8`)。`uuid` 字段保持原值即可(new-game 会重新分配)。
|
||
|
||
- [ ] **Step 3: 校验种子是空且不含 framework/extensions/缓存**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||
ls templates/game-seed && echo "--- 不应出现 framework/extensions/library/temp ---" && \
|
||
( [ ! -e templates/game-seed/assets/framework ] && [ ! -e templates/game-seed/extensions ] && \
|
||
[ ! -e templates/game-seed/library ] && echo "OK: 种子干净" || echo "ERROR: 种子含不应有的内容" )
|
||
```
|
||
|
||
Expected: 打印 `OK: 种子干净`
|
||
|
||
- [ ] **Step 4: 用真实种子做一次 new-game 冒烟(随后删除产物)**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||
node scripts/new-game.mjs smoke-demo && \
|
||
test -f games/smoke-demo/assets/game/GameModule.ts && \
|
||
node -e "const fs=require('fs');const t=fs.lstatSync('games/smoke-demo/assets/framework');console.log('framework isSymlink:', t.isSymbolicLink())" && \
|
||
rm -rf games/smoke-demo
|
||
```
|
||
|
||
Expected: 打印 `framework isSymlink: true`,无错误;产物已删除
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/templates/game-seed
|
||
git commit -m "chore(tools): 建立真实种子工程 templates/game-seed
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: `check-cocos-version.mjs` — 引擎版本一致性校验
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/check-cocos-version.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/check-cocos-version.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/check-cocos-version.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { makeTempMonorepo, makeGameProject, cleanup } from './helpers.mjs';
|
||
import { checkVersions } from '../check-cocos-version.mjs';
|
||
|
||
test('checkVersions 全部一致时 mismatches 为空', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
makeGameProject(mono.games, 'a', '3.8.8');
|
||
makeGameProject(mono.games, 'b', '3.8.8');
|
||
const r = checkVersions(mono.host, mono.games);
|
||
assert.equal(r.expected, '3.8.8');
|
||
assert.deepEqual(r.mismatches, []);
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('checkVersions 报告与宿主不一致的工程', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
makeGameProject(mono.games, 'a', '3.8.8');
|
||
makeGameProject(mono.games, 'b', '3.8.6');
|
||
const r = checkVersions(mono.host, mono.games);
|
||
assert.equal(r.mismatches.length, 1);
|
||
assert.match(r.mismatches[0].project, /[\\/]b$/);
|
||
assert.equal(r.mismatches[0].version, '3.8.6');
|
||
assert.equal(r.mismatches[0].expected, '3.8.8');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/check-cocos-version.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../check-cocos-version.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `check-cocos-version.mjs`**
|
||
|
||
创建 `scripts/check-cocos-version.mjs`:
|
||
|
||
```js
|
||
#!/usr/bin/env node
|
||
import { pathToFileURL } from 'node:url';
|
||
import { HOST_PROJECT, GAMES_DIR, readCreatorVersion, listGameProjects } from './lib/paths.mjs';
|
||
|
||
// 比对各子游戏工程与宿主工程的 creator.version
|
||
export function checkVersions(hostProject, gamesDir) {
|
||
const expected = readCreatorVersion(hostProject);
|
||
const mismatches = [];
|
||
for (const proj of listGameProjects(gamesDir)) {
|
||
const version = readCreatorVersion(proj);
|
||
if (version !== expected) {
|
||
mismatches.push({ project: proj, version, expected });
|
||
}
|
||
}
|
||
return { expected, mismatches };
|
||
}
|
||
|
||
// CLI 入口:有不一致则退出码 1
|
||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||
const { expected, mismatches } = checkVersions(HOST_PROJECT, GAMES_DIR);
|
||
console.log(`宿主 Cocos 版本: ${expected}`);
|
||
if (mismatches.length === 0) {
|
||
console.log('OK: 所有子游戏工程版本一致。');
|
||
process.exit(0);
|
||
}
|
||
console.error('版本不一致:');
|
||
for (const m of mismatches) {
|
||
console.error(` ${m.project}: ${m.version}(应为 ${m.expected})`);
|
||
}
|
||
process.exit(1);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/check-cocos-version.test.mjs`
|
||
Expected: PASS — 2 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/check-cocos-version.mjs cocoscreator_projects/scripts/test/check-cocos-version.test.mjs
|
||
git commit -m "feat(tools): check-cocos-version 引擎版本一致性校验
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: `bump-cocos.mjs` — 批量升级引擎版本字段
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/test/bump-cocos.test.mjs`
|
||
- Create: `cocoscreator_projects/scripts/bump-cocos.mjs`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
创建 `scripts/test/bump-cocos.test.mjs`:
|
||
|
||
```js
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import { makeTempMonorepo, makeGameProject, cleanup } from './helpers.mjs';
|
||
import { bumpVersion } from '../bump-cocos.mjs';
|
||
|
||
test('bumpVersion 改写各工程 creator.version 并报告 from/to', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const a = makeGameProject(mono.games, 'a', '3.8.8');
|
||
const changed = bumpVersion([mono.host, a], '3.8.9');
|
||
assert.equal(changed.length, 2);
|
||
assert.equal(changed[1].from, '3.8.8');
|
||
assert.equal(changed[1].to, '3.8.9');
|
||
const pkgA = JSON.parse(readFileSync(join(a, 'package.json'), 'utf8'));
|
||
assert.equal(pkgA.creator.version, '3.8.9');
|
||
const pkgHost = JSON.parse(readFileSync(join(mono.host, 'package.json'), 'utf8'));
|
||
assert.equal(pkgHost.creator.version, '3.8.9');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
|
||
test('bumpVersion 对缺 creator 字段的工程补建后写入', () => {
|
||
const mono = makeTempMonorepo();
|
||
try {
|
||
const a = makeGameProject(mono.games, 'a', '3.8.8');
|
||
writeFileSync(join(a, 'package.json'), JSON.stringify({ name: 'a' }));
|
||
bumpVersion([a], '3.8.9');
|
||
const pkgA = JSON.parse(readFileSync(join(a, 'package.json'), 'utf8'));
|
||
assert.equal(pkgA.creator.version, '3.8.9');
|
||
} finally {
|
||
cleanup(mono.root);
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
Run: `node --test scripts/test/bump-cocos.test.mjs`
|
||
Expected: FAIL — 报 `Cannot find module '../bump-cocos.mjs'`
|
||
|
||
- [ ] **Step 3: 实现 `bump-cocos.mjs`**
|
||
|
||
创建 `scripts/bump-cocos.mjs`:
|
||
|
||
```js
|
||
#!/usr/bin/env node
|
||
import { join } from 'node:path';
|
||
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import { pathToFileURL } from 'node:url';
|
||
import { HOST_PROJECT, GAMES_DIR, listGameProjects } from './lib/paths.mjs';
|
||
|
||
// 批量把若干工程的 creator.version 改为 version,返回每个工程的 from/to
|
||
export function bumpVersion(projectDirs, version) {
|
||
const changed = [];
|
||
for (const dir of projectDirs) {
|
||
const pkgPath = join(dir, 'package.json');
|
||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
||
if (!pkg.creator) pkg.creator = {};
|
||
const from = pkg.creator.version ?? null;
|
||
pkg.creator.version = version;
|
||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||
changed.push({ dir, from, to: version });
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
// CLI 入口:bump 宿主 + 所有子游戏到指定版本
|
||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||
const version = process.argv[2];
|
||
if (!version) {
|
||
console.error('用法: npm run bump-cocos <version> 例: npm run bump-cocos 3.8.9');
|
||
process.exit(1);
|
||
}
|
||
const targets = [HOST_PROJECT, ...listGameProjects(GAMES_DIR)];
|
||
const changed = bumpVersion(targets, version);
|
||
for (const c of changed) {
|
||
console.log(`${c.dir}: ${c.from} → ${c.to}`);
|
||
}
|
||
console.log('\n⚠️ 仍需用新版 Cocos Creator 逐个打开各工程执行迁移(library 重建)。');
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
Run: `node --test scripts/test/bump-cocos.test.mjs`
|
||
Expected: PASS — 2 tests passed
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/bump-cocos.mjs cocoscreator_projects/scripts/test/bump-cocos.test.mjs
|
||
git commit -m "feat(tools): bump-cocos 批量升级引擎版本字段
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: 全量测试、端到端冒烟与工具链 README
|
||
|
||
**Files:**
|
||
- Create: `cocoscreator_projects/scripts/README.md`
|
||
|
||
- [ ] **Step 1: 运行全部单测**
|
||
|
||
Run: `cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects && node --test scripts/test/`
|
||
Expected: PASS — 全部测试通过(7 个测试文件,21 个用例)
|
||
|
||
- [ ] **Step 2: 端到端冒烟(真实种子 → new-game → check-version → 清理)**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||
node scripts/new-game.mjs e2e-demo && \
|
||
node scripts/check-cocos-version.mjs && \
|
||
node scripts/setup-links.mjs && \
|
||
rm -rf games/e2e-demo && \
|
||
echo "E2E OK"
|
||
```
|
||
|
||
Expected: 依次打印新建工程路径、`OK: 所有子游戏工程版本一致。`、junction 状态、`E2E OK`
|
||
|
||
- [ ] **Step 3: 编写工具链 README**
|
||
|
||
创建 `scripts/README.md`:
|
||
|
||
```markdown
|
||
# Monorepo 工具链
|
||
|
||
所有命令在 `cocoscreator_projects/` 下用 `npm run <script>` 执行。
|
||
|
||
| 命令 | 作用 |
|
||
|------|------|
|
||
| `npm run new-game <name>` | 从 `templates/game-seed` 克隆一个子游戏工程到 `games/<name>`,重置身份、铺 `game/` 骨架、挂 framework junction |
|
||
| `npm run setup-links` | 为 `games/*` 重建 `assets/framework` junction(clone 仓库后必跑一次) |
|
||
| `npm run check-cocos` | 校验所有子游戏工程的 `creator.version` 与宿主 `YouleNexus` 一致(不一致退出码 1) |
|
||
| `npm run bump-cocos <version>` | 批量改宿主+所有子游戏的 `creator.version`(之后仍需逐个用编辑器打开迁移) |
|
||
| `npm test` | 运行工具链全部单元测试 |
|
||
|
||
## 关键约束
|
||
- **framework 真源**唯一存放于 `YouleNexus/assets/framework`;各子游戏的 `assets/framework` 是指向它的 junction(不入库,见根 `.gitignore`)。clone 后跑 `setup-links` 重建。
|
||
- **Cocos 版本必须全 monorepo 统一**(junction 共享同一份资源,跨版本不兼容)。升级走 `bump-cocos` + 逐工程编辑器迁移,详见 spec §8。
|
||
- 子游戏私有内容只放 `games/<name>/assets/game/`;协议一律引用 `docs/protocol/`(spec §0.1)。
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd G:/Works/YouleGamesCocosCreator
|
||
git add cocoscreator_projects/scripts/README.md
|
||
git commit -m "docs(tools): 工具链 README 与端到端冒烟通过
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 验收标准(Plan 1 完成定义)
|
||
|
||
1. `node --test scripts/test/` 全绿(7 文件、21 用例)。
|
||
2. `npm run new-game <name>` 能生成一个:身份独立(新 uuid)、挂好 framework junction、含 `assets/game/` 骨架的子游戏工程。
|
||
3. `npm run setup-links` 幂等重建 junction;`npm run check-cocos` 在版本一致时退出码 0、不一致时为 1。
|
||
4. 根 `.gitignore` 正确忽略 `cocoscreator_projects/games/*/assets/framework`。
|
||
5. 框架真源 `YouleNexus/assets/framework` 六层占位结构入库;junction 与缓存不入库。
|
||
|
||
## 后续计划衔接
|
||
- **Plan 2(框架内核 net+protocol+core)** 开工前需先:依 `docs/protocol/01、04` 落 `protocol/` 的 TS 类型、确定响应式 Store 选型(spec §9)。
|
||
- 框架真源开始有实质代码后,本计划建立的 junction 共享与版本校验即为其提供工程底座。
|