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>
133 lines
8.0 KiB
JavaScript
133 lines
8.0 KiB
JavaScript
/**
|
||
* tier2/engine/build-phaser.mjs —— tier2 Phaser 多文件源工程 esbuild 构建脚本(A3 buildProfile 实现 · build 工具底座)
|
||
* owner:tier2 引擎线(M1-Phaser-scaffold+host)| 消费方:A7 `build` 工具(单写 agent ReAct 核心环)/ harness(U3 构建段)
|
||
*
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
* 【这份是什么 · 对照左支单包 iife】
|
||
* 左支(Tier0/1)build = 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,缺省 __Tier2GameBundle),host 取它建 PhaserGameInstance。
|
||
*
|
||
* 【与 A3 源项目契约 buildProfile 对齐】(tier2-source-project.schema.json)
|
||
* buildProfile = { bundler: 'esbuild', format: 'iife', globalName, target, minify }。本脚本默认参数与之对齐。
|
||
* phaser 经 external 还是打进 bundle:tier2 spike 期 phaser 由宿主页面 <script> 预载并经 opts.engine 注入,
|
||
* 故构建时 external 'phaser'(不打进 bundle,减体积 + 与宿主注入边界一致);游戏内 `import Phaser from 'phaser'`
|
||
* 在 iife 下解析为全局 window.Phaser(esbuild 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) {
|
||
// 构建失败 → 完整报错 log(A7 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));
|
||
}
|