From 673c6dbbb7a494a25cd356a2401fabf52ba867f9 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 14:23:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=8E=9F=E7=94=9F=E5=A3=B3?= =?UTF-8?q?=E9=94=81=E6=96=87=E4=BB=B6=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳配置检查锁定package-lock实际解析版本 桌面壳配置检查锁定package-lock与Cargo.lock解析版本 宿主壳方案文档补充锁文件版本门禁 共享决策日志记录原生壳锁文件依赖边界 --- apps/desktop-shell/scripts/check-config.mjs | 117 ++++++++++++++++++ apps/mobile-shell/scripts/check-config.mjs | 39 ++++++ .../shared-memory/decision-log.md | 2 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 6 +- 4 files changed, 160 insertions(+), 4 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 1f4536c73..dfa7219ad 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -6,6 +6,8 @@ const packagePath = new URL('../package.json', import.meta.url); const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const rootPackagePath = new URL('../../../package.json', import.meta.url); const rootPackageConfig = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); +const rootPackageLockPath = new URL('../../../package-lock.json', import.meta.url); +const rootPackageLock = JSON.parse(fs.readFileSync(rootPackageLockPath, 'utf8')); const capabilityPath = new URL( '../src-tauri/capabilities/main.json', import.meta.url, @@ -15,6 +17,8 @@ const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url); const buildScript = fs.readFileSync(buildScriptPath, 'utf8'); const cargoManifestPath = new URL('../src-tauri/Cargo.toml', import.meta.url); const cargoManifest = fs.readFileSync(cargoManifestPath, 'utf8'); +const cargoLockPath = new URL('../src-tauri/Cargo.lock', import.meta.url); +const cargoLock = fs.readFileSync(cargoLockPath, 'utf8'); const iconDirPath = new URL('../src-tauri/icons/', import.meta.url); const generatedPermissionDir = new URL( '../src-tauri/permissions/autogenerated/', @@ -140,6 +144,22 @@ function extractCargoDependencyLine(source, sectionName, dependencyName) { throw new Error(`Cargo ${sectionName}.${dependencyName} is missing`); } +function extractCargoLockPackages(source) { + return source + .split('\n[[package]]\n') + .map((block) => block.trim()) + .filter((block) => block.includes('name = ')) + .map((block) => { + const name = block.match(/^name = "([^"]+)"/m)?.[1]; + const version = block.match(/^version = "([^"]+)"/m)?.[1]; + const dependenciesMatch = block.match(/^dependencies = \[\n([\s\S]*?)\n\]/m); + const dependencies = dependenciesMatch + ? [...dependenciesMatch[1].matchAll(/ "([^"]+)"/g)].map((entry) => entry[1]) + : []; + return { block, dependencies, name, version }; + }); +} + function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -242,6 +262,15 @@ function assertPackageDependencyVersion( } } +function assertPackageLockVersion(dependency, expected) { + const actual = rootPackageLock.packages?.[`node_modules/${dependency}`]?.version; + if (actual !== expected) { + throw new Error( + `root package-lock ${dependency} resolved version drifted: expected ${expected} but got ${actual}`, + ); + } +} + function assertCargoDependencyLine(sectionName, dependencyName, expectedLine) { const actualLine = extractCargoDependencyLine( cargoManifest, @@ -255,6 +284,52 @@ function assertCargoDependencyLine(sectionName, dependencyName, expectedLine) { } } +const cargoLockPackages = extractCargoLockPackages(cargoLock); + +function findCargoLockPackage(packageName, expectedVersion) { + return cargoLockPackages.find( + (entry) => entry.name === packageName && entry.version === expectedVersion, + ); +} + +function assertCargoLockPackageVersion(packageName, expectedVersion) { + if (!findCargoLockPackage(packageName, expectedVersion)) { + const actualVersions = cargoLockPackages + .filter((entry) => entry.name === packageName) + .map((entry) => entry.version) + .join(', '); + throw new Error( + `Cargo.lock ${packageName} resolved version drifted: expected ${expectedVersion} but got ${actualVersions || 'missing'}`, + ); + } +} + +function assertCargoLockDirectDependency( + parentPackageName, + parentVersion, + dependencyName, + expectedVersion, +) { + const parentPackage = findCargoLockPackage(parentPackageName, parentVersion); + if (!parentPackage) { + throw new Error( + `Cargo.lock ${parentPackageName} ${parentVersion} is missing`, + ); + } + + const dependencyToken = parentPackage.dependencies.find((dependency) => { + const [name, version] = dependency.split(' '); + return name === dependencyName && (!version || version === expectedVersion); + }); + if (!dependencyToken) { + throw new Error( + `Cargo.lock ${parentPackageName} ${parentVersion} dependency ${dependencyName} drifted: expected ${expectedVersion}`, + ); + } + + assertCargoLockPackageVersion(dependencyName, expectedVersion); +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -349,6 +424,13 @@ for (const [dependency, expected] of Object.entries({ ); } +for (const [dependency, expected] of Object.entries({ + '@tauri-apps/cli': '2.11.2', + typescript: '5.8.3', +})) { + assertPackageLockVersion(dependency, expected); +} + for (const [sectionName, dependencyName, expectedLine] of [ ['build-dependencies', 'tauri-build', 'tauri-build = { version = "2.6.2", features = [] }'], ['dependencies', 'base64', 'base64 = "0.22"'], @@ -384,6 +466,41 @@ for (const [sectionName, dependencyName, expectedLine] of [ assertCargoDependencyLine(sectionName, dependencyName, expectedLine); } +for (const [packageName, expectedVersion] of [ + ['tauri-build', '2.6.2'], + ['base64', '0.22.1'], + ['serde', '1.0.228'], + ['serde_json', '1.0.150'], + ['tauri', '2.11.2'], + ['tauri-plugin-clipboard-manager', '2.3.2'], + ['tauri-plugin-dialog', '2.7.1'], + ['tauri-plugin-notification', '2.3.3'], + ['tauri-plugin-opener', '2.5.4'], + ['tauri-plugin-single-instance', '2.4.2'], +]) { + assertCargoLockPackageVersion(packageName, expectedVersion); +} + +for (const [dependencyName, expectedVersion] of [ + ['tauri-build', '2.6.2'], + ['base64', '0.22.1'], + ['serde', '1.0.228'], + ['serde_json', '1.0.150'], + ['tauri', '2.11.2'], + ['tauri-plugin-clipboard-manager', '2.3.2'], + ['tauri-plugin-dialog', '2.7.1'], + ['tauri-plugin-notification', '2.3.3'], + ['tauri-plugin-opener', '2.5.4'], + ['tauri-plugin-single-instance', '2.4.2'], +]) { + assertCargoLockDirectDependency( + 'genarrative-desktop-shell', + config.version, + dependencyName, + expectedVersion, + ); +} + function readPngSize(file) { const buffer = fs.readFileSync(file); if ( diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index f52e22eec..0ec0c08bc 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -21,6 +21,8 @@ const packagePath = new URL('../package.json', import.meta.url); const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const rootPackagePath = new URL('../../../package.json', import.meta.url); const rootPackageConfig = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); +const rootPackageLockPath = new URL('../../../package-lock.json', import.meta.url); +const rootPackageLock = JSON.parse(fs.readFileSync(rootPackageLockPath, 'utf8')); const iconPath = new URL('../assets/icon.png', import.meta.url); const icon = PNG.sync.read(fs.readFileSync(iconPath)); const brandBackgroundColor = '#fffdf9'; @@ -212,6 +214,15 @@ function assertPackageDependencyVersion( } } +function assertPackageLockVersion(dependency, expected) { + const actual = rootPackageLock.packages?.[`node_modules/${dependency}`]?.version; + if (actual !== expected) { + throw new Error( + `root package-lock ${dependency} resolved version drifted: expected ${expected} but got ${actual}`, + ); + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -352,6 +363,27 @@ for (const [dependency, expected] of Object.entries({ ); } +for (const [dependency, expected] of Object.entries({ + '@expo/metro-runtime': '56.0.15', + expo: '56.0.12', + 'expo-clipboard': '56.0.4', + 'expo-document-picker': '56.0.4', + 'expo-file-system': '56.0.8', + 'expo-haptics': '56.0.3', + 'expo-image-picker': '56.0.18', + 'expo-linking': '56.0.14', + 'expo-network': '56.0.5', + 'expo-notifications': '56.0.18', + 'expo-sharing': '56.0.18', + 'expo-status-bar': '56.0.4', + react: '19.2.4', + 'react-native': '0.86.0', + 'react-native-safe-area-context': '5.8.0', + 'react-native-webview': '13.16.1', +})) { + assertPackageLockVersion(dependency, expected); +} + for (const [dependency, expected] of Object.entries({ typescript: '~5.8.2', vitest: '^0.34.6', @@ -372,6 +404,13 @@ for (const [dependency, expected] of Object.entries({ ); } +for (const [dependency, expected] of Object.entries({ + typescript: '5.8.3', + vitest: '0.34.6', +})) { + assertPackageLockVersion(dependency, 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 18bf28af1..150a1c45a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2455,7 +2455,7 @@ ## 2026-06-18 原生壳关键依赖版本收口 - 背景:Expo / React Native WebView / Tauri / Cargo 插件版本会直接影响 WebView 安全默认值、managed config 解析、production bundle、Tauri capability、插件初始化和 release 构建行为;如果只改依赖声明,壳行为可能绕过 HostBridge 门禁和现有验收口径静默漂移。 -- 决策:移动壳配置检查锁定 `apps/mobile-shell/package.json` 和根 `package.json` 中当前 Expo SDK 56、React 19、React Native 0.86、`react-native-webview`、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 与 Vitest 版本。桌面壳配置检查锁定 `apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI / TypeScript 版本,并锁定 `src-tauri/Cargo.toml` 中 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 和 clipboard、dialog、notification、opener、single-instance 插件版本及 Tauri `tray-icon` feature。后续升级这些依赖必须同步更新配置门禁、方案文档和验证结果。 +- 决策:移动壳配置检查锁定 `apps/mobile-shell/package.json` 和根 `package.json` 中当前 Expo SDK 56、React 19、React Native 0.86、`react-native-webview`、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 与 Vitest 版本,并检查根 `package-lock.json` 的实际解析版本。桌面壳配置检查锁定 `apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI / TypeScript 版本,检查根 `package-lock.json` 的实际解析版本,并锁定 `src-tauri/Cargo.toml` 中 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 和 clipboard、dialog、notification、opener、single-instance 插件版本及 Tauri `tray-icon` feature,同时检查 `src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本。后续升级这些依赖必须同步更新配置门禁、方案文档、lockfile 和验证结果。 - 影响范围:`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`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index cd5c440f0..154de0124 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -235,7 +235,7 @@ GameBridge 禁止: - 每个请求必须有超时,重复 `id` 不得重复执行支付、登录、系统分享、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果。 - 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景。不能只凭 `native_app` 宿主类型假设能力可用。 - 壳能力声明与两端壳测试必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须存在于共享 HostBridge 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口和两端壳生产源码临时替身词扫描不得漂移。 -- Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。 +- Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、`package-lock.json` / `Cargo.lock` 解析版本、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。 - 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin` 和 `payment.request` 的伪声明。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 - 桌面壳不得提前安装或初始化崩溃上报、analytics、遥测日志、自动更新或渠道分发 SDK;这类能力必须先补齐真实后端 / 第三方端点、采集口径、用户授权、隐私披露、签名和发布流程,再进入 Tauri 配置、Cargo 依赖、Node 依赖或 Rust 初始化代码。 @@ -308,7 +308,7 @@ GameBridge 禁止: 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 的快捷命令。 -2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json` 的 Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明。 +2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json` 的 Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;根 `package-lock.json` 里的实际解析版本也必须由配置检查锁定。升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明或锁文件。 ### Phase 3:Tauri 桌面壳 MVP @@ -355,7 +355,7 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json` 和根 H5 `package.json` 不得安装 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` 包,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API;Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。 -2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;`src-tauri/Cargo.toml` 的 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 以及 clipboard、dialog、notification、opener、single-instance 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。 +2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;根 `package-lock.json` 里的实际解析版本也必须一致;`src-tauri/Cargo.toml` 的 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 以及 clipboard、dialog、notification、opener、single-instance 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查,`src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本也必须同步受检。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。 2026-06-18 追加:H5 到 Tauri 的 command 名进入共享契约。`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'`,`nativeAppHostBridge` 只能通过该常量调用 Tauri 注入的 `core.invoke`;桌面壳配置检查会对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,禁止 H5 侧写死或调用其它 Tauri command。