- 插件包改为 .xuqm.zip 事务制品,manifest/bundle/资源/SHA-256 统一校验 - 原生层拆分为桥接编排、插件包暂存、底层存储三个单一职责类 - 状态查询改为纯读取,不再误触发安装;新增 NATIVE_BASELINE_INCOMPATIBLE 拒绝 - SemVer 解析拆分为独立原生类并补原生测试 - XWebView 收口唯一内核,错误态统一中文层与重试 - common 增加请求头/运行时契约收口与 http 测试
141 行
4.3 KiB
JavaScript
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')}`
|
|
}
|