import {spawnSync} from 'node:child_process'; import fs from 'node:fs'; const shellRoot = new URL('../', import.meta.url); const easConfigPath = new URL('eas.json', shellRoot); const packagePath = new URL('package.json', shellRoot); const easConfig = JSON.parse(fs.readFileSync(easConfigPath, 'utf8')); const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const androidBuildOutputPath = '../../build/native/mobile/genarrative-mobile-android.apk'; const iosSimulatorBuildOutputPath = '../../build/native/mobile/genarrative-mobile-ios-simulator.tar.gz'; function assertObject(value, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${label} must be an object`); } } function assertNoKeys(value, blockedKeys, label) { for (const key of blockedKeys) { if (key in value) { throw new Error(`${label} must not configure ${key}`); } } } assertObject(easConfig.cli, 'EAS cli config'); assertObject(easConfig.build, 'EAS build config'); if (easConfig.cli.version !== '>= 20.3.0') { throw new Error('mobile shell EAS CLI version gate must stay pinned to the checked devDependency floor'); } if (packageConfig.devDependencies?.['eas-cli'] !== '^20.3.0') { throw new Error('mobile shell package must declare the checked EAS CLI devDependency'); } const easVersionResult = spawnSync(npmCommand, ['exec', 'eas', '--', '--version'], { cwd: shellRoot, encoding: 'utf8', }); if (easVersionResult.error) { throw new Error(`failed to start local EAS CLI version check: ${easVersionResult.error.message}`); } if (easVersionResult.signal) { throw new Error(`local EAS CLI version check was terminated by signal ${easVersionResult.signal}`); } if ((easVersionResult.status ?? 0) !== 0) { process.stdout.write(easVersionResult.stdout ?? ''); process.stderr.write(easVersionResult.stderr ?? ''); process.exit(easVersionResult.status ?? 1); } if (!(easVersionResult.stdout ?? '').trim().startsWith('eas-cli/20.3.0 ')) { throw new Error('mobile shell local EAS CLI version drifted'); } if (easConfig.cli.appVersionSource !== 'local') { throw new Error('mobile shell EAS builds must use local app.json version fields'); } for (const scriptName of ['build:android', 'build:ios', 'build-config:smoke']) { if (typeof packageConfig.scripts?.[scriptName] !== 'string') { throw new Error(`mobile shell package missing ${scriptName} script`); } } if ( packageConfig.scripts['build:android'] !== `eas build --local --profile production --platform android --output ${androidBuildOutputPath}` ) { throw new Error('mobile shell Android package build script drifted'); } if ( packageConfig.scripts['build:ios'] !== `eas build --local --profile production-simulator --platform ios --output ${iosSimulatorBuildOutputPath}` ) { throw new Error('mobile shell iOS package build script drifted'); } for (const [label, outputPath, extension] of [ ['Android', androidBuildOutputPath, '.apk'], ['iOS simulator', iosSimulatorBuildOutputPath, '.tar.gz'], ]) { if (!outputPath.startsWith('../../build/native/mobile/')) { throw new Error(`mobile shell ${label} local build output must stay under root build/native/mobile`); } if (!outputPath.endsWith(extension)) { throw new Error(`mobile shell ${label} local build output must end with ${extension}`); } } if (packageConfig.scripts['build-config:smoke'] !== 'node scripts/check-eas-build-config.mjs') { throw new Error('mobile shell EAS config smoke script drifted'); } const production = easConfig.build.production; const productionSimulator = easConfig.build['production-simulator']; assertObject(production, 'EAS Android production profile'); assertObject(productionSimulator, 'EAS iOS simulator production profile'); if (production.distribution !== 'internal' || production.channel !== 'production') { throw new Error('mobile shell Android production profile must build an internal production package'); } if (production.android?.buildType !== 'apk') { throw new Error('mobile shell Android production profile must produce an installable APK'); } if (production.env?.EXPO_NO_DOTENV !== '1') { throw new Error('mobile shell Android production profile must ignore local dotenv files'); } if ( productionSimulator.distribution !== 'internal' || productionSimulator.channel !== 'production' ) { throw new Error('mobile shell iOS simulator production profile must stay internal production'); } if (productionSimulator.ios?.simulator !== true) { throw new Error('mobile shell iOS production smoke profile must target simulator builds until signing is configured'); } if (productionSimulator.env?.EXPO_NO_DOTENV !== '1') { throw new Error('mobile shell iOS simulator production profile must ignore local dotenv files'); } for (const [profileName, profile] of Object.entries(easConfig.build)) { assertObject(profile, `EAS ${profileName} profile`); assertNoKeys( profile, [ 'credentialsSource', 'autoIncrement', 'submit', 'runtimeVersion', 'releaseChannel', ], `EAS ${profileName} profile`, ); } if ('submit' in easConfig) { throw new Error('mobile shell EAS config must not include store submit profiles yet'); } console.log('[mobile-shell:eas-build-config] OK');