feat(tools): scaffold.mjs 种子复制/改身份/铺 game 骨架

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:12:59 +08:00
co-authored by Claude Opus 4.8
parent 9a0a7f60c1
commit 8bb3b80544
2 changed files with 122 additions and 0 deletions
@@ -0,0 +1,53 @@
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 = {};
`;
}
@@ -0,0 +1,69 @@
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);
}
});