import { createHash } from 'node:crypto' import { existsSync, readFileSync, readdirSync } from 'node:fs' import path from 'node:path' const NATIVE_EXTENSIONS = new Set([ '.c', '.cc', '.cmake', '.cpp', '.gradle', '.h', '.java', '.kts', '.kt', '.properties', '.toml', '.xml', ]) const IGNORED_DIRECTORIES = new Set(['.cxx', '.gradle', 'build', 'generated']) const IGNORED_FILES = new Set(['local.properties']) function walkNativeFiles(directory, root = directory) { if (!existsSync(directory)) return [] const files = [] for (const entry of readdirSync(directory, { withFileTypes: true })) { if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) continue const absolute = path.join(directory, entry.name) if (entry.isDirectory()) files.push(...walkNativeFiles(absolute, root)) else if ( !IGNORED_FILES.has(entry.name) && (NATIVE_EXTENSIONS.has(path.extname(entry.name)) || entry.name === 'gradlew') ) { files.push({ absolute, relative: path.relative(root, absolute).split(path.sep).join('/') }) } } return files } function packageJson(directory) { const file = path.join(directory, 'package.json') return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null } function nativeDependencyVersions(root, hostPackage) { const dependencies = { ...hostPackage.dependencies, ...hostPackage.devDependencies } const native = [] for (const name of Object.keys(dependencies).sort()) { const directory = path.join(root, 'node_modules', ...name.split('/')) const manifest = packageJson(directory) if (!manifest) continue const hasNativeCode = existsSync(path.join(directory, 'android')) || existsSync(path.join(directory, 'react-native.config.js')) || manifest.codegenConfig !== undefined if (hasNativeCode || name === 'react-native') { native.push(`${name}@${manifest.version ?? dependencies[name]}`) } } return native } /** Deterministic identity of code and dependencies that can only change through a full APK. */ export function computeNativeBaseline(root = process.cwd()) { const hostPackage = packageJson(root) if (!hostPackage) throw new Error('package.json is missing') const hash = createHash('sha256') for (const dependency of nativeDependencyVersions(root, hostPackage)) { hash.update(`dependency\0${dependency}\0`) } for (const file of walkNativeFiles(path.join(root, 'android')).sort((a, b) => a.relative.localeCompare(b.relative), )) { hash.update(`file\0${file.relative}\0`) hash.update(readFileSync(file.absolute)) hash.update('\0') } return `android-sha256:${hash.digest('hex')}` }