diff --git a/game-runtime/src/adapter/scaffold-from-source.mjs b/game-runtime/src/adapter/scaffold-from-source.mjs new file mode 100644 index 00000000..98ee2309 --- /dev/null +++ b/game-runtime/src/adapter/scaffold-from-source.mjs @@ -0,0 +1,59 @@ +'use strict'; +/** + * scaffold-from-source.mjs —— E2「从 sourceProject 真构建」的工件落地(U2,plan 002) + * owner:前端+引擎线(Mac) + * + * 【做什么】把存储的 SourceProject 落成可被 build.mjs 编译的 game 目录工件: + * ① buildFactorySource(sourceProject) → generated-factory.js 文本(适配器 import + 内联 source + 默认导出工厂); + * ② scaffoldFromSource(...) → 写 generated-factory.js + 拷 entry-bundle/index 模板,返 build.mjs 入口路径。 + * + * 【为什么】当前两路(worker/SAA)都直写 factorySrc(整工厂)、不消费 sourceProject;后端确定性 modify guard-pending + * 在「从 sourceProject 真构建」上(plan 001 U6 / plan 002 KTD2)。本模块 = 引擎线交付的那个落地工件: + * sourceProject →(本模块)→ generated-factory.js(entry-bundle 的固定 import 目标)→ build.mjs → __GameBundle。 + * 端到端已由 _adapter-probe 真九门验证(commit 046454d9)。 + * + * 【边界】纯文本生成 buildFactorySource 零 fs(可零依赖单测);scaffoldFromSource 做 fs 编排(集成段用,需模板路径)。 + */ + +import { writeFileSync, copyFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +/** 标准 game 目录(games/_wg1-gen//)到适配器的相对 import(与 entry-bundle 的 ../../../src 同深)。 */ +export const STD_ADAPTER_IMPORT = '../../../src/adapter/create-game-from-definition.js'; + +/** + * 生成 generated-factory.js 文本:适配器 import + 内联 SourceProject + 默认导出 GameHostFactory。 + * @param {object} sourceProject SourceProject(source-project.schema.json)。 + * @param {object} [opts] { adapterImport?:string } 适配器相对 import(默认标准深度)。 + * @returns {string} generated-factory.js 文件内容。 + */ +export function buildFactorySource(sourceProject, opts = {}) { + const adapterImport = opts.adapterImport || STD_ADAPTER_IMPORT; + // 内联 source 保证 esbuild 自包含打包(无运行期外部 JSON 请求)。2 空格缩进,可读 + diff 友好。 + const json = JSON.stringify(sourceProject, null, 2); + return [ + "'use strict';", + '// 自动生成(E2 scaffold-from-source.mjs):SourceProject → GameHostFactory。勿手改,改源重生成。', + `import { adaptGameDefinition } from '${adapterImport}';`, + `const SOURCE = ${json};`, + 'export default adaptGameDefinition(SOURCE);', + '', + ].join('\n'); +} + +/** + * 把 SourceProject 落成可构建 game 目录:写 generated-factory.js + 拷 entry-bundle/index 模板。 + * @param {object} args + * @param {object} args.sourceProject SourceProject。 + * @param {string} args.gameDir 目标 game 目录绝对路径(须为 games/_wg1-gen// 标准深度,否则传 opts.adapterImport)。 + * @param {string} args.sharedDir _shared 目录绝对路径(取 entry-bundle.template.js / index.template.html)。 + * @param {object} [args.opts] 透传 buildFactorySource opts。 + * @returns {string} build.mjs 入口路径(gameDir/entry-bundle.js)。 + */ +export function scaffoldFromSource({ sourceProject, gameDir, sharedDir, opts }) { + mkdirSync(gameDir, { recursive: true }); + writeFileSync(join(gameDir, 'generated-factory.js'), buildFactorySource(sourceProject, opts || {})); + copyFileSync(join(sharedDir, 'entry-bundle.template.js'), join(gameDir, 'entry-bundle.js')); + copyFileSync(join(sharedDir, 'index.template.html'), join(gameDir, 'index.html')); + return join(gameDir, 'entry-bundle.js'); // → node scripts/build.mjs <此> --global-name=__GameBundle +} diff --git a/game-runtime/src/adapter/scaffold.test.mjs b/game-runtime/src/adapter/scaffold.test.mjs new file mode 100644 index 00000000..18f78f8d --- /dev/null +++ b/game-runtime/src/adapter/scaffold.test.mjs @@ -0,0 +1,41 @@ +/** + * scaffold.test.mjs —— E2 buildFactorySource 零依赖 node 单测(node --test) + * owner:前端+引擎线(Mac)|计划 002 U2。验「SourceProject → generated-factory.js 文本」工件正确。 + * + * 注:scaffoldFromSource 的 fs 编排 + build.mjs 全链是集成段(需 esbuild),已由 _adapter-probe 真九门验证(commit 046454d9); + * 本单测只覆盖纯文本生成 buildFactorySource(零 fs/零 esbuild,守 package.json「lane 本机零依赖只跑 node --test」)。 + */ + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { buildFactorySource, STD_ADAPTER_IMPORT } from './scaffold-from-source.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'tap-target.source.json'), 'utf8')); + +test('buildFactorySource 产合契约 generated-factory 文本', () => { + const src = buildFactorySource(SOURCE); + // 适配器 import(标准深度)+ 默认导出工厂 + 内联 SOURCE。 + assert.ok(src.includes(`import { adaptGameDefinition } from '${STD_ADAPTER_IMPORT}'`), '应含适配器 import'); + assert.ok(src.includes('export default adaptGameDefinition(SOURCE)'), '应默认导出工厂'); + assert.ok(src.includes('const SOURCE ='), '应内联 SOURCE 常量'); +}); + +test('内联 SOURCE 可往返 parse 且与原 SourceProject 深等', () => { + const src = buildFactorySource(SOURCE); + // 抽出 "const SOURCE = {...};" 的 JSON 体,JSON.parse 往返应与原 source 深等(无丢字段/无污染)。 + const m = src.match(/const SOURCE = ([\s\S]*?);\nexport default/); + assert.ok(m, '应能定位内联 SOURCE 字面量'); + const roundTrip = JSON.parse(m[1]); + assert.deepEqual(roundTrip, SOURCE, '内联 source 应与原 SourceProject 深等'); +}); + +test('adapterImport 可覆盖(非标准深度 game 目录)', () => { + const src = buildFactorySource(SOURCE, { adapterImport: '../../src/adapter/create-game-from-definition.js' }); + assert.ok(src.includes(`from '../../src/adapter/create-game-from-definition.js'`), '应用覆盖的 import 路径'); + assert.ok(!src.includes(STD_ADAPTER_IMPORT), '不应再含标准路径'); +});