From 0e4cf6356fbc4603159f0188587875d778468e05 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 1 Oct 2026 16:50:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7Phaser4=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E5=B9=B6=E6=95=B4=E7=90=86=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阻断无npm构建工作区的单文件项目发布,明确客户端首版支持范围 拆分作品资料媒体默认逻辑与版本历史模型,满足前端刷新组件规则 补充对应Rust发布门禁与导出测试 --- .../src-tauri/src/project/export.rs | 22 +++++++------- .../src-tauri/src/tests/project.rs | 30 +++++++++++++++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/export.rs b/apps/ai-game-creator-shell/src-tauri/src/project/export.rs index c048e0147..8165e72f9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/export.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/export.rs @@ -260,6 +260,7 @@ pub(crate) async fn export_local_project_package_for_publish_at( root: &Path, ) -> Result { 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) } /// 发布前的客户端预检:包体不得超过平台上限。 diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 9523a9625..0981b7256 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -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