收口原生壳锁文件版本
移动壳配置检查锁定package-lock实际解析版本 桌面壳配置检查锁定package-lock与Cargo.lock解析版本 宿主壳方案文档补充锁文件版本门禁 共享决策日志记录原生壳锁文件依赖边界
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user