Files
Genarrative/apps/mobile-shell/scripts/check-expo-export.mjs
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

179 lines
5.0 KiB
JavaScript

import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
const shellRoot = new URL('../', import.meta.url);
const outputRoot = new URL('../.expo-export-smoke/', import.meta.url);
const hostBridgeContractUrl = new URL(
'../../../packages/shared/src/contracts/hostBridge.ts',
import.meta.url,
);
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const platforms = ['android', 'ios'];
const blockedDevelopmentWebUrlPatterns = [
/http:\\?\/\\?\/localhost(?::\d+)?/u,
/http:\\?\/\\?\/127\.0\.0\.1(?::\d+)?/u,
/http:\\?\/\\?\/\[::1\](?::\d+)?/u,
];
function readHostBridgePublicWebUrl() {
const contractSource = fs.readFileSync(hostBridgeContractUrl, 'utf8');
const publicWebUrlMatch = contractSource.match(
/HOST_BRIDGE_PUBLIC_WEB_URL\s*=\s*'([^']+)'/u,
);
const publicWebOriginMatch = contractSource.match(
/HOST_BRIDGE_PUBLIC_WEB_ORIGIN\s*=\s*'([^']+)'/u,
);
if (!publicWebUrlMatch || !publicWebOriginMatch) {
throw new Error('HostBridge public web URL contract is missing');
}
const publicWebUrl = publicWebUrlMatch[1];
const publicWebOrigin = publicWebOriginMatch[1];
if (new URL(publicWebUrl).origin !== publicWebOrigin) {
throw new Error(
'HostBridge public web URL and origin contract must point to the same origin',
);
}
return publicWebUrl;
}
const expectedPublicWebUrl = readHostBridgePublicWebUrl();
const requiredNativeHostContextTokens = [
'native_app',
'expo_mobile',
'hostCapabilities',
'hostVersion',
'bridgeVersion',
];
function runExpoExport(platform) {
const outputDir = `.expo-export-smoke/${platform}`;
const result = spawnSync(
npmCommand,
[
'exec',
'expo',
'export',
'--',
'--platform',
platform,
'--output-dir',
outputDir,
],
{
cwd: shellRoot,
encoding: 'utf8',
stdio: 'pipe',
},
);
if (result.error) {
throw new Error(
`failed to start Expo ${platform} export smoke: ${result.error.message}`,
);
}
if (result.signal) {
throw new Error(
`Expo ${platform} export 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 readMetadata(platform) {
const metadataPath = new URL(`${platform}/metadata.json`, outputRoot);
if (!fs.existsSync(metadataPath)) {
throw new Error(`Expo ${platform} export did not produce metadata.json`);
}
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
if (metadata.version !== 0) {
throw new Error(`Expo ${platform} export metadata version drifted`);
}
if (metadata.bundler !== 'metro') {
throw new Error(`Expo ${platform} export must use the Metro bundler`);
}
const fileMetadata = metadata.fileMetadata ?? {};
const metadataPlatforms = Object.keys(fileMetadata);
if (metadataPlatforms.length !== 1 || metadataPlatforms[0] !== platform) {
throw new Error(`Expo ${platform} export metadata must only include its platform`);
}
const platformMetadata = fileMetadata[platform];
if (!Array.isArray(platformMetadata?.assets)) {
throw new Error(`Expo ${platform} export metadata must include an assets array`);
}
const bundlePath = platformMetadata?.bundle;
if (typeof bundlePath !== 'string' || bundlePath.length === 0) {
throw new Error(`Expo ${platform} export metadata is missing bundle path`);
}
return bundlePath;
}
function assertBundle(platform, bundlePath) {
const bundleFile = new URL(`${platform}/${bundlePath}`, outputRoot);
if (!fs.existsSync(bundleFile)) {
throw new Error(`Expo ${platform} bundle is missing: ${bundlePath}`);
}
const stats = fs.statSync(bundleFile);
if (stats.size < 100_000) {
throw new Error(`Expo ${platform} bundle is unexpectedly small`);
}
if (
!bundlePath.startsWith(`_expo/static/js/${platform}/AppEntry-`) ||
!bundlePath.endsWith('.hbc')
) {
throw new Error(`Expo ${platform} bundle path does not target AppEntry`);
}
const bundleSource = fs.readFileSync(bundleFile, 'utf8');
if (!bundleSource.includes(expectedPublicWebUrl)) {
throw new Error(
`Expo ${platform} production bundle must include the shared public web URL`,
);
}
for (const token of requiredNativeHostContextTokens) {
if (!bundleSource.includes(token)) {
throw new Error(
`Expo ${platform} production bundle must include native host context token ${token}`,
);
}
}
for (const blockedPattern of blockedDevelopmentWebUrlPatterns) {
if (blockedPattern.test(bundleSource)) {
throw new Error(
`Expo ${platform} production bundle must not include a local development H5 URL`,
);
}
}
}
fs.rmSync(outputRoot, {recursive: true, force: true});
try {
for (const platform of platforms) {
runExpoExport(platform);
assertBundle(platform, readMetadata(platform));
}
console.log('[mobile-shell:expo-export] OK');
} finally {
fs.rmSync(outputRoot, {recursive: true, force: true});
}