- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南 - 添加 SDK API 重设计规范,统一初始化和登录接口设计 - 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案 - 创建平台 REST API 规范,定义服务端到服务端的调用接口 - 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
173 行
7.8 KiB
JavaScript
173 行
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* XuqmGroup Update Service — React Native Bundle Release Script
|
|
*
|
|
* Platform-native approach: runs via Node.js (always available in RN projects).
|
|
* Add to your app's package.json scripts:
|
|
* "xuqm:release": "node node_modules/@xuqm/update/scripts/xuqm_release.mjs"
|
|
*
|
|
* Config: xuqm.config.json in the project root
|
|
* {
|
|
* "serverUrl": "https://update.dev.xuqinmin.com",
|
|
* "appKey": "your-app-key",
|
|
* "apiToken": "your-api-token",
|
|
* "rn": {
|
|
* "modules": [
|
|
* { "moduleId": "main", "entryFile": "index.js", "packageName": "com.example.app", "platforms": ["android", "ios"] }
|
|
* ],
|
|
* "bundleOutputDir": "/tmp/xuqm_rn_bundle",
|
|
* "publishImmediately": false
|
|
* }
|
|
* }
|
|
*
|
|
* Dependencies: none beyond Node.js built-ins (uses native fetch + node:child_process)
|
|
* Minimum Node.js: 18 (built-in fetch)
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process'
|
|
import { createReadStream, mkdirSync, existsSync, readFileSync } from 'node:fs'
|
|
import { createInterface } from 'node:readline'
|
|
import path from 'node:path'
|
|
|
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
|
|
const CONFIG_FILE = process.env.XUQM_CONFIG_FILE ?? 'xuqm.config.json'
|
|
if (!existsSync(CONFIG_FILE)) {
|
|
console.error(`[xuqm] Config not found: ${CONFIG_FILE}`)
|
|
process.exit(1)
|
|
}
|
|
const cfg = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
|
|
const { serverUrl, appKey, apiToken, rn = {} } = cfg
|
|
if (!serverUrl || !appKey || !apiToken) {
|
|
console.error('[xuqm] serverUrl / appKey / apiToken are required in config')
|
|
process.exit(1)
|
|
}
|
|
const modules = rn.modules ?? [{ moduleId: 'main', entryFile: 'index.js', platforms: ['android', 'ios'] }]
|
|
const bundleOutputDir = rn.bundleOutputDir ?? '/tmp/xuqm_rn_bundle'
|
|
const publishImmediately = rn.publishImmediately === true || rn.publishImmediately === 'true'
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
const ask = (q) => new Promise(res => rl.question(`\x1b[36m${q}\x1b[0m`, res))
|
|
const confirm = async (q) => /^y/i.test(((await ask(`${q} [y/N]: `)) ?? '').trim())
|
|
|
|
function apiHeaders() { return { Authorization: `Bearer ${apiToken}` } }
|
|
|
|
async function apiFetch(path, opts = {}) {
|
|
const res = await fetch(`${serverUrl}${path}`, { headers: apiHeaders(), ...opts })
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
|
return res.json()
|
|
}
|
|
|
|
// Multipart upload using FormData (Node 18+ has built-in FormData)
|
|
async function uploadBundle(moduleId, platform, bundleFile, version, minVersion, note, packageName) {
|
|
const form = new FormData()
|
|
const blob = new Blob([readFileSync(bundleFile)], { type: 'application/octet-stream' })
|
|
form.append('appId', appKey)
|
|
form.append('moduleId', moduleId)
|
|
form.append('platform', platform.toUpperCase())
|
|
form.append('version', version)
|
|
form.append('minCommonVersion', minVersion)
|
|
form.append('note', note)
|
|
if (packageName) form.append('packageName', packageName)
|
|
form.append('bundle', blob, path.basename(bundleFile))
|
|
|
|
const res = await fetch(`${serverUrl}/api/v1/rn/upload`, {
|
|
method: 'POST',
|
|
headers: apiHeaders(),
|
|
body: form,
|
|
})
|
|
if (!res.ok) throw new Error(`Upload failed: ${await res.text()}`)
|
|
return res.json()
|
|
}
|
|
|
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
console.log('\x1b[36m=== XuqmGroup React Native Release ===\x1b[0m\n')
|
|
|
|
// ── 1. Local version from package.json ──────────────────────────────────
|
|
const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
|
|
let localVersion = pkg.version ?? ''
|
|
console.log(`Local version (package.json): \x1b[32m${localVersion}\x1b[0m`)
|
|
|
|
// ── 2. Server latest ─────────────────────────────────────────────────────
|
|
let serverVersion = 'none'
|
|
try {
|
|
const resp = await apiFetch(`/api/v1/rn/list?appId=${appKey}`)
|
|
const published = (resp.data ?? []).filter(x => x.publishStatus === 'PUBLISHED')
|
|
serverVersion = published[0]?.version ?? 'none'
|
|
} catch { /* no bundles yet */ }
|
|
console.log(`Server latest: \x1b[33m${serverVersion}\x1b[0m`)
|
|
|
|
// ── 3. Validate version ───────────────────────────────────────────────────
|
|
if (localVersion && serverVersion !== 'none' && localVersion <= serverVersion) {
|
|
console.warn(`\x1b[33m⚠ Local (${localVersion}) ≤ server (${serverVersion})\x1b[0m`)
|
|
const cont = await confirm('Continue anyway?')
|
|
if (!cont) { localVersion = await ask('New version string: ') }
|
|
}
|
|
if (!localVersion) localVersion = await ask('Version string (e.g. 1.2.3): ')
|
|
|
|
// ── 4. Options ────────────────────────────────────────────────────────────
|
|
const note = await ask('Release notes: ')
|
|
const minVersion = await ask('Min compatible native version (e.g. 1.0.0): ')
|
|
|
|
console.log('\n\x1b[36m--- Summary ---\x1b[0m')
|
|
console.log(` Version: ${localVersion}`)
|
|
console.log(` Modules: ${modules.map(m => m.moduleId).join(', ')}`)
|
|
const publishNow = publishImmediately || await confirm('Publish uploaded bundles immediately?')
|
|
const ok = await confirm('Proceed?')
|
|
if (!ok) { console.log('Aborted.'); rl.close(); return }
|
|
|
|
// ── 5. Build & upload each module/platform ────────────────────────────────
|
|
mkdirSync(bundleOutputDir, { recursive: true })
|
|
|
|
for (const mod of modules) {
|
|
for (const platform of mod.platforms) {
|
|
const outDir = path.join(bundleOutputDir, mod.moduleId, platform)
|
|
mkdirSync(outDir, { recursive: true })
|
|
const bundleFile = path.join(outDir, `${mod.moduleId}.${platform}.bundle`)
|
|
|
|
// Build using react-native CLI (platform-native, no extra deps)
|
|
console.log(`\n\x1b[36mBuilding ${mod.moduleId}/${platform}...\x1b[0m`)
|
|
execSync(
|
|
`npx react-native bundle \
|
|
--platform ${platform} \
|
|
--entry-file ${mod.entryFile ?? 'index.js'} \
|
|
--bundle-output ${bundleFile} \
|
|
--dev false \
|
|
--minify true`,
|
|
{ stdio: 'inherit' }
|
|
)
|
|
|
|
// Upload
|
|
console.log('\x1b[36mUploading...\x1b[0m')
|
|
const resp = await uploadBundle(mod.moduleId, platform, bundleFile, localVersion, minVersion, note, mod.packageName)
|
|
const bundleId = resp.data?.id
|
|
console.log(`\x1b[32m✓ ${mod.moduleId}/${platform} uploaded, ID: ${bundleId}\x1b[0m`)
|
|
if (publishNow) {
|
|
const publishResp = await fetch(`${serverUrl}/api/v1/rn/${bundleId}/publish`, {
|
|
method: 'POST',
|
|
headers: apiHeaders(),
|
|
})
|
|
if (!publishResp.ok) {
|
|
throw new Error(`Publish failed: ${publishResp.status} ${await publishResp.text()}`)
|
|
}
|
|
console.log(` Published immediately: POST ${serverUrl}/api/v1/rn/${bundleId}/publish`)
|
|
} else {
|
|
console.log(` Publish: POST ${serverUrl}/api/v1/rn/${bundleId}/publish`)
|
|
}
|
|
}
|
|
}
|
|
|
|
rl.close()
|
|
console.log('\n\x1b[32m=== Release complete ===\x1b[0m')
|
|
}
|
|
|
|
main().catch(e => {
|
|
console.error(`\n\x1b[31m❌ ${e.message}\x1b[0m`)
|
|
rl.close()
|
|
process.exit(1)
|
|
})
|