收紧移动壳观测渠道初始化边界

移动壳配置检查拒绝观测、渠道、更新 SDK 相关依赖。

移动壳配置检查拒绝生产入口和 HostBridge 提前初始化观测、渠道、更新 SDK。

更新 Expo React Native 与 Tauri 宿主壳方案中的 Phase 4 能力边界。

更新共享决策日志记录移动壳观测与渠道 SDK 初始化前置条件。
This commit is contained in:
2026-06-18 11:39:05 +08:00
parent 242f872ac0
commit 25b140d63e
3 changed files with 75 additions and 18 deletions
+72 -16
View File
@@ -40,6 +40,37 @@ const devScaffoldTerms = [
'模' + '拟',
'伪' + '造',
];
const blockedMobileChannelDependencies = [
'@react-native-firebase/analytics',
'@react-native-firebase/app',
'@segment/analytics-react-native',
'@sentry/react-native',
'amplitude-react-native',
'expo-application',
'expo-updates',
'posthog-react-native',
'react-native-code-push',
];
const blockedMobileChannelSnippets = [
'@react-native-firebase/analytics',
'@react-native-firebase/app',
'@segment/analytics-react-native',
'@sentry/react-native',
'Amplitude.getInstance',
'Analytics.screen',
'Analytics.track',
'CodePush.sync',
'PostHogProvider',
'Sentry.init',
'Updates.checkForUpdateAsync',
'Updates.fetchUpdateAsync',
'Updates.reloadAsync',
'analytics().logEvent',
'amplitude.init',
'codePush(',
'posthog.capture',
'posthog.init',
];
const blockedAndroidPermissions = [
'android.permission.RECORD_AUDIO',
'android.permission.RECEIVE_BOOT_COMPLETED',
@@ -79,6 +110,45 @@ function extractStringConstExport(source, exportName) {
return match[1];
}
function assertNoBlockedMobileChannelDependencies() {
const dependencySections = [
'dependencies',
'devDependencies',
'optionalDependencies',
'peerDependencies',
];
for (const dependency of blockedMobileChannelDependencies) {
for (const section of dependencySections) {
if (packageConfig[section]?.[dependency]) {
throw new Error(
`mobile shell must not depend on ${dependency} before the real channel contract exists`,
);
}
}
}
}
function assertNoBlockedMobileChannelSnippets() {
const sources = [
['app.json', JSON.stringify(appConfig)],
['App.tsx', appSource],
['mobileHostBridge.ts', bridgeSource],
['mobileShellUrl.ts', mobileShellUrlSource],
['mobileShellRuntime.ts', mobileShellRuntimeSource],
];
for (const [sourceName, source] of sources) {
for (const snippet of blockedMobileChannelSnippets) {
if (source.includes(snippet)) {
throw new Error(
`mobile shell ${sourceName} must not initialize ${snippet} before the real channel contract exists`,
);
}
}
}
}
function collectProductionSourceFiles(entry) {
const stats = fs.statSync(entry);
if (stats.isDirectory()) {
@@ -148,6 +218,8 @@ function countAlphaPixels(png) {
assertNoDevScaffoldTerms(
productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)),
);
assertNoBlockedMobileChannelDependencies();
assertNoBlockedMobileChannelSnippets();
const sharedCapabilities = extractStringArrayExport(
sharedContractSource,
@@ -405,22 +477,6 @@ for (const dependency of [
}
}
for (const dependency of [
'expo-application',
'expo-updates',
'@sentry/react-native',
'@react-native-firebase/app',
'@react-native-firebase/analytics',
'react-native-code-push',
'posthog-react-native',
'amplitude-react-native',
'@segment/analytics-react-native',
]) {
if (packageConfig.dependencies?.[dependency] || packageConfig.devDependencies?.[dependency]) {
throw new Error(`mobile shell must not depend on ${dependency} before the real channel contract exists`);
}
}
const imagePickerPlugin = appConfig.plugins?.find((plugin) =>
Array.isArray(plugin) ? plugin[0] === 'expo-image-picker' : plugin === 'expo-image-picker',
);