npm package: @gaozh1024/hot-updater
Description: OTA hot update helpers for Expo / React Native with manifest resolution, launch orchestration, provider hooks, and release scripts.
Latest version: 0.1.0

# `@gaozh1024/hot-updater`

面向业务 App 的 OTA 热更新封装，基于 `@hot-updater/react-native`。

## 文档导航

- 接入说明: `./docs/integration.md`
- Manifest 规范: `./docs/manifest-spec.md`
- OSS 发布流程: `./docs/oss-release-guide.md`
- 发布脚本: `./docs/release-scripts.md`
- Manifest 示例: `./docs/manifest.example.json`

## 当前推荐流程

推荐把 OTA 分成两层：

- 框架层：
  - 负责 manifest 拉取
  - 负责版本判定
  - 负责下载、重启、日志
  - 提供 Provider / hook / 发布脚本
- 项目层：
  - 在启动页调用 `startLaunchUpdateCheck()`
  - 决定使用什么弹窗组件
  - 决定启动页、登录页、首页这些业务页面怎么跳转

当前推荐的用户体验：

- 启动页进入后并行做会话恢复和 OTA 检查
- JSON 获取慢或失败时，不阻塞主流程进入 App
- 非强更：
  - 后台静默下载
  - 下载完成时可接业务弹窗
  - 如果用户稍后处理，下次打开自动应用
- 强更：
  - 不需要单独强更页面
  - 直接下载并重启

## 最小接入

```ts
import { createOssHotUpdater, createHotUpdaterContext } from '@gaozh1024/hot-updater';

export const hotUpdater = createOssHotUpdater({
  manifestBaseURL: 'https://ota-cdn.xxx.com/manifest',
});

export const { HotUpdaterProvider, useHotUpdaterContext } =
  createHotUpdaterContext(hotUpdater);
```

入口仍然建议保留：

```ts
const AppWithHotUpdater = hotUpdater.wrapApp(App);

export default AppWithHotUpdater;
```

这里主要是为了复用 `resolver` 和原生 `HotUpdater.wrap(...)` 接入；真正的启动检查建议放到业务启动页，而不是依赖默认全屏更新页。

## Android 额外要求

当前这个框架不会自动修改 Android 的 `MainApplication.kt`。  
如果使用者只安装库、不补原生入口，OTA 可能会出现“下载成功但冷启动不生效”。

Android 侧至少需要补这两处：

```kotlin
import com.hotupdater.HotUpdater

override fun getJSBundleFile(): String =
  HotUpdater.getJSBundleFile(applicationContext)

override val reactHost: ReactHost
  get() = getDefaultReactHost(applicationContext, reactNativeHost).also {
    HotUpdater.setReactHost(it)
  }
```

`@hot-updater/react-native` 的 plugin 负责的是它自己的基础接入，不等于会替业务项目补完整个 `MainApplication.kt`。

## iOS 额外要求

iOS 这边更接近“由底层 plugin 自动接入”。

我本地检查了已安装的 `@hot-updater/react-native` plugin，它会尝试：

- 给 `AppDelegate.swift` 增加 `import HotUpdater`
- 把 release 下的 `Bundle.main.url(...main.jsbundle)` 替换成 `HotUpdater.bundleURL()`

所以：

- `@gaozh1024/hot-updater` 本身不会自动 patch iOS
- 但底层 `@hot-updater/react-native` plugin 理论上会处理这一步

不过仍建议业务项目手动核对一次 `AppDelegate.swift` 是否已经是：

```swift
import HotUpdater

override func bundleURL() -> URL? {
#if DEBUG
  return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
  return HotUpdater.bundleURL()
#endif
}
```

## Provider 能力

`createHotUpdaterContext(hotUpdater, options)` 提供：

- `HotUpdaterProvider`
- `useHotUpdaterContext()`

context 内可消费：

- `summary`
- `manifest`
- `refreshManifest()`
- `checkForUpdates()`
- `startLaunchUpdateCheck()`
- `launchUpdateState`
- `reload()`

## 自定义提示框

框架默认仍然支持原生 `Alert`，但推荐项目侧注入自己的弹窗实现。

```ts
import React from 'react';
import { useAlert } from '@gaozh1024/rn-kit';
import { createHotUpdaterContext, createOssHotUpdater } from '@gaozh1024/hot-updater';

export const hotUpdater = createOssHotUpdater({
  manifestBaseURL: 'https://ota-cdn.xxx.com/manifest',
});

const { HotUpdaterProvider: BaseHotUpdaterProvider, useHotUpdaterContext } =
  createHotUpdaterContext(hotUpdater);

export function HotUpdaterProvider({ children }: { children: React.ReactNode }) {
  const { alert, confirm } = useAlert();

  return (
    <BaseHotUpdaterProvider
      manualPromptHandler={({ result, titles, actions }) => {
        if (result.status === 'up-to-date') {
          alert({
            title: titles.upToDateTitle,
            message: titles.upToDateMessage ?? result.message,
          });
          return;
        }

        if (result.status === 'error') {
          alert({
            title: titles.errorTitle,
            message: result.message,
          });
          return;
        }

        if (!result.shouldReload) {
          alert({
            title: titles.downloadedTitle,
            message: result.message,
          });
          return;
        }

        confirm({
          title: titles.downloadedTitle,
          message: result.message,
          cancelText: titles.restartLaterText,
          confirmText: titles.restartNowText,
          onConfirm: () => {
            void actions.reload();
          },
        });
      }}
      optionalUpdatePromptHandler={({ message, titles, actions }) => {
        actions.markPending();
        confirm({
          title: titles.downloadedTitle,
          message,
          cancelText: titles.restartLaterText,
          confirmText: titles.restartNowText,
          onConfirm: () => {
            void actions.reload();
          },
        });
      }}
    >
      {children}
    </BaseHotUpdaterProvider>
  );
}
```

## 文案配置

创建实例时传 `texts` 即可统一覆盖：

```ts
export const hotUpdater = createOssHotUpdater({
  manifestBaseURL: 'https://ota-cdn.xxx.com/manifest',
  texts: {
    preparingTitle: '正在准备应用',
    checkingTitle: '正在检查版本',
    updatingTitle: '正在下载更新',
    forceUpdatingTitle: '正在应用关键更新',
    upToDateTitle: '已是最新版本',
    upToDateMessage: '当前已经是最新版本。',
    checkFailedTitle: '检查更新失败',
    checkFailedMessage: '检查更新失败，请稍后重试。',
    downloadedReadyTitle: '更新已就绪',
    downloadedCompletedTitle: '更新完成',
    downloadedMessage: '更新包已下载完成，重启应用后即可生效。',
    forceUpdateCompletedMessage: '强制更新已完成，应用正在重启。',
    updateDownloadFailedMessage: '更新包下载失败，请稍后重试。',
    releaseOnlyMessage: '热更新仅在 Release 包中生效，请使用发布包验证。',
    restartNowText: '立即重启',
    restartLaterText: '稍后重启',
    launchPreparingMessage: '正在准备应用配置…',
    launchForceUpdatingMessage: '检测到关键更新，正在完成更新…',
    launchForceUpdateFailedMessage: '强制更新下载失败，请稍后重试。',
    launchForceUpdateCompletedMessage: '强制更新已完成，应用正在重启。',
    launchOptionalUpdateReadyMessage: '新版本已准备完成，重启应用后即可生效。',
    launchBackgroundUpdateFailedMessage: '后台更新失败，请稍后重试。',
    launchCheckFailedMessage: '启动时检查更新失败，请稍后重试。',
  },
});
```

## 默认行为

- 自动识别平台 `ios` / `android`
- 默认渠道优先读取 `HotUpdater.getChannel()`
- manifest 地址规则：
  - `{manifestBaseURL}/{platform}/{channel}.json`
- manifest 请求建议加 cache-buster，避免固定路径 JSON 被缓存
- 日志统一走 `hot-updater` namespace，可刷入业务 logger 浮层

## 框架负责

- 版本比较与更新判定
- 启动阶段检查与后台下载
- 手动检查更新
- 渠道、bundleId、manifest 诊断日志
- 本地 OTA 发布脚本

## 框架不负责

- OSS 上传
- CDN 刷新
- 业务弹窗样式
- 业务埋点
- 启动页、登录页、首页的业务跳转逻辑