feat(update): support safe plugin bundle reload

这个提交包含在:
XuqmGroup 2026-07-17 14:12:02 +08:00
父节点 c8a6d88ab9
当前提交 e7b31d7fc6
共有 5 个文件被更改,包括 48 次插入11 次删除

查看文件

@ -108,6 +108,8 @@ pnpm --dir packages/update test
- alpha 版本号改为取源码现有序号与 Nexus 远端序号的最大值后加一,禁止版本倒退和重复发布。
- `#49` 被 Jenkins CPS 的不可序列化正则 Matcher 阻断;算法改为只保留整数,并从 Nexus group 读标签、向 hosted 写制品。
- `#50` 证明旧 Jenkins 工作树仍保留 CRLF;主检出由 `CleanBeforeCheckout` 改为 `WipeWorkspace`,确保 `.gitattributes` 在全新工作树生效。
- `XuqmRuntime.activate` 增加显式 `reloadBundle`buz-only release 可替换旧定义并热加载;含 common 的 release 仍由宿主冷启动。
- `#51` 通过换行、格式和类型门禁后,暴露 pack 校验脚本硬编码 Unix `npm`;现改为 `fileURLToPath` 与 Windows `cmd.exe/npm.cmd` 分支。
### 2026-07-17 / common 第一轮

查看文件

@ -37,6 +37,10 @@ apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle
## 运行时规则
安装仅包含 buz 的 release set 后,使用
`XuqmRuntime.activate(moduleId, { reloadBundle: true })` 重新求值并激活新 bundle。若 release set
包含 common,宿主必须冷启动,不能在同一 JavaScript VM 中混用新旧 common。
```ts
import { UpdateSDK, XuqmRuntime } from '@xuqm/rn-update'

查看文件

@ -4,15 +4,26 @@ import { execFileSync } from 'node:child_process'
import { readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const cwd = new URL('..', import.meta.url).pathname
const result = JSON.parse(
execFileSync('npm', ['pack', '--json', '--ignore-scripts'], {
cwd,
encoding: 'utf8',
env: { ...process.env, npm_config_cache: path.join(tmpdir(), 'xuqm-rn-npm-cache') },
}),
)[0]
const cwd = fileURLToPath(new URL('..', import.meta.url))
const packOutput =
process.platform === 'win32'
? execFileSync(
process.env.ComSpec ?? 'cmd.exe',
['/d', '/s', '/c', 'npm.cmd pack --json --ignore-scripts'],
{
cwd,
encoding: 'utf8',
env: { ...process.env, npm_config_cache: path.join(tmpdir(), 'xuqm-rn-npm-cache') },
},
)
: execFileSync('npm', ['pack', '--json', '--ignore-scripts'], {
cwd,
encoding: 'utf8',
env: { ...process.env, npm_config_cache: path.join(tmpdir(), 'xuqm-rn-npm-cache') },
})
const result = JSON.parse(packOutput)[0]
const required = [
'android/src/main/AndroidManifest.xml',
'android/src/main/java/com/xuqm/update/XuqmBundleModule.java',

查看文件

@ -19,6 +19,11 @@ export interface XuqmRuntimeOptions {
checkUpdatesOnStart?: boolean
}
export interface XuqmPluginActivationOptions {
/** Re-evaluate the active bundle after a buz-only release was installed. */
reloadBundle?: boolean
}
const definitions = new Map<string, XuqmPluginDefinition>()
/** Called by a plugin bundle when it is evaluated. */
@ -55,9 +60,19 @@ export const XuqmRuntime = (() => {
return { kind: 'none' as const }
},
async activate(moduleId: string): Promise<void> {
async activate(
moduleId: string,
activationOptions: XuqmPluginActivationOptions = {},
): Promise<void> {
if (!options) throw new Error('[XuqmRuntime] configure() must be called before activate()')
if (active.has(moduleId)) return
if (active.has(moduleId) && !activationOptions.reloadBundle) return
if (activationOptions.reloadBundle) {
const previous = definitions.get(moduleId)
if (previous?.deactivate) await previous.deactivate()
active.delete(moduleId)
definitions.delete(moduleId)
}
let definition = definitions.get(moduleId)
if (!definition) {

查看文件

@ -20,4 +20,9 @@ export type {
ReleaseSetPlan,
} from './releaseSet'
export { XuqmRuntime, definePlugin } from './PluginRuntime'
export type { XuqmPluginContext, XuqmPluginDefinition, XuqmRuntimeOptions } from './PluginRuntime'
export type {
XuqmPluginActivationOptions,
XuqmPluginContext,
XuqmPluginDefinition,
XuqmRuntimeOptions,
} from './PluginRuntime'