From 8bb3b805440b3c27a11904047ec455db8c1b6d15 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 28 Jun 2026 12:12:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20scaffold.mjs=20=E7=A7=8D?= =?UTF-8?q?=E5=AD=90=E5=A4=8D=E5=88=B6/=E6=94=B9=E8=BA=AB=E4=BB=BD/?= =?UTF-8?q?=E9=93=BA=20game=20=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scripts/lib/scaffold.mjs | 53 ++++++++++++++ .../scripts/test/scaffold.test.mjs | 69 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 cocoscreator_projects/scripts/lib/scaffold.mjs create mode 100644 cocoscreator_projects/scripts/test/scaffold.test.mjs diff --git a/cocoscreator_projects/scripts/lib/scaffold.mjs b/cocoscreator_projects/scripts/lib/scaffold.mjs new file mode 100644 index 0000000..0948827 --- /dev/null +++ b/cocoscreator_projects/scripts/lib/scaffold.mjs @@ -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 = {}; +`; +} diff --git a/cocoscreator_projects/scripts/test/scaffold.test.mjs b/cocoscreator_projects/scripts/test/scaffold.test.mjs new file mode 100644 index 0000000..d334255 --- /dev/null +++ b/cocoscreator_projects/scripts/test/scaffold.test.mjs @@ -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); + } +});