diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index be2579fdd..8a15fdbf0 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -193,6 +193,15 @@ function assertNoBlockedDesktopSdkSnippets() { } } +function assertPackageScript(packageJson, packageLabel, scriptName, expected) { + const actual = packageJson.scripts?.[scriptName]; + if (actual !== expected) { + throw new Error( + `${packageLabel} script ${scriptName} drifted: expected ${expected} but got ${actual}`, + ); + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -250,6 +259,23 @@ assertNoTauriGuestNpmDependencies(rootPackageConfig, 'root H5 package'); assertNoBlockedCargoDependencies(); assertNoBlockedDesktopSdkSnippets(); +for (const [scriptName, expected] of Object.entries({ + dev: 'tauri dev', + build: 'tauri build', + typecheck: 'node scripts/check-config.mjs', +})) { + assertPackageScript(packageConfig, 'desktop shell package', scriptName, expected); +} + +for (const [scriptName, expected] of Object.entries({ + 'desktop-shell:dev': 'npm --prefix apps/desktop-shell run dev', + 'desktop-shell:build': 'npm --prefix apps/desktop-shell run build --', + 'desktop-shell:typecheck': 'npm --prefix apps/desktop-shell run typecheck', + 'desktop-shell:test': 'cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml', +})) { + assertPackageScript(rootPackageConfig, 'root package', scriptName, expected); +} + function readPngSize(file) { const buffer = fs.readFileSync(file); if ( @@ -566,6 +592,10 @@ if (config.build?.beforeBuildCommand !== 'npm --prefix ../.. run build:raw && np throw new Error('desktop shell build command must run from apps/desktop-shell'); } +if (config.build?.beforeDevCommand !== 'npm --prefix ../.. run dev:web') { + throw new Error('desktop shell dev command must run the root H5 Vite dev server'); +} + const [mainWindow] = config.app?.windows ?? []; if (!mainWindow || mainWindow.create !== false) { throw new Error('desktop shell must create the main window from Rust setup'); diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 5704ae3b1..d292bb1c6 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -188,6 +188,15 @@ function assertNoBlockedMobileChannelSnippets() { } } +function assertPackageScript(packageJson, packageLabel, scriptName, expected) { + const actual = packageJson.scripts?.[scriptName]; + if (actual !== expected) { + throw new Error( + `${packageLabel} script ${scriptName} drifted: expected ${expected} but got ${actual}`, + ); + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -272,6 +281,28 @@ assertNoBlockedMobileChannelDependencies(packageConfig, 'mobile shell package'); assertNoBlockedMobileChannelDependencies(rootPackageConfig, 'root H5 package'); assertNoBlockedMobileChannelSnippets(); +for (const [scriptName, expected] of Object.entries({ + dev: 'expo start', + android: 'expo run:android', + ios: 'expo run:ios', + test: 'vitest run -c vitest.config.ts', + 'config:smoke': 'node scripts/check-expo-config.mjs', + 'export:smoke': 'node scripts/check-expo-export.mjs', + typecheck: 'tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs', +})) { + assertPackageScript(packageConfig, 'mobile shell package', scriptName, expected); +} + +for (const [scriptName, expected] of Object.entries({ + 'mobile-shell:dev': 'npm --prefix apps/mobile-shell run dev', + 'mobile-shell:typecheck': 'npm --prefix apps/mobile-shell run typecheck', + 'mobile-shell:test': 'npm --prefix apps/mobile-shell run test', + 'mobile-shell:config': 'npm --prefix apps/mobile-shell run config:smoke', + 'mobile-shell:export': 'npm --prefix apps/mobile-shell run export:smoke', +})) { + assertPackageScript(rootPackageConfig, 'root package', scriptName, expected); +} + const sharedCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_CAPABILITIES', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index b0490937f..a3795520f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2438,6 +2438,13 @@ - 影响范围:`apps/mobile-shell/app.json`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/mobile-shell/scripts/check-expo-config.mjs`、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-18 原生壳命令入口收口 + +- 背景:Expo / Tauri 壳的源码、权限和构建配置已经进入门禁,但 package scripts 仍属于真实开发和验收入口;如果后续把根级或壳级 dev / build / typecheck / test 命令改成临时快捷命令,就可能绕过 Expo public config、Metro export、Tauri dev、Tauri release build smoke 或根 H5 构建。 +- 决策:移动壳 `apps/mobile-shell/package.json` 的 `dev`、`android`、`ios`、`test`、`config:smoke`、`export:smoke`、`typecheck` 和根 `mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程;桌面壳 `apps/desktop-shell/package.json` 的 `dev`、`build`、`typecheck`、根 `desktop-shell:*` 入口,以及 Tauri `beforeDevCommand` / `beforeBuildCommand` 必须保持指向真实 Tauri dev / build、根 H5 `dev:web` / `build:raw` 和桌面壳配置检查流程。两端配置检查负责拒绝命令入口漂移。 +- 影响范围:根 `package.json`、`apps/mobile-shell/package.json`、`apps/desktop-shell/package.json`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。 +- 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-18 原生 HostBridge 入站消息来源收口 - 背景:H5 主站会同时承载原生壳 HostBridge 和后续 AI H5 sandbox / GameBridge;如果 H5 侧只按 JSON envelope 识别 HostBridge response / event,sandbox iframe 可以构造同形 `postMessage` 干扰待处理宿主请求或伪造宿主事件。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index bd2066b12..6b7dd3908 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -305,6 +305,8 @@ GameBridge 禁止: 2026-06-18 追加:移动壳可分发身份、外观和默认权限进入配置门禁。Expo `name` 固定为 `Genarrative`,`slug` 固定为 `genarrative-mobile-shell`,`userInterfaceStyle` 固定为 `automatic`,`assetBundlePatterns` 固定为 `["**/*"]`,`extra.genarrativeHostBridgeVersion` 固定为 `1`;Android `permissions` 默认不得显式请求任何权限,所有当前不需要的高风险权限只能通过 `blockedPermissions` 阻断,后续新增权限必须先有真实宿主能力、系统权限说明和 H5 fallback 方案,再补配置与检查。`apps/mobile-shell/scripts/check-config.mjs` 会检查源 `app.json`,`apps/mobile-shell/scripts/check-expo-config.mjs` 会检查 Expo CLI 最终解析出的 public config,避免 config plugin 或 Expo 解析阶段引入身份、资源或权限漂移。 +2026-06-18 追加:移动壳命令入口进入配置门禁。`apps/mobile-shell/package.json` 的 `dev`、`android`、`ios`、`test`、`config:smoke`、`export:smoke`、`typecheck` 以及根 `package.json` 的 `mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程,不能替换成只跑静态脚本或绕过生产 bundler 的快捷命令。 + ### Phase 3:Tauri 桌面壳 MVP - 新增 `apps/desktop-shell/`。 @@ -334,6 +336,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳安装包身份固定为 `world.genarrative.desktop`,产品名为 `Genarrative`,Tauri、Node package 与 Cargo package 版本统一为 `0.1.0`。Release 主窗口只能从打包进二进制的 `index.html` 进入根 `dist` H5 资产,dev URL 只能指向本机 Vite 调试入口;CSP 必须保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:` 这类扩大桌面攻击面的来源。当前不配置自动更新器,直到存在真实更新端点、签名密钥和发布流程再接入;`apps/desktop-shell/scripts/check-config.mjs` 会校验这些包身份、版本、CSP 和 updater 禁用约束。 +2026-06-18 追加:桌面壳命令入口进入配置门禁。`apps/desktop-shell/package.json` 的 `dev`、`build`、`typecheck`,根 `package.json` 的 `desktop-shell:*` 入口,以及 Tauri `beforeDevCommand` / `beforeBuildCommand` 都必须保持在真实 Tauri dev / build、根 H5 `dev:web`、根 H5 `build:raw` 和桌面壳配置检查路径上;本地桌面调试不能改成加载外部 H5、跳过根 Vite 或绕过 Tauri release build smoke 的命令。 + 2026-06-18 追加:桌面壳默认不接入崩溃上报、analytics、遥测日志或渠道分发 SDK。`apps/desktop-shell/scripts/check-config.mjs` 会拒绝 Sentry、Datadog、PostHog、Segment、Amplitude、Bugsnag、OpenTelemetry、Tauri log / updater 等 Node / Cargo 依赖和 Rust 初始化片段;后续只有在真实采集端点、数据字段、用户授权、隐私披露、签名和发布流程确定后,才能按单项能力补充方案与实现。 2026-06-18 追加:桌面壳 release / dev 入口 URL 的 `hostVersion` 必须与 Tauri `tauri.conf.json`、Node package 和 Cargo package 版本一致;`host.getRuntime` 回包继续使用 `env!("CARGO_PKG_VERSION")`,配置检查会拒绝入口 query 版本、Tauri 配置版本或 Rust runtime 版本来源分叉,避免桌面包升级时 H5 首屏上下文与 runtime 回读不一致。