feat(adapter): E2 从 sourceProject 真构建 formalize——scaffold-from-source + 单测

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 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-17 13:51:20 -07:00
parent 046454d95e
commit d763755117
2 changed files with 100 additions and 0 deletions

View File

@ -0,0 +1,59 @@
'use strict';
/**
* scaffold-from-source.mjs E2 sourceProject 真构建的工件落地U2plan 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/<game>/)到适配器的相对 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 SourceProjectsource-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.mjsSourceProject → 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/<x>/ , 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 <此> <out> --global-name=__GameBundle
}

View File

@ -0,0 +1,41 @@
/**
* scaffold.test.mjs E2 buildFactorySource 零依赖 node 单测node --test
* owner前端+引擎线Mac计划 002 U2SourceProject generated-factory.js 文本工件正确
*
* scaffoldFromSource fs 编排 + build.mjs 全链是集成段( esbuild),已由 _adapter-probe 真九门验证(commit 046454d9);
* 本单测只覆盖纯文本生成 buildFactorySource( fs/ esbuild, package.jsonlane 本机零依赖只跑 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), '不应再含标准路径');
});