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 npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const platforms = ['android', 'ios']; 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.bundler !== 'metro') { throw new Error(`Expo ${platform} export must use the Metro bundler`); } const bundlePath = metadata.fileMetadata?.[platform]?.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.includes(`/static/js/${platform}/AppEntry-`)) { throw new Error(`Expo ${platform} bundle path does not target AppEntry`); } } 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}); }