收紧Phaser4发布边界并整理前端模型
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

阻断无npm构建工作区的单文件项目发布,明确客户端首版支持范围

拆分作品资料媒体默认逻辑与版本历史模型,满足前端刷新组件规则

补充对应Rust发布门禁与导出测试
This commit is contained in:
2026-10-01 16:50:53 +08:00
parent a2b70d92b8
commit 0e4cf6356f
2 changed files with 34 additions and 18 deletions
@@ -260,6 +260,7 @@ pub(crate) async fn export_local_project_package_for_publish_at(
root: &Path,
) -> Result<LocalProjectExportPackageResult, String> {
if validate_project_game_entry(root).is_ok() {
ensure_publish_project_stack(root)?;
return export_local_project_package_at(root);
}
let manifest = read_manifest_for_project(root)?;
@@ -403,11 +404,11 @@ fn workspace_has_vite_config(package_root: &Path) -> bool {
/// 发布到游戏广场首版只接受 Phaser 4 + Vite 的 npm 工程。
///
/// 只校验「确实存在 npm 构建工作区」的项目(`resolve_publish_build_cwd` 命中):老的
/// 单文件 HTML 项目没有 `package.json`,不在首版 npm 打包合同内,保持原有发布能力。
/// 客户端发布必须命中 npm 构建工作区;无 `package.json` 的单文件 HTML 项目不属于首版 Phaser 4 发布合同。
/// 判定顺序以**将被构建的那个工作区**为准,它没声明 phaser 时再看项目里的另一个工作区
/// (根工程 / `game` 子工程),避免多工作区项目被误判;校验失败必须给出可操作错误——
/// 缺什么依赖、当前声明是什么版本、要怎么做。错误文案只带项目内相对路径。
fn ensure_publish_project_stack_at(
root: &Path,
build_cwd_relative: &'static str,
@@ -419,13 +420,11 @@ fn ensure_publish_project_stack_at(
};
let mut phaser: Option<(String, String, PathBuf)> = None;
let mut vite_ready = false;
let mut workspace_found = false;
for cwd_relative in candidates {
let Some((package_root, package)) = read_publish_workspace_package(root, cwd_relative)?
else {
continue;
};
workspace_found = true;
if phaser.is_none() {
if let Some(spec) = declared_dependency_spec(&package, "phaser") {
phaser = Some((spec, cwd_relative.to_string(), package_root.clone()));
@@ -437,9 +436,6 @@ fn ensure_publish_project_stack_at(
vite_ready = true;
}
}
if !workspace_found {
return Ok(());
}
let Some((spec, cwd_relative, package_root)) = phaser else {
return Err(format!(
"发布到游戏广场目前只支持 Phaser 4(npm + Vite)工程:{build_cwd_relative}/package.json 未声明 phaser 依赖。请先安装 Phaser 4(例如 npm install phaser@4.2.1)后重新发布。"
@@ -463,12 +459,14 @@ fn ensure_publish_project_stack_at(
Ok(())
}
/// 发布前校验工程栈:存在 npm 构建工作区时要求 Phaser 4 + Vite,否则不作判断。
/// 发布前校验工程栈:客户端发布必须使用 Phaser 4 + Vite npm 工程。
pub(crate) fn ensure_publish_project_stack(root: &Path) -> Result<(), String> {
match resolve_publish_build_cwd(root)? {
Some(cwd_relative) => ensure_publish_project_stack_at(root, cwd_relative),
None => Ok(()),
}
let Some(cwd_relative) = resolve_publish_build_cwd(root)? else {
return Err(
"发布到游戏广场目前只支持 Phaser 4(npm + Vite)工程:项目没有可识别的 npm build 工作区。请先使用 Phaser 4 + Vite 工程并提供 npm run build 后重新发布。".to_string(),
);
};
ensure_publish_project_stack_at(root, cwd_relative)
}
/// 发布前的客户端预检:包体不得超过平台上限。
@@ -4149,14 +4149,15 @@ fn publish_stack_validation_reads_semver_ranges_and_lockfile_versions() {
}
#[test]
fn publish_stack_validation_skips_projects_without_npm_build_workspace() {
fn publish_stack_validation_rejects_projects_without_npm_build_workspace() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-stack-legacy", "无 npm 构建项目")
.expect("project init");
// 去掉 npm 工作区(没有 package.json 也就没有 build 脚本),退化成老单文件项目。
// 去掉 npm 工作区(没有 package.json 也就没有 build 脚本),单文件项目不属于客户端首版合同。
fs::remove_file(root.join("game/package.json")).expect("remove game package json");
ensure_publish_project_stack(&root).expect("没有 npm 构建工作区时不做 Phaser 4 判断");
let error = ensure_publish_project_stack(&root).expect_err("单文件项目必须失败关闭");
assert!(error.contains("只支持 Phaser 4"), "{error}");
fs::remove_dir_all(root).ok();
}
@@ -4213,12 +4214,29 @@ writeFileSync('dist/assets/app.js', 'document.documentElement.dataset.autoBuild
}
#[tokio::test]
async fn publish_export_skips_build_when_playable_entry_exists() {
async fn publish_export_skips_build_when_phaser_vite_entry_exists() {
let root = unique_project_path();
init_existing_html_project_at(&root, "project-publish-skip", "已构建发布项目")
.expect("project init");
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
.expect("write playable html");
fs::create_dir_all(root.join("game/dist")).expect("create dist directory");
fs::write(
root.join("game/package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "publish-skip-fixture",
"private": true,
"scripts": { "build": "vite build" },
"dependencies": { "phaser": "4.2.1" },
"devDependencies": { "vite": "^6.2.0" }
}))
.expect("serialize package json"),
)
.expect("write package json");
fs::write(root.join("game/vite.config.js"), "export default {};\n").expect("write vite config");
fs::write(
root.join("game/dist/index.html"),
fake_llm_game_draft().game_html,
)
.expect("write playable html");
let result = export_local_project_package_for_publish_at(&root)
.await