XuqmGroup-RNSDK/packages/update/scripts/native-baseline.mjs

141 行
4.3 KiB
JavaScript

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',
'.m',
'.mm',
'.pbxproj',
'.podspec',
'.properties',
'.swift',
'.toml',
'.xcconfig',
'.xml',
])
const PACKAGED_ASSET_EXTENSIONS = new Set([
'.bmp',
'.gif',
'.jpeg',
'.jpg',
'.mp3',
'.mp4',
'.otf',
'.png',
'.svg',
'.ttf',
'.webp',
'.wav',
])
const IGNORED_DIRECTORIES = new Set([
'.cxx',
'.git',
'.gradle',
'build',
'bundle',
'coverage',
'dist',
'generated',
'node_modules',
'rn-bundles',
])
const IGNORED_FILES = new Set(['local.properties'])
function walkFiles(directory, root, extensions, exactNames = new Set()) {
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(...walkFiles(absolute, root, extensions, exactNames))
else if (
!IGNORED_FILES.has(entry.name) &&
(extensions.has(path.extname(entry.name).toLowerCase()) || exactNames.has(entry.name))
) {
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 runtimeDependencyVersions(root, hostPackage) {
const dependencies = hostPackage.dependencies ?? {}
const runtime = []
for (const name of Object.keys(dependencies).sort()) {
const directory = path.join(root, 'node_modules', ...name.split('/'))
const manifest = packageJson(directory)
runtime.push(`${name}@${manifest?.version ?? dependencies[name]}`)
}
return runtime
}
/**
* 开发期 file: 依赖不会因为源码变化自动提升 package version因此必须把它们的
* 原生源码纳入完整包基线正式 registry 依赖仍由不可变版本号标识
*/
function localDependencyNativeFiles(root, hostPackage, platform) {
const dependencies = hostPackage.dependencies ?? {}
const files = []
for (const [name, requested] of Object.entries(dependencies).sort(([left], [right]) =>
left.localeCompare(right),
)) {
if (typeof requested !== 'string' || !requested.startsWith('file:')) continue
const dependencyRoot = path.resolve(root, requested.slice('file:'.length))
for (const file of walkFiles(
path.join(dependencyRoot, platform),
dependencyRoot,
new Set([...NATIVE_EXTENSIONS, ...PACKAGED_ASSET_EXTENSIONS]),
new Set(platform === 'android' ? ['gradlew'] : ['Podfile']),
)) {
files.push({ ...file, relative: `local-dependency/${name}/${file.relative}` })
}
}
return files
}
/** Deterministic identity of code and dependencies that can only change through a full APK. */
export function computeNativeBaseline(root = process.cwd(), platform = 'android') {
if (platform !== 'android' && platform !== 'ios') {
throw new Error('platform must be android or ios')
}
const hostPackage = packageJson(root)
if (!hostPackage) throw new Error('package.json is missing')
const hash = createHash('sha256')
hash.update(`platform\0${platform}\0`)
for (const dependency of runtimeDependencyVersions(root, hostPackage)) {
hash.update(`dependency\0${dependency}\0`)
}
const nativeFiles = walkFiles(
path.join(root, platform),
root,
new Set([...NATIVE_EXTENSIONS, ...PACKAGED_ASSET_EXTENSIONS]),
new Set(platform === 'android' ? ['gradlew'] : ['Podfile']),
)
const sourceAssets = [path.join(root, 'src'), path.join(root, 'assets')].flatMap(directory =>
walkFiles(directory, root, PACKAGED_ASSET_EXTENSIONS),
)
const localDependencyFiles = localDependencyNativeFiles(root, hostPackage, platform)
for (const file of [...nativeFiles, ...sourceAssets, ...localDependencyFiles].sort((a, b) =>
a.relative.localeCompare(b.relative),
)) {
hash.update(`file\0${file.relative}\0`)
hash.update(readFileSync(file.absolute))
hash.update('\0')
}
return `${platform}-sha256:${hash.digest('hex')}`
}