44d0507d98
移动壳 Android 包配置阻断重启和精确定时通知权限 移动壳配置检查拒绝远程推送 token 和通知响应监听流程 宿主壳方案和共享决策记录即时本地通知边界
196 lines
5.5 KiB
JavaScript
196 lines
5.5 KiB
JavaScript
import {spawnSync} from 'node:child_process';
|
|
|
|
import fs from 'node:fs';
|
|
|
|
const appConfigPath = new URL('../app.json', import.meta.url);
|
|
const packagePath = new URL('../package.json', import.meta.url);
|
|
const shellRoot = new URL('../', import.meta.url);
|
|
|
|
const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')).expo;
|
|
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
|
|
const result = spawnSync(
|
|
npmCommand,
|
|
['exec', 'expo', 'config', '--', '--type', 'public', '--json'],
|
|
{
|
|
cwd: shellRoot,
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
|
|
if (result.error) {
|
|
throw new Error(`failed to start Expo config smoke: ${result.error.message}`);
|
|
}
|
|
|
|
if (result.signal) {
|
|
throw new Error(`Expo config smoke was terminated by signal ${result.signal}`);
|
|
}
|
|
|
|
if ((result.status ?? 0) !== 0) {
|
|
process.stdout.write(result.stdout ?? '');
|
|
process.stderr.write(result.stderr ?? '');
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
function parseExpoConfigOutput(output) {
|
|
const trimmed = output.trim();
|
|
if (trimmed.startsWith('{')) {
|
|
return JSON.parse(trimmed);
|
|
}
|
|
|
|
const jsonStart = output.indexOf('{');
|
|
if (jsonStart === -1) {
|
|
throw new Error('Expo config smoke did not print JSON output');
|
|
}
|
|
|
|
return JSON.parse(output.slice(jsonStart));
|
|
}
|
|
|
|
const expoConfig = parseExpoConfigOutput(result.stdout ?? '');
|
|
|
|
function assertEqual(actual, expected, label) {
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`Expo config ${label} drifted: expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertIncludes(list, expected, label) {
|
|
if (!Array.isArray(list) || !list.includes(expected)) {
|
|
throw new Error(`Expo config ${label} missing ${expected}`);
|
|
}
|
|
}
|
|
|
|
function findPlugin(name) {
|
|
return expoConfig.plugins?.find((plugin) =>
|
|
Array.isArray(plugin) ? plugin[0] === name : plugin === name,
|
|
);
|
|
}
|
|
|
|
assertEqual(expoConfig.name, 'Genarrative', 'name');
|
|
assertEqual(expoConfig.slug, 'genarrative-mobile-shell', 'slug');
|
|
assertEqual(expoConfig.scheme, 'genarrative', 'scheme');
|
|
assertEqual(expoConfig.version, packageConfig.version, 'version');
|
|
assertEqual(expoConfig.version, appConfig.version, 'app version');
|
|
assertEqual(expoConfig.orientation, 'default', 'orientation');
|
|
assertEqual(expoConfig.userInterfaceStyle, 'automatic', 'userInterfaceStyle');
|
|
assertEqual(expoConfig.icon, './assets/icon.png', 'icon');
|
|
assertEqual(expoConfig.splash?.image, './assets/icon.png', 'splash image');
|
|
assertEqual(expoConfig.splash?.resizeMode, 'contain', 'splash resize mode');
|
|
assertEqual(expoConfig.splash?.backgroundColor, '#fffdf9', 'splash background');
|
|
assertEqual(
|
|
expoConfig.extra?.genarrativeHostBridgeVersion,
|
|
1,
|
|
'HostBridge version',
|
|
);
|
|
|
|
assertEqual(
|
|
expoConfig.ios?.bundleIdentifier,
|
|
'world.genarrative.mobile',
|
|
'iOS bundle identifier',
|
|
);
|
|
assertEqual(expoConfig.ios?.buildNumber, '1', 'iOS build number');
|
|
assertIncludes(
|
|
expoConfig.ios?.associatedDomains,
|
|
'applinks:app.genarrative.world',
|
|
'iOS associated domains',
|
|
);
|
|
assertEqual(
|
|
expoConfig.ios?.infoPlist?.ITSAppUsesNonExemptEncryption,
|
|
false,
|
|
'iOS encryption export flag',
|
|
);
|
|
assertEqual(
|
|
expoConfig.ios?.infoPlist?.NSAppTransportSecurity?.NSAllowsArbitraryLoads,
|
|
false,
|
|
'iOS ATS arbitrary loads',
|
|
);
|
|
|
|
assertEqual(
|
|
expoConfig.android?.package,
|
|
'world.genarrative.mobile',
|
|
'Android package',
|
|
);
|
|
assertEqual(expoConfig.android?.versionCode, 1, 'Android versionCode');
|
|
assertEqual(
|
|
expoConfig.android?.usesCleartextTraffic,
|
|
false,
|
|
'Android cleartext traffic',
|
|
);
|
|
assertEqual(expoConfig.android?.allowBackup, false, 'Android backup flag');
|
|
assertEqual(
|
|
expoConfig.android?.softwareKeyboardLayoutMode,
|
|
'resize',
|
|
'Android software keyboard layout mode',
|
|
);
|
|
assertIncludes(
|
|
expoConfig.android?.blockedPermissions,
|
|
'android.permission.RECORD_AUDIO',
|
|
'Android blocked permissions',
|
|
);
|
|
for (const permission of [
|
|
'android.permission.RECEIVE_BOOT_COMPLETED',
|
|
'android.permission.SCHEDULE_EXACT_ALARM',
|
|
'android.permission.USE_EXACT_ALARM',
|
|
]) {
|
|
assertIncludes(
|
|
expoConfig.android?.blockedPermissions,
|
|
permission,
|
|
'Android background notification blocked permissions',
|
|
);
|
|
}
|
|
assertEqual(
|
|
expoConfig.android?.adaptiveIcon?.foregroundImage,
|
|
'./assets/icon.png',
|
|
'Android adaptive icon foreground',
|
|
);
|
|
assertEqual(
|
|
expoConfig.android?.adaptiveIcon?.backgroundColor,
|
|
'#fffdf9',
|
|
'Android adaptive icon background',
|
|
);
|
|
|
|
const appLinkFilter = expoConfig.android?.intentFilters?.find((filter) =>
|
|
filter?.data?.some(
|
|
(entry) =>
|
|
entry?.scheme === 'https' && entry?.host === 'app.genarrative.world',
|
|
),
|
|
);
|
|
if (!appLinkFilter) {
|
|
throw new Error('Expo config Android app link filter is missing');
|
|
}
|
|
|
|
const imagePickerPlugin = findPlugin('expo-image-picker');
|
|
if (!Array.isArray(imagePickerPlugin)) {
|
|
throw new Error('Expo config image picker plugin is missing options');
|
|
}
|
|
assertEqual(
|
|
typeof imagePickerPlugin[1]?.photosPermission,
|
|
'string',
|
|
'image picker photo permission text type',
|
|
);
|
|
assertEqual(
|
|
typeof imagePickerPlugin[1]?.cameraPermission,
|
|
'string',
|
|
'image picker camera permission text type',
|
|
);
|
|
assertEqual(
|
|
imagePickerPlugin[1]?.microphonePermission,
|
|
false,
|
|
'image picker microphone permission',
|
|
);
|
|
|
|
const notificationsPlugin = findPlugin('expo-notifications');
|
|
if (!Array.isArray(notificationsPlugin)) {
|
|
throw new Error('Expo config notifications plugin is missing options');
|
|
}
|
|
assertEqual(
|
|
notificationsPlugin[1]?.enableBackgroundRemoteNotifications,
|
|
false,
|
|
'background remote notifications',
|
|
);
|
|
|
|
console.log('[mobile-shell:expo-config] OK');
|