games-development-ai/tier2/engine/build-phaser.mjs
zizi da184f067c fix(tier2): build-phaser 用 alias 垫片替 external 解 phaser——破 A_boot 工厂缺失
iife 格式下 external:['phaser'] 会把 import Phaser from 'phaser' 编成顶层
__require("phaser")(iife 里是抛 Dynamic require not supported 的存根),
bundle 顶层求值即抛 → var __Tier2GameBundle 拿不到赋值 → host 报「工厂缺失」(A_boot)。
改:新增 phaser-window-shim.js(default 导出 window.Phaser),build 用 esbuild alias
把 'phaser' 解析到它(绝对路径,不受 cwd 影响)。普通 ESM 解析不抛错,Phaser 本体仍不进
bundle(垫片只读 window.Phaser),与 host opts.engine 注入边界(Q4)同源。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:26:24 +00:00

133 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* tier2/engine/build-phaser.mjs —— tier2 Phaser 多文件源工程 esbuild 构建脚本A3 buildProfile 实现 · build 工具底座)
* ownertier2 引擎线M1-Phaser-scaffold+host 消费方A7 `build` 工具(单写 agent ReAct 核心环)/ harnessU3 构建段)
*
* ════════════════════════════════════════════════════════════════════════════
* 【这份是什么 · 对照左支单包 iife】
* 左支Tier0/1build = game-runtime/scripts/build.mjs单入口 `--global-name=__GameBundle` iife一个工厂文件
* tier2 右支是**多文件 src/ 工程**main.js 入口 + scene 树 + 系统模块互相 import——
* 本脚本用 esbuild bundle 把这棵 import 图打成一个 iife bundle入口 = src/main.js默认导出 PhaserGameFactory
* 挂全局名buildProfile.globalName缺省 __Tier2GameBundlehost 取它建 PhaserGameInstance。
*
* 【与 A3 源项目契约 buildProfile 对齐】tier2-source-project.schema.json
* buildProfile = { bundler: 'esbuild', format: 'iife', globalName, target, minify }。本脚本默认参数与之对齐。
* phaser 经 external 还是打进 bundletier2 spike 期 phaser 由宿主页面 <script> 预载并经 opts.engine 注入,
* 故构建时 external 'phaser'(不打进 bundle减体积 + 与宿主注入边界一致);游戏内 `import Phaser from 'phaser'`
* 在 iife 下解析为全局 window.Phaseresbuild external + globalName 映射,见 alias / external 配置)。
*
* 【运行环境纪律】(对齐左支 build.mjs
* 本脚本依赖 esbuild工具链依赖需 `npm i -D esbuild`**只在 mini-desktop 集成段跑**
* 6c6g 本机无 esbuild → 动态 import 失败时显式报错给指引不静默失败。6c6g 只做 node --check 静态校验。
*
* 用法(集成段 mini-desktop
* node build-phaser.mjs <工程根目录> [--global-name=NAME] [--out=bundle.iife.js]
* 例node build-phaser.mjs ../fixtures/mini-fei-e --global-name=__Tier2GameBundle
* ════════════════════════════════════════════════════════════════════════════
*/
'use strict';
import { argv, exit } from 'node:process';
import { join, isAbsolute, resolve, dirname } from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL, fileURLToPath } from 'node:url';
// 本脚本所在目录(tier2/engine/)——用于把 phaser alias 解析到同目录下的运行时垫片(绝对路径,
// 不受集成段 cwd=game-runtime 影响)。
const ENGINE_DIR = dirname(fileURLToPath(import.meta.url));
// phaser 运行时垫片:esbuild alias 把 'phaser' 解析到它,default 导出 window.Phaser(见 phaser-window-shim.js 注释)。
const PHASER_SHIM = join(ENGINE_DIR, 'phaser-window-shim.js');
/** tier2 锁定构建参数(与 A3 buildProfile 对齐spike 期可调,第二引擎落地后固化)。 */
export const TIER2_BUILD_OPTIONS = {
bundle: true, // 多文件 src/ 打成单 bundle
minify: false, // spike 期不 minify便于 headless 报错定位;产线化后开)
format: 'iife', // iife 挂全局名host 经 window[globalName] 取工厂
target: 'es2019', // 与左支同口径
sourcemap: false,
legalComments: 'none',
// Phaser 本体不打进 bundle、运行时绑宿主预载的 window.Phaser:用 alias 把 'phaser' 解析到运行时垫片
// phaser-window-shim.js(default 导出 window.Phaser),而非 external。
// 为什么不用 external:['phaser']——iife 格式下 external 会把 `import Phaser from 'phaser'` 编成顶层
// `__require("phaser")`(iife 里 __require 是抛 "Dynamic require not supported" 的存根),bundle 顶层求值即抛、
// __Tier2GameBundle 拿不到赋值(A_boot「工厂缺失」根因)。alias 到垫片则是普通 ESM 解析、不抛错,
// Phaser 本体仍不进 bundle(垫片只读 window.Phaser),与 host opts.engine 注入边界(Q4)同源。
alias: { phaser: PHASER_SHIM },
};
/**
* 构建一个 tier2 Phaser 源工程。集成段调用;本机无 esbuild 时抛错给指引。
* @param {string} projectDir 工程根目录(含 src/main.js 入口)
* @param {Object} [opts]
* @param {string} [opts.globalName='__Tier2GameBundle'] iife 全局变量名
* @param {string} [opts.entry='src/main.js'] 入口(对齐 A3 entry
* @param {string} [opts.outfile='bundle.iife.js'] 产物文件名(落工程根目录下)
* @returns {Promise<{ ok: boolean, bundlePath: string, log: string }>}
*/
export async function buildPhaserProject(projectDir, opts = {}) {
let esbuild;
try {
// 动态 import esbuild。本脚本在 tier2/engine/ 下,裸 import('esbuild') 按"模块位置"解析,
// 找不到 game-runtime/node_modules(构建依赖按 wg1 惯例装在 game-runtime)。
// 故:先裸 import(repo 根/tier2 装了才命中),失败再从 cwd 解析——集成段(smoke/run.py)总是
// 先 cd 到 game-runtime 再调本脚本,cwd 即 game-runtime、esbuild 在其 node_modules 下。
try {
esbuild = await import('esbuild');
} catch {
const requireFromCwd = createRequire(join(process.cwd(), 'package.json'));
esbuild = await import(pathToFileURL(requireFromCwd.resolve('esbuild')).href);
}
} catch {
const msg =
'[build-phaser] 未找到 esbuild —— 本脚本仅在集成段mini-desktop运行。\n' +
' 请在集成段执行npm i -D esbuild再重跑。\n' +
' 6c6g 本机请勿运行构建(纪律:禁 chrome / 重依赖,只做 node --check 静态校验)。';
console.error(msg);
throw new Error('esbuild 不可用(非集成段环境)');
}
const root = isAbsolute(projectDir) ? projectDir : resolve(process.cwd(), projectDir);
const entry = join(root, opts.entry || 'src/main.js');
const outfile = join(root, opts.outfile || 'bundle.iife.js');
const globalName = opts.globalName || '__Tier2GameBundle';
try {
const result = await esbuild.build({
...TIER2_BUILD_OPTIONS,
entryPoints: [entry],
outfile,
globalName,
metafile: true, // 落 meta供核验 import 图 + 哪些 chunk 引用 phaser external
});
// 落 metafile诊断import 图 / external 解析)。
if (result && result.metafile) {
const { writeFileSync } = await import('node:fs');
writeFileSync(outfile + '.meta.json', JSON.stringify(result.metafile));
}
const log = `[build-phaser] OK ${entry}${outfile} (iife/es2019/external:phaser/globalName=${globalName})`;
console.log(log);
return { ok: true, bundlePath: outfile, log };
} catch (e) {
// 构建失败 → 完整报错 logA7 build 工具据它喂回循环让 agent 修源)。
const log = '[build-phaser] 失败:\n' + (e && e.message ? e.message : String(e));
console.error(log);
return { ok: false, bundlePath: outfile, log };
}
}
// ── CLI 入口(仅集成段使用)─────────────────────────────────────────────────
if (import.meta.url === `file://${argv[1]}`) {
const [projectDir, ...rest] = argv.slice(2);
if (!projectDir) {
console.error('用法node build-phaser.mjs <工程根目录> [--global-name=NAME] [--out=bundle.iife.js]');
exit(1);
}
const gn = (rest.find((a) => a.startsWith('--global-name=')) || '').split('=')[1];
const out = (rest.find((a) => a.startsWith('--out=')) || '').split('=')[1];
// --entry 透传(默认 src/main.js;集成层 run.py 仅在非默认入口时传)。
const entry = (rest.find((a) => a.startsWith('--entry=')) || '').split('=')[1];
buildPhaserProject(projectDir, { globalName: gn, outfile: out, entry })
.then((r) => exit(r.ok ? 0 : 1))
.catch(() => exit(1));
}