018697203f
清理冗余导入、可变绑定和赋值,将 AGC 默认编译警告从 231 条降至 195 条 按测试和平台边界限定导入,保留兼容出口与待核查的业务参数 修复预览部署器嵌套 npm 警告和移动端检查脚本的 Windows 启动错误 更新警告清单、开发运维文档与共享开发流程,记录验证结果和剩余项
179 lines
5.3 KiB
JavaScript
179 lines
5.3 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
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 shellRequire = createRequire(new URL('package.json', shellRoot));
|
|
const expoPackagePath = shellRequire.resolve('expo/package.json');
|
|
const expoPackage = JSON.parse(fs.readFileSync(expoPackagePath, 'utf8'));
|
|
const expoCliPath = resolve(dirname(expoPackagePath), expoPackage.bin.expo);
|
|
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(
|
|
process.execPath,
|
|
[expoCliPath, '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 });
|
|
}
|