From d7637551177b2ecc64d83fb4549cb602c1c883c2 Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 17 Jun 2026 13:51:20 -0700 Subject: [PATCH] =?UTF-8?q?feat(adapter):=20E2=20=E4=BB=8E=20sourceProject?= =?UTF-8?q?=20=E7=9C=9F=E6=9E=84=E5=BB=BA=20formalize=E2=80=94=E2=80=94sca?= =?UTF-8?q?ffold-from-source=20+=20=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U2(plan 002):把「sourceProject → 可构建 game 工件」从手工探针formalize成可复用函数, 即后端确定性 modify guard-pending 在等的引擎线交付物(plan 001 U6 / plan 002 KTD2)。 - buildFactorySource(sourceProject)→ generated-factory.js 文本(适配器 import + 内联 source + 默认导出工厂)。 - scaffoldFromSource(...)→ 写 generated-factory + 拷 entry-bundle/index 模板,返 build.mjs 入口。 - 零依赖单测 3/3:文本合契约 / 内联 source 往返深等 / adapterImport 可覆盖。 - e2e 验(集成段,本机):scaffoldFromSource 落变体源(5 目标)→ build.mjs → 211KB bundle(LittleJS 真入包); 端到端九门由 _adapter-probe 验过(046454d9)。 Co-Authored-By: Claude Opus 4.8 --- .../src/adapter/scaffold-from-source.mjs | 59 +++++++++++++++++++ game-runtime/src/adapter/scaffold.test.mjs | 41 +++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 game-runtime/src/adapter/scaffold-from-source.mjs create mode 100644 game-runtime/src/adapter/scaffold.test.mjs 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), '不应再含标准路径'); +});