XuqmGroup-RNSDK/packages/update/scripts/xuqm_release.mjs
XuqmGroup 507ce3d2ee feat(update): release-set 事务制品与原生层职责拆分
- 插件包改为 .xuqm.zip 事务制品,manifest/bundle/资源/SHA-256 统一校验
- 原生层拆分为桥接编排、插件包暂存、底层存储三个单一职责类
- 状态查询改为纯读取,不再误触发安装;新增 NATIVE_BASELINE_INCOMPATIBLE 拒绝
- SemVer 解析拆分为独立原生类并补原生测试
- XWebView 收口唯一内核,错误态统一中文层与重试
- common 增加请求头/运行时契约收口与 http 测试
2026-07-20 19:31:43 +08:00

138 行
5.5 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { createInterface } from 'node:readline'
import process from 'node:process'
import {
SUPPORTED_PLATFORMS,
bundleOutputDirectory,
parseModuleOptions,
readConfig,
resolveVersionedModules,
selectModules,
} from './xuqm-config.mjs'
import { computeNativeBaseline } from './native-baseline.mjs'
const root = process.cwd()
const rawArgs = process.argv.slice(2)
function takeOption(name) {
const index = rawArgs.indexOf(name)
if (index < 0) return undefined
const value = rawArgs[index + 1]
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`)
rawArgs.splice(index, 2)
return value
}
const platform = takeOption('--platform') ?? 'android'
const noteOption = takeOption('--note')
const minNativeApiLevelOption = takeOption('--min-native-api-level')
const publishOptionIndex = rawArgs.indexOf('--publish')
const publishOption = publishOptionIndex >= 0
if (publishOption) rawArgs.splice(publishOptionIndex, 1)
const { moduleIds, remaining } = parseModuleOptions(rawArgs)
if (remaining.length) throw new Error(`unknown option(s): ${remaining.join(' ')}`)
if (!SUPPORTED_PLATFORMS.has(platform)) throw new Error('platform must be android or ios')
const config = readConfig(root)
const versioned = resolveVersionedModules(config, root, process.env.XUQM_APP_VERSION)
const selectedModules = selectModules({ ...config, modules: versioned.modules }, moduleIds)
if (moduleIds.length > 0 && selectedModules.some(module => module.type === 'startup')) {
throw new Error('startup is embedded in the host APK and cannot be published independently')
}
const modules = selectedModules.filter(module => module.type !== 'startup')
if (modules.length === 0) throw new Error('no publishable common/app/buz module selected')
const nativeBaselineId = computeNativeBaseline(root, platform)
const serverUrl = process.env.XUQM_SERVER_URL ?? config.release?.serverUrl
const appKey = process.env.XUQM_APP_KEY ?? config.release?.appKey
const apiToken = process.env.XUQM_API_TOKEN ?? config.release?.apiToken
const androidPropertiesFile = path.join(root, 'android', 'gradle.properties')
const bundleFormat =
platform === 'android' &&
existsSync(androidPropertiesFile) &&
/^\s*hermesEnabled\s*=\s*true\s*$/im.test(readFileSync(androidPropertiesFile, 'utf8'))
? 'hermes-bytecode'
: 'javascript'
if (!serverUrl || !appKey || !apiToken) {
throw new Error(
'publish requires XUQM_SERVER_URL, XUQM_APP_KEY and XUQM_API_TOKEN (or release.* config)',
)
}
const rl = createInterface({ input: process.stdin, output: process.stdout })
const ask = question => new Promise(resolve => rl.question(`\x1b[36m${question}\x1b[0m`, resolve))
const confirm = async question => /^y/i.test(((await ask(`${question} [y/N]: `)) ?? '').trim())
function apiHeaders() {
return { Authorization: `Bearer ${apiToken}` }
}
async function uploadBundle(module, note, minNativeApiLevel) {
const file = path.join(
bundleOutputDirectory(root, config, platform, module.id),
`${module.id}.${platform}.xuqm.zip`,
)
if (!existsSync(file)) throw new Error(`plugin package is missing: ${file}`)
const form = new FormData()
form.append('appKey', appKey)
form.append('moduleId', module.id)
form.append('platform', platform.toUpperCase())
form.append('version', module.moduleVersion)
form.append('appVersionRange', module.appVersionRange)
form.append('builtAgainstNativeBaselineId', nativeBaselineId)
form.append('bundleFormat', bundleFormat)
form.append('commonVersionRange', module.commonVersionRange ?? '')
form.append('minNativeApiLevel', String(module.minNativeApiLevel ?? minNativeApiLevel))
form.append('note', note)
if (config.packageName) form.append('packageName', config.packageName)
form.append('bundle', new Blob([readFileSync(file)]), `${module.id}.${platform}.xuqm.zip`)
const response = await fetch(`${serverUrl}/api/v1/rn/upload`, {
method: 'POST',
headers: apiHeaders(),
body: form,
})
if (!response.ok) throw new Error(`upload failed for ${module.id}: ${await response.text()}`)
return response.json()
}
async function main() {
const note = noteOption ?? (await ask('Release notes: '))
const minNativeApiLevel = Number(
minNativeApiLevelOption ?? config.release?.minNativeApiLevel ?? 2,
)
if (!Number.isInteger(minNativeApiLevel) || minNativeApiLevel < 1) {
throw new Error('minNativeApiLevel must be a positive integer')
}
const publishImmediately = publishOption || config.release?.publishImmediately === true
console.log('\n\x1b[36m--- Xuqm RN plugin publish ---\x1b[0m')
console.log(` Platform: ${platform}`)
console.log(
` Modules: ${modules.map(module => `${module.id}@${module.moduleVersion}`).join(', ')}`,
)
console.log(` Publish immediately: ${publishImmediately}`)
if (!(await confirm('Proceed?'))) return
for (const module of modules) {
const result = await uploadBundle(module, note, minNativeApiLevel)
const bundleId = result.data?.id
console.log(`\x1b[32m✓ ${module.id}/${platform} uploaded, ID: ${bundleId}\x1b[0m`)
if (publishImmediately) {
const response = await fetch(`${serverUrl}/api/v1/rn/${bundleId}/publish`, {
method: 'POST',
headers: apiHeaders(),
})
if (!response.ok) throw new Error(`publish failed for ${module.id}: ${await response.text()}`)
console.log(` ${module.id}/${platform} published`)
}
}
}
main()
.catch(error => {
console.error(`\n\x1b[31m${error.message}\x1b[0m`)
process.exitCode = 1
})
.finally(() => rl.close())