补强移动壳能力流程门禁

根级原生壳检查反查 Expo 关键能力真实流程

宿主壳方案文档记录移动能力到实现与测试合同

项目记忆同步原生壳能力真实链路约束
This commit is contained in:
2026-06-20 18:40:03 +08:00
parent 52ceb32770
commit 981755413d
3 changed files with 230 additions and 3 deletions
+227
View File
@@ -239,6 +239,170 @@ const wechatCapabilityFlowContracts = [
],
},
];
const mobileCapabilityFlowContracts = [
{
capability: 'notification.showLocal',
files: [
'apps/mobile-shell/app.json',
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'apps/mobile-shell/src/host-bridge/notifications.ts',
'apps/mobile-shell/scripts/check-config.mjs',
'apps/mobile-shell/scripts/check-expo-config.mjs',
nativeShellPlanPath,
],
snippets: [
['apps/mobile-shell/app.json', 'android.permission.POST_NOTIFICATIONS'],
[
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'showMobileHostBridgeLocalNotification(request)',
],
[
'apps/mobile-shell/src/host-bridge/notifications.ts',
'Notifications.scheduleNotificationAsync',
],
[
'apps/mobile-shell/src/host-bridge/notifications.ts',
'Notifications.setNotificationChannelAsync',
],
[
'apps/mobile-shell/src/host-bridge/notifications.ts',
'HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID',
],
[
'apps/mobile-shell/scripts/check-config.mjs',
'HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID',
],
[
'apps/mobile-shell/scripts/check-expo-config.mjs',
'android.permission.POST_NOTIFICATIONS',
],
[nativeShellPlanPath, 'Expo 移动壳的关键能力'],
],
tests: [
'apps/mobile-shell/src/host-bridge/notifications.test.ts',
'apps/mobile-shell/src/host-bridge/bridge.test.ts',
],
},
{
capability: 'file.captureImage',
files: [
'apps/mobile-shell/app.json',
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'apps/mobile-shell/src/host-bridge/files.ts',
'apps/mobile-shell/scripts/check-config.mjs',
nativeShellPlanPath,
],
snippets: [
['apps/mobile-shell/app.json', 'expo-image-picker'],
[
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'captureMobileHostBridgeImageFile(request)',
],
[
'apps/mobile-shell/src/host-bridge/files.ts',
'ImagePicker.requestCameraPermissionsAsync',
],
[
'apps/mobile-shell/src/host-bridge/files.ts',
'ImagePicker.launchCameraAsync',
],
['apps/mobile-shell/scripts/check-config.mjs', 'file.captureImage'],
[nativeShellPlanPath, 'Expo 移动壳的关键能力'],
],
tests: [
'apps/mobile-shell/src/host-bridge/files.test.ts',
'apps/mobile-shell/src/host-bridge/bridge.test.ts',
],
},
{
capability: 'scanner.scanQrCode',
files: [
'apps/mobile-shell/app.json',
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'apps/mobile-shell/src/host-bridge/scanner.ts',
'apps/mobile-shell/src/shell/QrScannerOverlay.tsx',
'apps/mobile-shell/scripts/check-config.mjs',
nativeShellPlanPath,
],
snippets: [
['apps/mobile-shell/app.json', 'expo-camera'],
[
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'scanMobileHostBridgeQrCode(request)',
],
[
'apps/mobile-shell/src/host-bridge/scanner.ts',
'HOST_BRIDGE_SCANNER_TIMEOUT_MS',
],
[
'apps/mobile-shell/src/shell/QrScannerOverlay.tsx',
'Camera.requestCameraPermissionsAsync',
],
['apps/mobile-shell/src/shell/QrScannerOverlay.tsx', 'CameraView'],
['apps/mobile-shell/scripts/check-config.mjs', 'scanner.scanQrCode'],
[nativeShellPlanPath, 'Expo 移动壳的关键能力'],
],
tests: [
'apps/mobile-shell/src/host-bridge/scanner.test.ts',
'apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx',
'apps/mobile-shell/src/shell/ShellApp.test.tsx',
'apps/mobile-shell/src/host-bridge/bridge.test.ts',
],
},
{
capability: 'network.statusChanged',
files: [
'apps/mobile-shell/src/shell/network.ts',
'apps/mobile-shell/src/shell/ShellApp.tsx',
'apps/mobile-shell/scripts/check-config.mjs',
nativeShellPlanPath,
],
snippets: [
[
'apps/mobile-shell/src/shell/network.ts',
'Network.addNetworkStateListener',
],
[
'apps/mobile-shell/src/shell/ShellApp.tsx',
"injectHostBridgeEvent('network.statusChanged', payload)",
],
[
'apps/mobile-shell/src/shell/ShellApp.tsx',
"logMobileHostEventFailure('network.statusChanged', error)",
],
['apps/mobile-shell/scripts/check-config.mjs', 'network.statusChanged'],
[nativeShellPlanPath, 'Expo 移动壳的关键能力'],
],
tests: [
'apps/mobile-shell/src/shell/network.test.ts',
'apps/mobile-shell/src/shell/ShellApp.test.tsx',
'apps/mobile-shell/src/host-bridge/bridge.test.ts',
],
},
{
capability: 'share.open',
files: [
'apps/mobile-shell/src/host-bridge/dispatch.ts',
'apps/mobile-shell/src/host-bridge/share.ts',
'apps/mobile-shell/scripts/check-config.mjs',
nativeShellPlanPath,
],
snippets: [
['apps/mobile-shell/src/host-bridge/dispatch.ts', 'openShare(request)'],
['apps/mobile-shell/src/host-bridge/share.ts', 'Share.share'],
[
'apps/mobile-shell/src/host-bridge/share.ts',
'normalizeHostBridgeShareOpenPayload',
],
['apps/mobile-shell/scripts/check-config.mjs', 'share.open'],
[nativeShellPlanPath, 'Expo 移动壳的关键能力'],
],
tests: [
'apps/mobile-shell/src/host-bridge/share.test.ts',
'apps/mobile-shell/src/host-bridge/bridge.test.ts',
],
},
];
const h5NativeAppRouteFlowContracts = [
{
route: '/child-motion-demo',
@@ -2043,6 +2207,66 @@ function assertWechatMiniProgramCapabilityFlows() {
}
}
function assertExpoMobileCapabilityFlows() {
const sharedContractSource = fs.readFileSync(sharedHostBridgeContractPath, 'utf8');
const declaredCapabilities = extractTsStringArray(
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
);
const declaredCapabilitySet = new Set(declaredCapabilities);
const contractedCapabilities = mobileCapabilityFlowContracts.map(
({ capability }) => capability,
);
assertUniqueList(
contractedCapabilities,
'Expo mobile capability flow contracts',
);
const mobileShellTestSet = new Set(
expectedMobileHostBridgeFiles
.filter((fileName) => fileName.includes('.test.'))
.map((fileName) => `apps/mobile-shell/src/host-bridge/${fileName}`)
.concat(
expectedMobileShellFiles
.filter((fileName) => fileName.includes('.test.'))
.map((fileName) => `apps/mobile-shell/src/shell/${fileName}`),
),
);
for (const contract of mobileCapabilityFlowContracts) {
if (!declaredCapabilitySet.has(contract.capability)) {
throw new Error(
`Expo mobile capability flow contract declared for missing capability: ${contract.capability}`,
);
}
for (const filePath of contract.files) {
if (!fs.existsSync(filePath)) {
throw new Error(
`Expo mobile ${contract.capability} flow file is missing: ${filePath}`,
);
}
}
for (const [filePath, snippet] of contract.snippets) {
assertFileIncludesSnippet(
filePath,
snippet,
`Expo mobile ${contract.capability} flow`,
);
}
for (const testPath of contract.tests) {
if (!mobileShellTestSet.has(testPath)) {
throw new Error(
`Expo mobile ${contract.capability} flow test is not part of mobile-shell:test: ${testPath}`,
);
}
}
}
}
function assertH5NativeAppRouteFlows() {
const h5HostBridgeTestSet = new Set(h5HostBridgeTests);
for (const contract of h5NativeAppRouteFlowContracts) {
@@ -2537,6 +2761,9 @@ assertWechatMiniProgramRouteParity();
console.log('[check:native-shells] wechat-mini-program-capability-flows');
assertWechatMiniProgramCapabilityFlows();
console.log('[check:native-shells] expo-mobile-capability-flows');
assertExpoMobileCapabilityFlows();
console.log('[check:native-shells] h5-native-app-route-flows');
assertH5NativeAppRouteFlows();