import { createHash } from 'node:crypto' import { brotliBundle } from 'sootsim-engine/preview/compress-node' import { runPresignedUpload, type InitRequestBody, } from 'sootsim-engine/preview/presigned-upload' import { authHeaderValue, cloudAccountIdOrExit, resolveCliAuth } from '../auth' import { producePreviewArtifact } from '../cloud-client' import { openUrl } from '../open-url' import { resolveDefaultUploadOrigin, resolvePublicPreviewOrigin } from './upload' function takeValue(args: string[], flag: string): string | null { const index = args.indexOf(flag) if (index < 0) return null const value = args[index + 1] args.splice(index, 2) if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`) return value } export async function runPreview(args: string[]): Promise { if (args.includes('--help') || args.includes('-h')) { console.log(` rnx preview — publish a prebuilt React Native bundle as a standalone share usage: rnx preview [--platform ios|android] [--open] The share contains the immutable build and its automatically discovered Metro assets. It is not a recorded replay. `) return 0 } try { const input = [...args] const origin = await resolveDefaultUploadOrigin( takeValue(input, '--origin') ?? undefined, ) const publicOrigin = resolvePublicPreviewOrigin( origin, takeValue(input, '--public-origin') ?? undefined, ) const platform = takeValue(input, '--platform') ?? 'ios' const openIndex = input.indexOf('--open') const open = openIndex >= 0 if (open) input.splice(openIndex, 1) if (platform !== 'ios' && platform !== 'android') { throw new Error('--platform must be ios or android') } if (input.length !== 1 || input[0]?.startsWith('-')) { throw new Error('usage: rnx preview ') } const auth = resolveCliAuth() if (!auth) throw new Error('rnx preview needs `rnx login` or RNX_API_KEY') const accountId = cloudAccountIdOrExit(auth) const artifact = await producePreviewArtifact(input[0]) const bundle = brotliBundle(artifact.bytes) const initBody: InitRequestBody = { kind: 'build', buildPlatform: platform, contentHash: createHash('sha256').update(artifact.bytes).digest('hex'), bundleSizeBytes: artifact.bytes.byteLength, bundleContentType: 'application/javascript', bundleEncoding: 'br', bundleMinified: true, bundleOrigin: null, entry: null, isTransformed: false, deviceSpec: platform === 'android' ? { model: 'pixel-8', width: 412, height: 915 } : { model: 'iphone-16', width: 402, height: 874 }, files: [], } const { finalize } = await runPresignedUpload({ originBase: origin, endpoints: { init: '/api/v1/previews', finalize: '/api/v1/previews/finalize', }, initBody, bundleBytes: bundle, filesByHash: new Map(), authHeader: authHeaderValue(auth), accountId, }) const url = finalize.url.startsWith('http') ? finalize.url : `${publicOrigin}${finalize.url}` console.log(` preview: ${url}`) console.log( ` artifact: sha256:${artifact.sha256} (${artifact.bytes.byteLength} bytes)`, ) for (const warning of artifact.warnings) console.warn(` warning: ${warning}`) if (open) await openUrl(url) return 0 } catch (error) { console.error( ` rnx preview failed: ${error instanceof Error ? error.message : String(error)}`, ) return 1 } }