mirror of
https://gitcode.com/gh_mirrors/re/react-native-pushy.git
synced 2025-09-17 22:26:09 +08:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d17eac48f0 | ||
![]() |
8e1a639dda | ||
![]() |
5999670917 | ||
![]() |
0f6875d8d5 | ||
![]() |
3752ad4e5d | ||
![]() |
a2f01b6213 | ||
![]() |
c2499f799b | ||
![]() |
680f77a8d8 | ||
![]() |
eaae1286f4 | ||
![]() |
20a21ae894 | ||
![]() |
5abe7b181b | ||
![]() |
e859238d97 | ||
![]() |
7796090e72 | ||
![]() |
f07be8cf0b | ||
![]() |
7c8f2d493f | ||
![]() |
437eb4f10f | ||
![]() |
5053a89cfa | ||
![]() |
4a84622c04 | ||
![]() |
cf812eacda | ||
![]() |
6a031f598f | ||
![]() |
7ecf879ac9 | ||
![]() |
c779befa86 | ||
![]() |
19f060abf5 | ||
![]() |
b9c655312e | ||
![]() |
2ad4635bc6 | ||
![]() |
0d3f95d2bd | ||
![]() |
36568356dc | ||
![]() |
1a3681f308 | ||
![]() |
81f6e57413 | ||
![]() |
46c7aa88dd | ||
![]() |
22a2902f71 | ||
![]() |
e9755392ec | ||
![]() |
c28eec436d | ||
![]() |
c46fb1be3a | ||
![]() |
1d0fb7d260 | ||
![]() |
bdd97ac6ac | ||
![]() |
ba3e92e3b8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,8 +1,5 @@
|
||||
/.idea
|
||||
/node_modules
|
||||
/local-cli/lib
|
||||
/react-native-pushy-cli/node_modules
|
||||
/react-native-pushy-cli/lib
|
||||
/android/build
|
||||
/android/obj
|
||||
*.iml
|
||||
|
@@ -4,8 +4,6 @@
|
||||
/.eslintrc
|
||||
/.nvmrc
|
||||
/.travis.yml
|
||||
/local-cli/src
|
||||
/react-native-pushy-cli
|
||||
/Example
|
||||
/android/build
|
||||
|
||||
@@ -46,3 +44,5 @@ npm-debug.log
|
||||
Example
|
||||
yarn.lock
|
||||
android/jni
|
||||
|
||||
domains.json
|
@@ -1,5 +1,4 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
@@ -182,15 +182,15 @@ PODS:
|
||||
- React-cxxreact (= 0.61.4)
|
||||
- React-jsi (= 0.61.4)
|
||||
- React-jsinspector (0.61.4)
|
||||
- react-native-update (5.5.0):
|
||||
- react-native-update (5.5.8):
|
||||
- React
|
||||
- react-native-update/BSDiff (= 5.5.0)
|
||||
- react-native-update/RCTPushy (= 5.5.0)
|
||||
- react-native-update/BSDiff (= 5.5.8)
|
||||
- react-native-update/RCTPushy (= 5.5.8)
|
||||
- SSZipArchive
|
||||
- react-native-update/BSDiff (5.5.0):
|
||||
- react-native-update/BSDiff (5.5.8):
|
||||
- React
|
||||
- SSZipArchive
|
||||
- react-native-update/RCTPushy (5.5.0):
|
||||
- react-native-update/RCTPushy (5.5.8):
|
||||
- React
|
||||
- SSZipArchive
|
||||
- React-RCTActionSheet (0.61.4):
|
||||
@@ -337,7 +337,7 @@ SPEC CHECKSUMS:
|
||||
React-jsi: ca921f4041505f9d5197139b2d09eeb020bb12e8
|
||||
React-jsiexecutor: 8dfb73b987afa9324e4009bdce62a18ce23d983c
|
||||
React-jsinspector: d15478d0a8ada19864aa4d1cc1c697b41b3fa92f
|
||||
react-native-update: 0696134a23c2ad1be899c12b33f9d3521284d458
|
||||
react-native-update: 491cb5902a18f1ebeeb903f1a619df94662fc52a
|
||||
React-RCTActionSheet: 7369b7c85f99b6299491333affd9f01f5a130c22
|
||||
React-RCTAnimation: d07be15b2bd1d06d89417eb0343f98ffd2b099a7
|
||||
React-RCTBlob: 8e0b23d95c9baa98f6b0e127e07666aaafd96c34
|
||||
@@ -353,4 +353,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 244ba888c650d3945bf72a8d01516fb0f1b3b097
|
||||
|
||||
COCOAPODS: 1.8.4
|
||||
COCOAPODS: 1.9.1
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# react-native-update [](http://badge.fury.io/js/react-native-update)
|
||||
|
||||
本组件是面向 React Native 提供热更新功能的组件,请结合[Update 服务](https://update.reactnative.cn/)使用。
|
||||
本组件是面向 React Native 提供热更新功能的组件,详情请访问我们的官方网站 <https://pushy.reactnative.cn>。
|
||||
|
||||
### 快速开始
|
||||
|
||||
请查看[文档](https://reactnativecn.github.io/react-native-pushy)
|
||||
请查看[文档](https://pushy.reactnative.cn/docs/getting-started.html)
|
||||
|
||||
### 优势
|
||||
|
||||
@@ -29,4 +29,4 @@ $ yarn start
|
||||
|
||||
本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
|
||||
|
||||
关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-pushy/issues)或者[中文社区](http://bbs.reactnative.cn/category/7)发帖提问。
|
||||
关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-pushy/issues)发帖提问。
|
||||
|
@@ -96,6 +96,7 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
|
||||
double lastProgressValue=0;
|
||||
while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
|
||||
totalRead += bytesRead;
|
||||
sink.emit();
|
||||
if (UpdateContext.DEBUG) {
|
||||
Log.d("RNUpdate", "Progress " + totalRead + "/" + contentLength);
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Log;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -21,6 +22,7 @@ public class UpdateContext {
|
||||
private Executor executor;
|
||||
|
||||
public static boolean DEBUG = false;
|
||||
private static ReactInstanceManager mReactInstanceManager;
|
||||
|
||||
public UpdateContext(Context context) {
|
||||
this.context = context;
|
||||
@@ -160,6 +162,15 @@ public class UpdateContext {
|
||||
this.clearUp();
|
||||
}
|
||||
|
||||
|
||||
public static void setCustomInstanceManager(ReactInstanceManager instanceManager) {
|
||||
mReactInstanceManager = instanceManager;
|
||||
}
|
||||
|
||||
public ReactInstanceManager getCustomReactInstanceManager() {
|
||||
return mReactInstanceManager;
|
||||
}
|
||||
|
||||
public static String getBundleUrl(Context context) {
|
||||
return new UpdateContext(context.getApplicationContext()).getBundleUrl();
|
||||
}
|
||||
|
@@ -127,7 +127,11 @@ public class UpdateModule extends ReactContextBaseJavaModule{
|
||||
updateContext.switchVersion(hash);
|
||||
Activity activity = getCurrentActivity();
|
||||
Application application = activity.getApplication();
|
||||
ReactInstanceManager instanceManager = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
|
||||
ReactInstanceManager instanceManager = updateContext.getCustomReactInstanceManager();
|
||||
|
||||
if (instanceManager == null) {
|
||||
instanceManager = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
|
||||
}
|
||||
|
||||
try {
|
||||
Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile");
|
||||
|
@@ -1,53 +0,0 @@
|
||||
# react-native-update [](http://badge.fury.io/js/react-native-update)
|
||||
|
||||
本组件是面向 React Native 提供热更新功能的组件,请结合[Update 服务](https://update.reactnative.cn/)使用。
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
注意,在 iOS 上使用热更新有被拒的可能。被拒之后可以按此步骤单独屏蔽 iOS 端(`react-native-update`版本需 >= 5.3.2):
|
||||
</summary>
|
||||
|
||||
1. 如果 RN 版本>=0.60,在项目根目录下编辑或创建 react-native.config.js,添加如下内容
|
||||
|
||||
```js
|
||||
// react-native.config.js
|
||||
module.exports = {
|
||||
dependencies: {
|
||||
'react-native-update': {
|
||||
platforms: {
|
||||
ios: null, // 阻止ios模块自动链接
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. 如果在原生代码端尚未配置,则跳过下面文档中的 ios 端的配置。如果已经配置,则按文档的步骤反向操作(添加的 ios 代码删去)。
|
||||
3. 如果是 0.60 以上版本或使用了 cocoapods,在 ios 目录中再次运行 pod install,确保 Podfile 和 Podfile.lock 中都没有'react-native-update'。如果 RN 版本<0.60,则运行`react-native unlink react-native-update`。
|
||||
4. 在 js 代码里调用 checkUpdate()方法前,判断 Platform.OS,如果是 ios 平台则直接 return 跳过。
|
||||
</details>
|
||||
|
||||
### 优势
|
||||
|
||||
1. 命令行工具&网页双端管理,版本发布过程简单便捷,完全可以集成 CI。
|
||||
2. 基于 bsdiff 算法创建的**超小更新包**,通常版本迭代后在 1-10KB 之间,避免数百 KB 的流量消耗。
|
||||
3. 支持崩溃回滚,安全可靠。
|
||||
4. meta 信息及开放 API,提供更高扩展性。
|
||||
5. 跨越多个版本进行更新时,只需要下载**一个更新包**,不需要逐版本依次更新。
|
||||
|
||||
### 本地开发
|
||||
|
||||
```
|
||||
$ git clone git@github.com:reactnativecn/react-native-pushy.git
|
||||
$ cd react-native-pushy/Example/testHotUpdate
|
||||
$ yarn
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
本地库文件使用 yarn link 链接,因此可直接在源文件中修改,在 testHotUpdate 项目中调试。
|
||||
|
||||
### 关于我们
|
||||
|
||||
本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
|
||||
|
||||
关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-pushy/issues)或者[中文社区](http://bbs.reactnative.cn/category/7)发帖提问。
|
@@ -1,10 +0,0 @@
|
||||
- 安装与快速入门
|
||||
|
||||
- [准备工作](guide.md)
|
||||
- [添加热更新功能](guide2.md)
|
||||
- [发布版本](guide3.md)
|
||||
- [常见问题与高级指南](faq_advance.md)
|
||||
|
||||
- [API接口](api.md)
|
||||
- [命令行工具](cli.md)
|
||||
- [最近更新](changelog.md)
|
66
docs/api.md
66
docs/api.md
@@ -1,66 +0,0 @@
|
||||
# API
|
||||
|
||||
### downloadRootDir
|
||||
|
||||
下载的根目录。你可以使用react-native-fs等第三方组件检查其中的内容。
|
||||
|
||||
### packageVersion
|
||||
|
||||
当前应用包的版本名。
|
||||
|
||||
### currentVersion
|
||||
|
||||
当前版本的Hash号。
|
||||
|
||||
### isFirstTime
|
||||
|
||||
是否更新后的首次启动。当此项为真时,你需要在合适的时候调用`markSuccess()`以确保更新成功。否则应用下一次启动时将会回滚。
|
||||
|
||||
### isRolledBack
|
||||
|
||||
是否刚刚经历了一次回滚。
|
||||
|
||||
### async function checkUpdate(appKey)
|
||||
|
||||
检查更新,返回值有三种情形:
|
||||
|
||||
1. `{expired: true}`:该应用包(原生部分)已过期,需要前往应用市场下载新的版本。
|
||||
```
|
||||
{
|
||||
expired: true,
|
||||
downloadUrl: 'http://appstore/downloadUrl',
|
||||
}
|
||||
```
|
||||
2. `{upToDate: true}`:当前已经更新到最新,无需进行更新。
|
||||
|
||||
3. `{update: true}`:当前有新版本可以更新。info的`name`、`description`字段可
|
||||
以用于提示用户,而`metaInfo`字段则可以根据你的需求自定义其它属性(如是否静默更新、
|
||||
是否强制更新等等)。另外还有几个字段,包含了完整更新包或补丁包的下载地址,
|
||||
```
|
||||
{
|
||||
update: true,
|
||||
name: '1.0.3-rc',
|
||||
hash: 'hash',
|
||||
description: '添加聊天功能\n修复商城页面BUG',
|
||||
metaInfo: '{"silent":true}',
|
||||
updateUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
pdiffUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
diffUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
}
|
||||
```
|
||||
|
||||
### async function downloadUpdate(info)
|
||||
|
||||
下载更新版本。info为checkUpdate函数的返回值,并且仅当`update:true`时实际进行下载。
|
||||
|
||||
### function switchVersion(hash)
|
||||
|
||||
立即重启应用,并加载已经下载完毕的版本。
|
||||
|
||||
### function switchVersionLater(hash)
|
||||
|
||||
在下一次启动应用的时候加载已经下载完毕的版本。
|
||||
|
||||
### function markSuccess()
|
||||
|
||||
在isFirstTime为true时需在应用成功启动后调用此函数,
|
@@ -1,73 +0,0 @@
|
||||
### 最近更新
|
||||
|
||||
## 5.5.3 (2019-12-18)
|
||||
|
||||
1. 忽略hermes的输出避免buffer溢出
|
||||
|
||||
## 5.5.2 (2019-12-06)
|
||||
|
||||
1. 修复使用use_frameworks时无法读取时间戳的问题
|
||||
|
||||
## 5.5.0 (2019-11-24)
|
||||
|
||||
1. 打包时加入时间戳
|
||||
|
||||
## 5.4.0 (2019-11-16)
|
||||
|
||||
1. 修改类名方法名
|
||||
|
||||
## 5.3.2 (2019-10-25)
|
||||
|
||||
1. 可以禁用ios端以避免审核被拒
|
||||
|
||||
## 5.3.0 (2019-09-19)
|
||||
|
||||
1. 替换 apk reader 以避免某些环境读取 apk 版本号报错的问题
|
||||
|
||||
## 5.2.9 (2019-09-18)
|
||||
|
||||
1. 添加 proguard 混淆规则以解决开启混淆后闪退的问题
|
||||
|
||||
## 5.2.8
|
||||
|
||||
1. 解决某些情形下 Android 调用 switchVersion 不能重启的问题
|
||||
|
||||
## 5.2.7
|
||||
|
||||
1. 改进 windows 端打更新包的兼容性(部分 windows 机器上会产生空 ppk 文件)
|
||||
|
||||
## 5.2.4
|
||||
|
||||
1. 支持 RN 0.61 的 hermes(路径变化)
|
||||
2. iOS 端使用第三方的 SSZipArchive 以减少重名冲突
|
||||
|
||||
## 5.2.2
|
||||
|
||||
1. 修复一处导致 iOS 回滚的问题
|
||||
|
||||
## 5.2.1
|
||||
|
||||
1. 检测如果开启了 hermes,则自动编译为 hermes 字节码格式
|
||||
|
||||
## 5.2.0
|
||||
|
||||
1. 添加 typescript 声明
|
||||
2. 支持 cocoapods
|
||||
|
||||
## 5.1.9
|
||||
|
||||
1. 重写 bundle 命令以提升版本兼容性
|
||||
2. 改进命令行的输出样式
|
||||
|
||||
## 5.1.8
|
||||
|
||||
1. 服务器迁移到 https
|
||||
2. android 支持 64 位
|
||||
|
||||
## 5.1.6
|
||||
|
||||
解决 Android 热更新后部分图片丢失问题:
|
||||
|
||||
同一个项目中放置了多个完全相同的文件,在 5.1.0 至 5.1.5 之间的版本中,更新后有时会出现其中的部分无法显示。此问题在 5.1.6 版本修复。
|
||||
|
||||
修复此问题涉及原生部分,需要重新打包。
|
118
docs/cli.md
118
docs/cli.md
@@ -1,118 +0,0 @@
|
||||
# 命令行工具
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
$ npm install -g react-native-update-cli
|
||||
$ npm install --save react-native-update
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
#### pushy bundle
|
||||
|
||||
生成资源包
|
||||
|
||||
* platform: ios|android 对应的平台
|
||||
* entryFile: 入口脚本文件
|
||||
* intermediaDir: 临时文件输出目录
|
||||
* output: 最终ppk文件输出路径
|
||||
* dev: 是否打包开发版本
|
||||
* verbose: 是否展现打包过程的详细信息
|
||||
|
||||
#### pushy diff <origin> <next>
|
||||
|
||||
提供两个ppk文件,生成从origin到next版本的差异更新包。
|
||||
|
||||
* output: diff文件输出路径
|
||||
|
||||
#### pushy diffFromApk <apkFile> <next>
|
||||
|
||||
提供一个apk文件和一个ppk文件,生成从apk文件到next版本的差异更新包。
|
||||
|
||||
如果使用热更新开放平台,你不需要自己执行此命令。
|
||||
|
||||
* output: diff文件输出路径
|
||||
|
||||
#### pushy diffFromIpa <ipaFile> <next>
|
||||
|
||||
提供一个ipa文件和一个ppk文件,生成从ipa文件到next版本的差异更新包。
|
||||
|
||||
如果使用热更新开放平台,你不需要自己执行此命令。
|
||||
|
||||
* output: diff文件输出路径
|
||||
|
||||
#### pushy login [<email>] [<pwd>]
|
||||
|
||||
登录热更新开放平台。你需要先登录才能使用下面的命令。
|
||||
|
||||
#### pushy logout
|
||||
|
||||
登出并清除本地的登录信息
|
||||
|
||||
#### pushy me
|
||||
|
||||
查看自己是否已经登录,以及昵称等信息。
|
||||
|
||||
#### pushy createApp
|
||||
|
||||
创建应用并立刻绑定到当前工程。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
- name: 应用名称
|
||||
- downloadUrl: 应用安装包的下载地址
|
||||
|
||||
#### pushy deleteApp [appId]
|
||||
|
||||
删除已有应用。所有已创建的应用包、热更新版本都会被同时删除。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
|
||||
#### pushy apps
|
||||
|
||||
查看当前已创建的全部应用。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
|
||||
#### pushy selectApp [appId]
|
||||
|
||||
绑定应用到当前工程。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
|
||||
#### pushy uploadIpa <ipaFile>
|
||||
|
||||
上传 ipa 文件到开放平台。
|
||||
|
||||
#### pushy uploadApk <apkFile>
|
||||
|
||||
上传 apk 文件到开放平台。
|
||||
|
||||
#### pushy packages
|
||||
|
||||
查看已经上传的包。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
|
||||
#### pushy publish <ppkFile>
|
||||
|
||||
发布新的更新版本。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
- name: 当前版本的名字(版本号)
|
||||
- description: 当前版本的描述信息,可以对用户进行展示
|
||||
- metaInfo: 当前版本的元信息,可以用来保存一些额外信息
|
||||
|
||||
#### pushy versions
|
||||
|
||||
分页列举可用的版本。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
|
||||
#### pushy update
|
||||
|
||||
为一个包版本绑定一个更新版本。这项操作也可以在网页管理端进行。
|
||||
|
||||
- platform: ios|android 对应的平台
|
||||
- versionId: 要绑定的版本 ID
|
||||
- packageId: 要绑定的包 ID
|
@@ -1,71 +0,0 @@
|
||||
## 常见问题
|
||||
|
||||
#### 报错 NDK not configured.
|
||||
|
||||
你需要下载并安装NDK,然后设置到环境变量`ANDROID_NDK_HOME`中。
|
||||
|
||||
#### 报错 Execution failed for task ':react-native-update:compileReleaseNdk'
|
||||
参看 https://github.com/reactnativecn/react-native-pushy/issues/64#issuecomment-287967742
|
||||
|
||||
#### iOS报错 Unable to execute JS call: __fbBatchedBridge is undefined
|
||||
|
||||
如果直接修改了jsCodeLocation,将不能在iOS模拟器上运行。可以使用真机测试。要在发布之前测试热更新功能,可以用adhoc方式发布测试包并进行测试。adhoc发布的包可以用于uploadIpa和生成差异包。
|
||||
|
||||
#### XCode报错 "_BZ2_bzRead", referenced from 等
|
||||
|
||||
在工程target的Build Phases->Link Binary with Libraries中加入libz.tbd、libbz2.1.0.tbd
|
||||
|
||||
## 高级指南
|
||||
|
||||
#### 过期的版本
|
||||
|
||||
你可以删除掉过期很久的版本。在一段时间后,版本会被真正清理。
|
||||
|
||||
如果有用户还处在已经被清理的版本上,当他发起更新的时候,将不能通过版本差异比对进行更新,即只能进行全量更新。
|
||||
|
||||
#### CI的集成
|
||||
|
||||
在开发环境中,每次bundle都会生成一个不同名字的ppk文件,这不利于持续集成(CI)系统的引入。
|
||||
|
||||
要解决这个问题,你可以使用`--output`参数来指定输出ppk文件的名字和路径,便于进行自动发布。
|
||||
|
||||
#### 版本测试与发布
|
||||
|
||||
我们强烈建议您先发布一个测试包,再发布一个除了版本号以外均完全相同的发布包。
|
||||
在每次往发布包发起热更新之前,先往对应的测试包进行更新操作,基本测试通过之后,可以将发布包更新到完全相同的热更新版本之上。
|
||||
如果在测试包中发现了重大问题,你就可以先进行修复,再次更新测试通过后,再将发布包更新至修复后的版本。
|
||||
这样,可以最大程度的避免用户通过热更新获得一个有问题的版本。
|
||||
|
||||
#### 元信息(Meta Info)的使用
|
||||
|
||||
在发布热更新版本时,或者在网页端,你可以编辑版本的元信息。
|
||||
这是一段在检查更新时可以获得的字符串,你可以在其中按你所想的格式保存一些信息。
|
||||
|
||||
举例来说,可能某个版本包含一些重要的更新内容,所以用户会得到一个不同样式的通知。如何使用元信息,完全取决于您的想象力!
|
||||
|
||||
下面会列举一些实战中更有意义的元信息的使用。
|
||||
|
||||
#### Hot-fix
|
||||
|
||||
有时候我们不小心发布了一个有严重问题的版本,所以需要进行一个紧急的修复,
|
||||
此时我们可能希望之前已经更新到有问题版本的用户进行紧急甚至静默进行更新。
|
||||
|
||||
这时候,我们可以在元信息中包含有问题的版本的列表,而在客户端检查更新时,将从元信息里取到的列表与当前版本(currentVersion)比对,
|
||||
如果匹配成功,我们就进行静默更新,否则则按照一般的更新流程提示用户。
|
||||
|
||||
#### 这个热更新服务收费吗?
|
||||
|
||||
目前我们的热更新服务完全免费,但限制每个账号不超过3个应用;每个应用不超过10个活跃的包和100个活跃的热更新版本;每个应用每个月不超过10000次下载。iOS和Android版本记做不同的应用。
|
||||
|
||||
已经移除的应用、包版本、热更新版本不在统计之列,所以你可以移除测试时产生的和已过期版本来更有效的利用空间。
|
||||
|
||||
我们会在将来推出付费的升级版本,针对用户量较大、版本迭代较快的用户提供扩容方案。如果您有急迫的需求,可以[联系我们](https://reactnative.cn/about.html#content)。
|
||||
|
||||
#### 我是否可以搭建自己的热更新服务?
|
||||
|
||||
你可以单独使用本组件的原生部分(不包括js模块)和命令行工具中的`bundle`、`diff`、`diffFromIpa`、`diffFromApk`四个功能。
|
||||
|
||||
这些功能都不会使用我们的热更新服务,也无需注册或登录账号。但你可能要编写自己的js模块来与不同的热更新服务器通讯。
|
||||
|
||||
如果您有兴趣使用我们的成果,搭建私有云服务,可以[联系我们](https://reactnative.cn/about.html#content)。
|
||||
|
228
docs/guide.md
228
docs/guide.md
@@ -1,228 +0,0 @@
|
||||
# 快速入门-准备工作
|
||||
|
||||
首先你应该有一个基于React Native开发的应用,我们把具有package.json的目录叫做你的"应用根目录"。
|
||||
|
||||
如果你还没有初始化应用,请参阅[开始使用React Native](https://reactnative.cn/docs/getting-started.html#content)。
|
||||
|
||||
所以我们也假设你已经拥有了开发React Native应用的一切环境,包括`Node.js`、`npm`、`XCode`、`Android SDK`等等。
|
||||
|
||||
## 安装
|
||||
|
||||
在你的项目根目录下运行以下命令:
|
||||
|
||||
```bash
|
||||
npm i -g react-native-update-cli
|
||||
npm i react-native-update
|
||||
```
|
||||
|
||||
> 如果下载极慢或者显示网络失败,请设置使用淘宝镜像`npx nrm use taobao`
|
||||
|
||||
如果你的RN版本 >= 0.60,请在iOS目录下执行:
|
||||
|
||||
```bash
|
||||
pod install
|
||||
```
|
||||
|
||||
如果你的RN版本 < 0.60,那么还需要[手动link](#一手动link)
|
||||
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
如果你的RN版本比较老(< 0.46),请点击这里的注意事项
|
||||
</summary>
|
||||
|
||||
如果你的RN版本比较老,请按下面表格尝试老一些的版本(但这些版本我们已不再维护,不能保证可以使用):
|
||||
|
||||
| React Native版本 | react-native-update版本 |
|
||||
| ---------------- | ----------------------- |
|
||||
| 0.26及以下 | 1.0.x |
|
||||
| 0.27 - 0.28 | 2.x |
|
||||
| 0.29 - 0.33 | 3.x |
|
||||
| 0.34 - 0.45 | 4.x |
|
||||
|
||||
安装命令示例:
|
||||
```
|
||||
npm i react-native-update@4.x
|
||||
```
|
||||
|
||||
如果RN的版本是0.45及以下,你还必须安装[Android NDK](http://androiddevtools.cn),版本最好选用r10e,并设置环境变量`ANDROID_NDK_HOME`,指向你的NDK根目录(例如`/Users/tdzl2003/Downloads/android-ndk-r10e`)。
|
||||
</details>
|
||||
|
||||
|
||||
请记得一定要重新编译(react-native run-ios或run-android命令编译,或在Xcode/Android Studio中重新编译)。
|
||||
|
||||
|
||||
## 手动link
|
||||
|
||||
如果RN版本 >= 0.60则可以跳过此步骤
|
||||
|
||||
### iOS
|
||||
|
||||
<details>
|
||||
<summary>RN < 0.60且使用CocoaPods(推荐)</summary>
|
||||
|
||||
1. 在ios/Podfile中添加
|
||||
```
|
||||
pod 'react-native-update', path: '../node_modules/react-native-update'
|
||||
```
|
||||
2. 在项目的ios目录下运行`pod install`
|
||||
3. 重新编译
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>RN < 0.60且不使用CocoaPods</summary>
|
||||
|
||||
1. 在XCode中的Project Navigator里,右键点击`Libraries` ➜ `Add Files to [你的工程名]`
|
||||
2. 进入`node_modules` ➜ `react-native-update` ➜ `ios 并选中 `RCTPushy.xcodeproj`
|
||||
3. 在XCode中的project navigator里,选中你的工程,在 `Build Phases` ➜ `Link Binary With Libraries` 中添加 `libRCTPushy.a`
|
||||
4. 继续在`Build Settings`里搜索`Header Search Path`,添加$(SRCROOT)/../node_modules/react-native-update/ios
|
||||
5. 在`Build Phases`添加一个`New Run Script Phase`运行脚本,内容如下
|
||||
```
|
||||
#!/bin/bash
|
||||
set -x
|
||||
DEST="../node_modules/react-native-update/ios/"
|
||||
date +%s > "$DEST/pushy_build_time.txt"
|
||||
```
|
||||
编译的时候就会在`../node_modules/react-native-update/ios/`文件夹下面生成一个`pushy_build_time.txt`文件。
|
||||
然后在`Copy Bundle Resources`里把生成的`pushy_build_time.txt`文件添加进去。
|
||||
|
||||
6. 重新编译
|
||||
|
||||
</details>
|
||||
|
||||
### Android
|
||||
|
||||
|
||||
<details>
|
||||
<summary>RN < 0.60</summary>
|
||||
1. 在`android/settings.gradle`中添加如下代码:
|
||||
```
|
||||
include ':react-native-update'
|
||||
project(':react-native-update').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-update/android')
|
||||
```
|
||||
|
||||
2. 在`android/app/build.gradle`的 dependencies 部分增加如下代码:
|
||||
```
|
||||
implementation project(':react-native-update')
|
||||
```
|
||||
|
||||
3. 打开`android/app/src/main/java/[...]/MainApplication.java`,
|
||||
- 在文件开头增加 `import cn.reactnative.modules.update.UpdatePackage;`
|
||||
- 在`getPackages()` 方法中增加 `new UpdatePackage()`(注意上一行可能要增加一个逗号)
|
||||
</details>
|
||||
|
||||
## 配置Bundle URL
|
||||
|
||||
注意此步骤无论任何版本,目前都需要手动配置。
|
||||
|
||||
### iOS
|
||||
|
||||
1. (RN >= 0.60或者使用CocoaPods集成此步可跳过)在工程target的Build Phases->Link Binary with Libraries中加入`libz.tbd`、`libbz2.1.0.tbd`
|
||||
|
||||
2. 在你的AppDelegate.m文件中增加如下代码:
|
||||
|
||||
```objective-c
|
||||
// ... 其它代码
|
||||
|
||||
#import "RCTPushy.h"
|
||||
|
||||
// 如果RN版本 >= 0.59,修改sourceURLForBridge
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
#if DEBUG
|
||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
|
||||
#else
|
||||
// 非DEBUG情况下替换为热更新bundle
|
||||
return [RCTPushy bundleURL];
|
||||
#endif
|
||||
}
|
||||
|
||||
// 如果RN版本 < 0.59,修改didFinishLaunchingWithOptions
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
#if DEBUG
|
||||
// 原来的jsCodeLocation保留在这里
|
||||
jsCodeLocation = ..........
|
||||
#else
|
||||
// 非DEBUG情况下替换为热更新bundle
|
||||
jsCodeLocation = [RCTPushy bundleURL];
|
||||
#endif
|
||||
// ... 其它代码
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
在MainApplication中增加如下代码:
|
||||
|
||||
```java
|
||||
// ... 其它代码
|
||||
|
||||
// 请注意不要少了这句import
|
||||
import cn.reactnative.modules.update.UpdateContext;
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||
@Override
|
||||
protected String getJSBundleFile() {
|
||||
return UpdateContext.getBundleUrl(MainApplication.this);
|
||||
}
|
||||
// ... 其它代码
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 登录与创建应用
|
||||
|
||||
首先请在<https://update.reactnative.cn>注册帐号,然后在你的项目根目录下运行以下命令:
|
||||
|
||||
```bash
|
||||
$ pushy login
|
||||
email: <输入你的注册邮箱>
|
||||
password: <输入你的密码>
|
||||
```
|
||||
|
||||
这会在项目文件夹下创建一个`.update`文件,注意不要把这个文件上传到Git等CVS系统上。你可以在`.gitignore`末尾增加一行`.update`来忽略这个文件。
|
||||
|
||||
登录之后可以创建应用。注意iOS平台和安卓平台需要分别创建:
|
||||
|
||||
```bash
|
||||
$ pushy createApp --platform ios
|
||||
App Name: <输入应用名字>
|
||||
$ pushy createApp --platform android
|
||||
App Name: <输入应用名字>
|
||||
```
|
||||
|
||||
> 两次输入的名字可以相同,这没有关系。
|
||||
|
||||
如果你已经在网页端或者其它地方创建过应用,也可以直接选择应用:
|
||||
|
||||
```bash
|
||||
$ pushy selectApp --platform ios
|
||||
1) 鱼多多(ios)
|
||||
2) 招财旺(ios)
|
||||
|
||||
Total 2 ios apps
|
||||
Enter appId: <输入应用前面的编号>
|
||||
```
|
||||
|
||||
选择或者创建过应用后,你将可以在文件夹下看到`update.json`文件,其内容类似如下形式:
|
||||
|
||||
```bash
|
||||
{
|
||||
"ios": {
|
||||
"appId": 1,
|
||||
"appKey": "<一串随机字符串>"
|
||||
},
|
||||
"android": {
|
||||
"appId": 2,
|
||||
"appKey": "<一串随机字符串>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你可以安全的把`update.json`上传到Git等CVS系统上,与你的团队共享这个文件,它不包含任何敏感信息。当然,他们在使用任何功能之前,都必须首先输入`pushy login`进行登录。
|
||||
|
||||
至此应用的创建/选择就已经成功了。下一步,你需要给代码添加相应的功能,请参阅[添加热更新功能](guide2.md)
|
177
docs/guide2.md
177
docs/guide2.md
@@ -1,177 +0,0 @@
|
||||
# 快速入门-添加热更新功能
|
||||
|
||||
## 获取appKey
|
||||
|
||||
检查更新时必须提供你的`appKey`,这个值保存在`update.json`中,并且根据平台不同而不同。你可以用如下的代码获取:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import _updateConfig from './update.json';
|
||||
const {appKey} = _updateConfig[Platform.OS];
|
||||
```
|
||||
|
||||
如果你不使用pushy命令行,你也可以从网页端查看到两个应用appKey,并根据平台的不同来选择。
|
||||
|
||||
## 检查更新、下载更新
|
||||
|
||||
异步函数checkUpdate可以检查当前版本是否需要更新:
|
||||
|
||||
```javascript
|
||||
const info = await checkUpdate(appKey)
|
||||
```
|
||||
|
||||
返回的info有三种情况:
|
||||
|
||||
1. `{expired: true}`:该应用包(原生部分)已过期,需要前往应用市场下载新的版本。
|
||||
|
||||
2. `{upToDate: true}`:当前已经更新到最新,无需进行更新。
|
||||
|
||||
3. `{update: true}`:当前有新版本可以更新。info的`name`、`description`字段可
|
||||
以用于提示用户,而`metaInfo`字段则可以根据你的需求自定义其它属性(如是否静默更新、
|
||||
是否强制更新等等)。另外还有几个字段,包含了完整更新包或补丁包的下载地址,
|
||||
react-native-update会首先尝试耗费流量更少的更新方式。将info对象传递给downloadUpdate作为参数即可。
|
||||
|
||||
## 切换版本
|
||||
|
||||
downloadUpdate的返回值是一个hash字符串,它是当前版本的唯一标识。
|
||||
|
||||
你可以使用`switchVersion`函数立即切换版本(此时应用会立即重新加载),或者选择调用
|
||||
`switchVersionLater`,让应用在下一次启动的时候再加载新的版本。
|
||||
|
||||
## 首次启动、回滚
|
||||
|
||||
在每次更新完毕后的首次启动时,`isFirstTime`常量会为`true`。
|
||||
你必须在应用退出前合适的任何时机,调用`markSuccess`,否则应用下一次启动的时候将会进行回滚操作。
|
||||
这一机制称作“反触发”,这样当你应用启动初期即遭遇问题的时候,也能在下一次启动时恢复运作。
|
||||
|
||||
你可以通过`isFirstTime`来获知这是当前版本的首次启动,也可以通过`isRolledBack`来获知应用刚刚经历了一次回滚操作。
|
||||
你可以在此时给予用户合理的提示。
|
||||
|
||||
## 完整的示例
|
||||
|
||||
```javascript
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
AppRegistry,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Text,
|
||||
View,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
|
||||
import {
|
||||
isFirstTime,
|
||||
isRolledBack,
|
||||
packageVersion,
|
||||
currentVersion,
|
||||
checkUpdate,
|
||||
downloadUpdate,
|
||||
switchVersion,
|
||||
switchVersionLater,
|
||||
markSuccess,
|
||||
} from 'react-native-update';
|
||||
|
||||
import _updateConfig from './update.json';
|
||||
const {appKey} = _updateConfig[Platform.OS];
|
||||
|
||||
class MyProject extends Component {
|
||||
componentDidMount(){
|
||||
if (isFirstTime) {
|
||||
Alert.alert('提示', '这是当前版本第一次启动,是否要模拟启动失败?失败将回滚到上一版本', [
|
||||
{text: '是', onPress: ()=>{throw new Error('模拟启动失败,请重启应用')}},
|
||||
{text: '否', onPress: ()=>{markSuccess()}},
|
||||
]);
|
||||
} else if (isRolledBack) {
|
||||
Alert.alert('提示', '刚刚更新失败了,版本被回滚.');
|
||||
}
|
||||
}
|
||||
doUpdate = async (info) => {
|
||||
try {
|
||||
const hash = await downloadUpdate(info);
|
||||
Alert.alert('提示', '下载完毕,是否重启应用?', [
|
||||
{text: '是', onPress: ()=>{switchVersion(hash);}},
|
||||
{text: '否',},
|
||||
{text: '下次启动时', onPress: ()=>{switchVersionLater(hash);}},
|
||||
]);
|
||||
} catch(err) {
|
||||
Alert.alert('提示', '更新失败.');
|
||||
}
|
||||
};
|
||||
checkUpdate = async () => {
|
||||
if (__DEV__) {
|
||||
// 开发模式不支持热更新,跳过检查
|
||||
return;
|
||||
}
|
||||
let info;
|
||||
try {
|
||||
info = await checkUpdate(appKey);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
return;
|
||||
}
|
||||
if (info.expired) {
|
||||
Alert.alert('提示', '您的应用版本已更新,请前往应用商店下载新的版本', [
|
||||
{text: '确定', onPress: ()=>{info.downloadUrl && Linking.openURL(info.downloadUrl)}},
|
||||
]);
|
||||
} else if (info.upToDate) {
|
||||
Alert.alert('提示', '您的应用版本已是最新.');
|
||||
} else {
|
||||
Alert.alert('提示', '检查到新的版本'+info.name+',是否下载?\n'+ info.description, [
|
||||
{text: '是', onPress: ()=>{this.doUpdate(info)}},
|
||||
{text: '否',},
|
||||
]);
|
||||
}
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.welcome}>
|
||||
欢迎使用热更新服务
|
||||
</Text>
|
||||
<Text style={styles.instructions}>
|
||||
这是版本一 {'\n'}
|
||||
当前包版本号: {packageVersion}{'\n'}
|
||||
当前版本Hash: {currentVersion||'(空)'}{'\n'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={this.checkUpdate}>
|
||||
<Text style={styles.instructions}>
|
||||
点击这里检查更新
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5FCFF',
|
||||
},
|
||||
welcome: {
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
margin: 10,
|
||||
},
|
||||
instructions: {
|
||||
textAlign: 'center',
|
||||
color: '#333333',
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
AppRegistry.registerComponent('MyProject', () => MyProject);
|
||||
```
|
||||
|
||||
现在,你的应用已经可以通过update服务检查版本并进行更新了。下一步,你可以开始尝试发布应用包和版本,请参阅[发布应用](guide3.md)
|
@@ -1,80 +0,0 @@
|
||||
# 快速入门-发布应用
|
||||
|
||||
现在你的应用已经具备了检测更新的功能,下面我们来尝试发布并更新它。
|
||||
|
||||
> **注意**,从update上传发布版本到发布版本正式上线期间,不要修改任何脚本和资源,这会影响update
|
||||
获取本地代码,从而导致版本不能更新。如果在发布之前修改了脚本或资源,请在网页端删除之前上传的版本并重新上传。
|
||||
|
||||
## 发布iOS应用
|
||||
|
||||
首先参考[文档-在设备上运行](https://reactnative.cn/docs/running-on-device-ios.html#content),
|
||||
确定你正在使用离线包。然后点击菜单。
|
||||
|
||||
按照正常的发布流程打包`.ipa`文件(Xcode中运行设备选真机或Generic iOS Device,然后菜单中选择Product-Archive),然后运行如下命令
|
||||
|
||||
```bash
|
||||
$ pushy uploadIpa <your-package.ipa>
|
||||
```
|
||||
|
||||
即可上传ipa以供后续版本比对之用。
|
||||
|
||||
随后你可以选择往AppStore发布这个版本,也可以先通过Test flight等方法进行测试。
|
||||
|
||||
## 发布安卓应用
|
||||
|
||||
首先参考[文档-生成已签名的APK](https://reactnative.cn/docs/signed-apk-android.html#content)设置签名,
|
||||
然后在android文件夹下运行`./gradlew assembleRelease`或`./gradlew aR`,你就可以在`android/app/build/outputs/apk/release/app-release.apk`中找到你的应用包。
|
||||
|
||||
然后运行如下命令
|
||||
|
||||
```bash
|
||||
$ pushy uploadApk android/app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
即可上传apk以供后续版本比对之用。
|
||||
|
||||
随后你可以选择往应用市场发布这个版本,也可以先往设备上直接安装这个apk文件以进行测试。
|
||||
|
||||
## 发布新的热更新版本
|
||||
|
||||
你可以尝试修改一行代码(譬如将版本一修改为版本二),然后生成新的热更新版本。
|
||||
|
||||
```bash
|
||||
$ pushy bundle --platform <ios|android>
|
||||
Bundling with React Native version: 0.22.2
|
||||
<各种进度输出>
|
||||
Bundled saved to: build/output/android.1459850548545.ppk
|
||||
Would you like to publish it?(Y/N)
|
||||
```
|
||||
|
||||
如果想要立即发布,此时输入Y。当然,你也可以在将来使用`pushy publish --platform <ios|android> <ppkFile>`来发布版本。
|
||||
|
||||
```
|
||||
Uploading [========================================================] 100% 0.0s
|
||||
Enter version name: <输入版本名字,如1.0.0-rc>
|
||||
Enter description: <输入版本描述>
|
||||
Enter meta info: {"ok":1}
|
||||
Ok.
|
||||
Would you like to bind packages to this version?(Y/N)
|
||||
```
|
||||
|
||||
此时版本已经提交到update服务,但用户暂时看不到此更新,你需要先将特定的包版本绑定到此热更新版本上。
|
||||
|
||||
此时输入Y立即绑定,你也可以在将来使用`pushy update --platform <ios|android>`来使得对应包版本的用户更新。
|
||||
除此以外,你还可以在网页端操作,简单的将对应的包版本拖到此版本下即可。
|
||||
|
||||
```
|
||||
Offset 0
|
||||
1) FvXnROJ1 1.0.1 (no package)
|
||||
2) FiWYm9lB 1.0 [1.0]
|
||||
Enter versionId or page Up/page Down/Begin(U/D/B) <输入序号,U/D翻页,B回到开始,序号就是上面列表中)前面的数字>
|
||||
|
||||
1) 1.0(normal) - 3 FiWYm9lB (未命名)
|
||||
|
||||
Total 1 packages.
|
||||
Enter packageId: <输入包版本序号,序号就是上面列表中)前面的数字>
|
||||
```
|
||||
|
||||
版本绑定完毕后,客户端就应当可以检查到更新并进行更新了。
|
||||
|
||||
恭喜你,至此为止,你已经完成了植入代码热更新的全部工作。接下来,你可以查阅[常见问题与高级指南](faq_advance.md)了解更多深入的知识,尤其是在实际项目中的运用技巧。
|
@@ -1,25 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>react-native-update - react-native hot update</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="react-native hot update">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'react-native-update',
|
||||
repo: 'https://github.com/reactnativecn/react-native-pushy',
|
||||
formatUpdated: '{MM}/{DD} {HH}:{mm}',
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 2,
|
||||
auto2top: true
|
||||
}
|
||||
</script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
</body>
|
||||
</html>
|
1
domains.json
Normal file
1
domains.json
Normal file
@@ -0,0 +1 @@
|
||||
["update.react-native.cn", "update.reactnative.cn"]
|
@@ -345,10 +345,10 @@ RCT_EXPORT_METHOD(markSuccess)
|
||||
break;
|
||||
case PushyTypePatchFromPpk:
|
||||
{
|
||||
NSString *lastVertionDir = [dir stringByAppendingPathComponent:originHashName];
|
||||
NSString *lastVersionDir = [dir stringByAppendingPathComponent:originHashName];
|
||||
|
||||
NSString *sourceOrigin = lastVertionDir;
|
||||
NSString *bundleOrigin = [lastVertionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
||||
NSString *sourceOrigin = lastVersionDir;
|
||||
NSString *bundleOrigin = [lastVersionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
||||
[self patch:hashName fromBundle:bundleOrigin source:sourceOrigin callback:callback];
|
||||
}
|
||||
break;
|
||||
@@ -424,7 +424,7 @@ RCT_EXPORT_METHOD(markSuccess)
|
||||
case PushyTypeFullDownload:
|
||||
return @".ppk";
|
||||
case PushyTypePatchFromPackage:
|
||||
return @".apk.patch";
|
||||
return @".ipa.patch";
|
||||
case PushyTypePatchFromPpk:
|
||||
return @".ppk.patch";
|
||||
default:
|
||||
|
@@ -99,16 +99,9 @@ completionHandler:(void (^)(NSError *error))completionHandler
|
||||
|
||||
// merge old files
|
||||
if (deletes!= nil) {
|
||||
NSError *error = nil;
|
||||
NSString *srcDir = [fromDir stringByAppendingPathComponent:@"assets"];
|
||||
NSString *desDir = [toDir stringByAppendingPathComponent:@"assets"];
|
||||
[self _mergeContentsOfPath:srcDir intoPath:desDir deletes:deletes error:&error];
|
||||
if (error) {
|
||||
if (completionHandler) {
|
||||
completionHandler(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
[self _mergeContentsOfPath:srcDir intoPath:desDir deletes:deletes];
|
||||
}
|
||||
|
||||
// copy files
|
||||
@@ -126,10 +119,11 @@ completionHandler:(void (^)(NSError *error))completionHandler
|
||||
NSError *error = nil;
|
||||
[fm copyItemAtPath:fromPath toPath:toPath error:&error];
|
||||
if (error) {
|
||||
if (completionHandler) {
|
||||
completionHandler(error);
|
||||
}
|
||||
return;
|
||||
NSLog(@"Pushy copy error: %@", error.localizedDescription);
|
||||
// if (completionHandler) {
|
||||
// completionHandler(error);
|
||||
// }
|
||||
// return;
|
||||
}
|
||||
}
|
||||
if (completionHandler) {
|
||||
@@ -150,7 +144,7 @@ completionHandler:(void (^)(NSError *error))completionHandler
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_mergeContentsOfPath:(NSString *)srcDir intoPath:(NSString *)dstDir deletes:(NSDictionary *)deletes error:(NSError**)err
|
||||
- (void)_mergeContentsOfPath:(NSString *)srcDir intoPath:(NSString *)dstDir deletes:(NSDictionary *)deletes
|
||||
{
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSDirectoryEnumerator *srcDirEnum = [fm enumeratorAtPath:srcDir];
|
||||
@@ -160,6 +154,8 @@ completionHandler:(void (^)(NSError *error))completionHandler
|
||||
NSString *srcFullPath = [srcDir stringByAppendingPathComponent:subPath];
|
||||
NSString *potentialDstPath = [dstDir stringByAppendingPathComponent:subPath];
|
||||
|
||||
NSError *error = nil;
|
||||
|
||||
BOOL inDeletes = NO;
|
||||
if (deletes) {
|
||||
NSString *path = [@"assets" stringByAppendingPathComponent:subPath];
|
||||
@@ -171,18 +167,19 @@ completionHandler:(void (^)(NSError *error))completionHandler
|
||||
BOOL isDirectory = ([fm fileExistsAtPath:srcFullPath isDirectory:&isDirectory] && isDirectory);
|
||||
if (isDirectory) {
|
||||
if (![fm fileExistsAtPath:potentialDstPath isDirectory:nil]) {
|
||||
[fm createDirectoryAtPath:potentialDstPath withIntermediateDirectories:YES attributes:nil error:err];
|
||||
if (err && *err) {
|
||||
return;
|
||||
[fm createDirectoryAtPath:potentialDstPath withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
if (error) {
|
||||
NSLog(@"Pushy merge error: %@", error.localizedDescription);
|
||||
// return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
if (![fm fileExistsAtPath:potentialDstPath]) {
|
||||
[fm copyItemAtPath:srcFullPath toPath:potentialDstPath error:err];
|
||||
if (err && *err) {
|
||||
return;
|
||||
[fm copyItemAtPath:srcFullPath toPath:potentialDstPath error:&error];
|
||||
if (error) {
|
||||
NSLog(@"Pushy merge error: %@", error.localizedDescription);
|
||||
// return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
lib/getHost.js
Normal file
57
lib/getHost.js
Normal file
@@ -0,0 +1,57 @@
|
||||
let availableDomain = 'update.react-native.cn';
|
||||
|
||||
function ping(domain, rejectImmediate) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = e => {
|
||||
if (xhr.readyState !== 4) {
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
resolve(domain);
|
||||
} else {
|
||||
rejectImmediate ? reject() : setTimeout(reject, 5000);
|
||||
}
|
||||
};
|
||||
xhr.open('HEAD', `https://${domain}`);
|
||||
xhr.send();
|
||||
xhr.timeout = 5000;
|
||||
xhr.ontimeout = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function logger(...args) {
|
||||
// console.warn('pushy', ...args);
|
||||
}
|
||||
|
||||
export async function tryBackupDomains() {
|
||||
try {
|
||||
await ping(availableDomain, true);
|
||||
logger('main domain ok');
|
||||
return;
|
||||
} catch (e) {
|
||||
logger('main domain failed');
|
||||
}
|
||||
let backupDomains = [];
|
||||
try {
|
||||
const resp = await fetch(
|
||||
'https://cdn.jsdelivr.net/gh/reactnativecn/react-native-pushy@master/domains.json',
|
||||
);
|
||||
backupDomains = await resp.json();
|
||||
logger('get remote domains:', backupDomains);
|
||||
} catch (e) {
|
||||
logger('get remote domains failed');
|
||||
return;
|
||||
}
|
||||
const fastestDomain = await Promise.race(backupDomains.map(ping));
|
||||
if (typeof fastestDomain === 'string') {
|
||||
logger(`pick domain: ${fastestDomain}`);
|
||||
availableDomain = fastestDomain;
|
||||
} else {
|
||||
logger('all remote domains failed');
|
||||
}
|
||||
}
|
||||
|
||||
export default function getHost() {
|
||||
return `https://${availableDomain}/api`;
|
||||
}
|
1
lib/index.d.ts
vendored
1
lib/index.d.ts
vendored
@@ -19,7 +19,6 @@ export interface UpdateAvailableResult {
|
||||
hash: string;
|
||||
description: string;
|
||||
metaInfo: string;
|
||||
updateUrl: string;
|
||||
pdiffUrl: string;
|
||||
diffUrl: string;
|
||||
}
|
||||
|
27
lib/index.js
27
lib/index.js
@@ -1,7 +1,11 @@
|
||||
import getHost, { tryBackupDomains } from './getHost';
|
||||
import { NativeAppEventEmitter, NativeModules } from 'react-native';
|
||||
const Pushy = NativeModules.Pushy || {};
|
||||
|
||||
const host = 'https://update.reactnative.cn/api';
|
||||
let Pushy = NativeModules.Pushy;
|
||||
|
||||
if (!Pushy) {
|
||||
throw new Error('react-native-update模块无法加载,请对照安装文档检查配置。');
|
||||
}
|
||||
|
||||
export const downloadRootDir = Pushy.downloadRootDir;
|
||||
export const packageVersion = Pushy.packageVersion;
|
||||
@@ -28,7 +32,6 @@ There is available update:
|
||||
hash: 'hash',
|
||||
description: '添加聊天功能\n修复商城页面BUG',
|
||||
metaInfo: '{"silent":true}',
|
||||
updateUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
pdiffUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
diffUrl: 'http://update-packages.reactnative.cn/hash',
|
||||
}
|
||||
@@ -40,9 +43,11 @@ function assertRelease() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpdate(APPKEY) {
|
||||
export async function checkUpdate(APPKEY, isRetry) {
|
||||
assertRelease();
|
||||
const resp = await fetch(`${host}/checkUpdate/${APPKEY}`, {
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${getHost()}/checkUpdate/${APPKEY}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -54,6 +59,13 @@ export async function checkUpdate(APPKEY) {
|
||||
buildTime,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
if (isRetry) {
|
||||
throw new Error('Could not connect to pushy server');
|
||||
}
|
||||
await tryBackupDomains();
|
||||
return checkUpdate(APPKEY, true);
|
||||
}
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error((await resp.json()).message);
|
||||
@@ -103,6 +115,11 @@ export function markSuccess() {
|
||||
Pushy.markSuccess();
|
||||
}
|
||||
|
||||
// function report(action) {
|
||||
// // ${project}.${host}/logstores/${logstore}/track?APIVersion=0.6.0&key1=val1
|
||||
// fetch(`${logUrl}&action=${action}`);
|
||||
// }
|
||||
|
||||
NativeAppEventEmitter.addListener('RCTPushyDownloadProgress', params => {});
|
||||
|
||||
NativeAppEventEmitter.addListener('RCTPushyUnzipProgress', params => {});
|
||||
|
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"syntax-object-rest-spread",
|
||||
"syntax-async-functions",
|
||||
"transform-es2015-arrow-functions",
|
||||
"transform-async-to-generator",
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-es2015-destructuring",
|
||||
"transform-es2015-spread",
|
||||
"transform-object-rest-spread",
|
||||
"transform-es2015-parameters",
|
||||
"transform-strict-mode"
|
||||
]
|
||||
}
|
@@ -1,162 +0,0 @@
|
||||
{
|
||||
"useCommand": true,
|
||||
"defaultCommand": "help",
|
||||
"commands": {
|
||||
"help": {
|
||||
},
|
||||
|
||||
"login":{
|
||||
},
|
||||
"logout": {
|
||||
},
|
||||
"me": {
|
||||
},
|
||||
|
||||
"createApp": {
|
||||
"options": {
|
||||
"name": {
|
||||
"hasValue": true
|
||||
},
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteApp": {
|
||||
},
|
||||
"selectApp": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"uploadIpa": {
|
||||
},
|
||||
"uploadApk": {
|
||||
},
|
||||
"packages": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"publish": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
},
|
||||
"name": {
|
||||
"hasValue": true
|
||||
},
|
||||
"description": {
|
||||
"hasValue": true
|
||||
},
|
||||
"metaInfo": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"update": {
|
||||
"options": {
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
},
|
||||
"versionId": {
|
||||
"hasValue": true
|
||||
},
|
||||
"packageId": {
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"build": {
|
||||
"description": "Bundle javascript and copy assets."
|
||||
},
|
||||
"bundle": {
|
||||
"description": "Bundle javascript code only.",
|
||||
"options": {
|
||||
"dev": {
|
||||
"default": "false",
|
||||
"hasValue": true
|
||||
},
|
||||
"platform": {
|
||||
"hasValue": true
|
||||
},
|
||||
"bundleName":{
|
||||
"default": "index.bundlejs",
|
||||
"hasValue": true
|
||||
},
|
||||
"entryFile": {
|
||||
"default": "index.js",
|
||||
"hasValue": true
|
||||
},
|
||||
"intermediaDir": {
|
||||
"default": "build/intermedia/${platform}",
|
||||
"hasValue": true
|
||||
},
|
||||
"output": {
|
||||
"default": "build/output/${platform}.${time}.ppk",
|
||||
"hasValue": true
|
||||
},
|
||||
"verbose": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"description": "Push builded file to server."
|
||||
},
|
||||
"diff": {
|
||||
"description": "Create diff patch",
|
||||
"options": {
|
||||
"output": {
|
||||
"default": "build/output/diff",
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"diffFromApk": {
|
||||
"description": "Create diff patch from a Android package(.apk)",
|
||||
"options": {
|
||||
"output": {
|
||||
"default": "build/output/diff-${time}.apk-patch",
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"diffFromIpa": {
|
||||
"description": "Create diff patch from a iOS package(.ipa)",
|
||||
"options": {
|
||||
"output": {
|
||||
"default": "build/output/diff-${time}.ipa-patch",
|
||||
"hasValue": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalOptions":{
|
||||
"no-interactive": {
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/22/16.
|
||||
*/
|
||||
module.exports = require('./lib');
|
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
const fetch = require('isomorphic-fetch');
|
||||
let host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn/api';
|
||||
const fs = require('fs-extra');
|
||||
import request from 'request';
|
||||
import ProgressBar from 'progress';
|
||||
|
||||
let session = undefined;
|
||||
let savedSession = undefined;
|
||||
|
||||
exports.loadSession = async function() {
|
||||
if (fs.existsSync('.update')) {
|
||||
try {
|
||||
exports.replaceSession(JSON.parse(fs.readFileSync('.update', 'utf8')));
|
||||
savedSession = session;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse file `.update`. Try to remove it manually.');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.getSession = function() {
|
||||
return session;
|
||||
};
|
||||
|
||||
exports.replaceSession = function(newSession) {
|
||||
session = newSession;
|
||||
};
|
||||
|
||||
exports.saveSession = function() {
|
||||
// Only save on change.
|
||||
if (session !== savedSession) {
|
||||
const current = session;
|
||||
const data = JSON.stringify(current, null, 4);
|
||||
fs.writeFileSync('.update', data, 'utf8');
|
||||
savedSession = current;
|
||||
}
|
||||
};
|
||||
|
||||
exports.closeSession = function() {
|
||||
if (fs.existsSync('.update')) {
|
||||
fs.unlinkSync('.update');
|
||||
savedSession = undefined;
|
||||
}
|
||||
session = undefined;
|
||||
host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn';
|
||||
};
|
||||
|
||||
async function query(url, options) {
|
||||
const resp = await fetch(url, options);
|
||||
const json = await resp.json();
|
||||
if (resp.status !== 200) {
|
||||
throw Object.assign(new Error(json.message || json.error), { status: resp.status });
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function queryWithoutBody(method) {
|
||||
return function(api) {
|
||||
return query(host + api, {
|
||||
method,
|
||||
headers: {
|
||||
'X-AccessToken': session ? session.token : '',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function queryWithBody(method) {
|
||||
return function(api, body) {
|
||||
return query(host + api, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-AccessToken': session ? session.token : '',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
exports.get = queryWithoutBody('GET');
|
||||
exports.post = queryWithBody('POST');
|
||||
exports.put = queryWithBody('PUT');
|
||||
exports.doDelete = queryWithBody('DELETE');
|
||||
|
||||
async function uploadFile(fn) {
|
||||
const { url, fieldName, formData } = await exports.post('/upload', {});
|
||||
let realUrl = url;
|
||||
|
||||
if (!/^https?\:\/\//.test(url)) {
|
||||
realUrl = host + url;
|
||||
}
|
||||
|
||||
const fileSize = fs.statSync(fn).size;
|
||||
|
||||
const bar = new ProgressBar(' Uploading [:bar] :percent :etas', {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
total: fileSize,
|
||||
});
|
||||
|
||||
const info = await new Promise((resolve, reject) => {
|
||||
formData.file = fs.createReadStream(fn);
|
||||
|
||||
formData.file.on('data', function(data) {
|
||||
bar.tick(data.length);
|
||||
});
|
||||
request.post(
|
||||
realUrl,
|
||||
{
|
||||
formData,
|
||||
},
|
||||
(err, resp, body) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
if (resp.statusCode > 299) {
|
||||
return reject(Object.assign(new Error(body), { status: resp.statusCode }));
|
||||
}
|
||||
resolve(JSON.parse(body));
|
||||
},
|
||||
);
|
||||
});
|
||||
return info;
|
||||
}
|
||||
|
||||
exports.uploadFile = uploadFile;
|
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
import {question} from './utils';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
const {
|
||||
post,
|
||||
get,
|
||||
doDelete,
|
||||
} = require('./api');
|
||||
|
||||
const validPlatforms = {
|
||||
ios: 1,
|
||||
android: 1,
|
||||
};
|
||||
|
||||
export function checkPlatform(platform) {
|
||||
if (!validPlatforms[platform]) {
|
||||
throw new Error(`Invalid platform '${platform}'`);
|
||||
}
|
||||
return platform
|
||||
}
|
||||
|
||||
export function getSelectedApp(platform) {
|
||||
checkPlatform(platform);
|
||||
|
||||
if (!fs.existsSync('update.json')){
|
||||
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
|
||||
}
|
||||
const updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
|
||||
if (!updateInfo[platform]) {
|
||||
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
|
||||
}
|
||||
return updateInfo[platform];
|
||||
}
|
||||
|
||||
export async function listApp(platform){
|
||||
const {data} = await get('/app/list');
|
||||
const list = platform?data.filter(v=>v.platform===platform):data;
|
||||
for (const app of list) {
|
||||
console.log(`${app.id}) ${app.name}(${app.platform})`);
|
||||
}
|
||||
if (platform) {
|
||||
console.log(`\nTotal ${list.length} ${platform} apps`);
|
||||
} else {
|
||||
console.log(`\nTotal ${list.length} apps`);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export async function chooseApp(platform) {
|
||||
const list = await listApp(platform);
|
||||
|
||||
while (true) {
|
||||
const id = await question('Enter appId:');
|
||||
const app = list.find(v=>v.id === (id|0));
|
||||
if (app) {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const commands = {
|
||||
createApp: async function ({options}) {
|
||||
const name = options.name || await question('App Name:');
|
||||
const {downloadUrl} = options;
|
||||
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
|
||||
const {id} = await post('/app/create', {name, platform});
|
||||
console.log(`Created app ${id}`);
|
||||
await this.selectApp({
|
||||
args: [id],
|
||||
options: {platform, downloadUrl},
|
||||
});
|
||||
},
|
||||
deleteApp: async function ({args, options}) {
|
||||
const {platform} = options;
|
||||
const id = args[0] || chooseApp(platform);
|
||||
if (!id) {
|
||||
console.log('Canceled');
|
||||
}
|
||||
await doDelete(`/app/${id}`);
|
||||
console.log('Ok.');
|
||||
},
|
||||
apps: async function ({options}){
|
||||
const {platform} = options;
|
||||
listApp(platform);
|
||||
},
|
||||
selectApp: async function({args, options}) {
|
||||
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
|
||||
const id = args[0] || (await chooseApp(platform)).id;
|
||||
|
||||
let updateInfo = {};
|
||||
if (fs.existsSync('update.json')) {
|
||||
try {
|
||||
updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse file `update.json`. Try to remove it manually.');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const {appKey} = await get(`/app/${id}`);
|
||||
updateInfo[platform] = {
|
||||
appId: id,
|
||||
appKey,
|
||||
};
|
||||
fs.writeFileSync('update.json', JSON.stringify(updateInfo, null, 4), 'utf8');
|
||||
},
|
||||
}
|
@@ -1,507 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/22/16.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
import { getRNVersion, translateOptions } from './utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import { ZipFile } from 'yazl';
|
||||
import { open as openZipFile } from 'yauzl';
|
||||
import { question } from './utils';
|
||||
import { checkPlatform } from './app';
|
||||
const { spawn, spawnSync, execSync } = require('child_process');
|
||||
const g2js = require('gradle-to-js/lib/parser');
|
||||
const os = require('os');
|
||||
|
||||
var diff;
|
||||
try {
|
||||
var bsdiff = require('node-bsdiff');
|
||||
diff = typeof bsdiff != 'function' ? bsdiff.diff : bsdiff;
|
||||
} catch (e) {
|
||||
diff = function() {
|
||||
console.warn(
|
||||
'This function needs "node-bsdiff". Please run "npm i node-bsdiff" from your project directory first!',
|
||||
);
|
||||
throw new Error('This function needs module "node-bsdiff". Please install it first.');
|
||||
};
|
||||
}
|
||||
|
||||
function exec(command) {
|
||||
const commandResult = spawnSync(command, {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
if (commandResult.error) {
|
||||
throw commandResult.error;
|
||||
}
|
||||
}
|
||||
|
||||
async function runReactNativeBundleCommand(
|
||||
bundleName,
|
||||
development,
|
||||
entryFile,
|
||||
outputFolder,
|
||||
platform,
|
||||
sourcemapOutput,
|
||||
config,
|
||||
) {
|
||||
let reactNativeBundleArgs = [];
|
||||
|
||||
let envArgs = process.env.PUSHY_ENV_ARGS;
|
||||
|
||||
if (envArgs) {
|
||||
Array.prototype.push.apply(reactNativeBundleArgs, envArgs.trim().split(/\s+/));
|
||||
}
|
||||
|
||||
fs.emptyDirSync(outputFolder);
|
||||
|
||||
Array.prototype.push.apply(reactNativeBundleArgs, [
|
||||
path.join("node_modules", "react-native", "local-cli", "cli.js"),
|
||||
"bundle",
|
||||
'--assets-dest',
|
||||
outputFolder,
|
||||
'--bundle-output',
|
||||
path.join(outputFolder, bundleName),
|
||||
'--dev',
|
||||
development,
|
||||
'--entry-file',
|
||||
entryFile,
|
||||
'--platform',
|
||||
platform,
|
||||
]);
|
||||
|
||||
if (sourcemapOutput) {
|
||||
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
|
||||
}
|
||||
|
||||
if (config) {
|
||||
reactNativeBundleArgs.push('--config', config);
|
||||
}
|
||||
|
||||
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
|
||||
console.log(`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
reactNativeBundleProcess.stdout.on('data', data => {
|
||||
console.log(data.toString().trim());
|
||||
});
|
||||
|
||||
reactNativeBundleProcess.stderr.on('data', data => {
|
||||
console.error(data.toString().trim());
|
||||
});
|
||||
|
||||
reactNativeBundleProcess.on('close', async exitCode => {
|
||||
if (exitCode) {
|
||||
reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
|
||||
} else {
|
||||
if (platform === 'android') {
|
||||
await compileHermesByteCode(bundleName, outputFolder);
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getHermesOSBin() {
|
||||
if (os.platform() === 'win32') return 'win64-bin';
|
||||
if (os.platform() === 'darwin') return 'osx-bin';
|
||||
if (os.platform() === 'linux') return 'linux64-bin';
|
||||
}
|
||||
|
||||
async function compileHermesByteCode(bundleName, outputFolder) {
|
||||
let enableHermes = false;
|
||||
try {
|
||||
const gradleConfig = await g2js.parseFile('android/app/build.gradle');
|
||||
const projectConfig = gradleConfig['project.ext.react'];
|
||||
for (const packagerConfig of projectConfig) {
|
||||
if (packagerConfig.includes('enableHermes') && packagerConfig.includes('true')) {
|
||||
enableHermes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
if (enableHermes) {
|
||||
console.log(`Hermes enabled, now compiling to hermes bytecode:\n`);
|
||||
const hermesPath = fs.existsSync('node_modules/hermes-engine')
|
||||
? 'node_modules/hermes-engine'
|
||||
: 'node_modules/hermesvm';
|
||||
execSync(
|
||||
`${hermesPath}/${getHermesOSBin()}/hermes -emit-binary -out ${outputFolder}/${bundleName} ${outputFolder}/${bundleName} -O`,
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function pack(dir, output) {
|
||||
console.log('Packing');
|
||||
fs.ensureDirSync(path.dirname(output));
|
||||
await new Promise((resolve, reject) => {
|
||||
var zipfile = new ZipFile();
|
||||
|
||||
function addDirectory(root, rel) {
|
||||
if (rel) {
|
||||
zipfile.addEmptyDirectory(rel);
|
||||
}
|
||||
const childs = fs.readdirSync(root);
|
||||
for (const name of childs) {
|
||||
if (name === '.' || name === '..') {
|
||||
continue;
|
||||
}
|
||||
const fullPath = path.join(root, name);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isFile()) {
|
||||
//console.log('adding: ' + rel+name);
|
||||
zipfile.addFile(fullPath, rel + name);
|
||||
} else if (stat.isDirectory()) {
|
||||
//console.log('adding: ' + rel+name+'/');
|
||||
addDirectory(fullPath, rel + name + '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addDirectory(dir, '');
|
||||
|
||||
zipfile.outputStream.on('error', err => reject(err));
|
||||
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
|
||||
resolve();
|
||||
});
|
||||
zipfile.end();
|
||||
});
|
||||
console.log('Bundled saved to: ' + output);
|
||||
}
|
||||
|
||||
function readEntire(entry, zipFile) {
|
||||
const buffers = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
zipFile.openReadStream(entry, (err, stream) => {
|
||||
stream.pipe({
|
||||
write(chunk) {
|
||||
buffers.push(chunk);
|
||||
},
|
||||
end() {
|
||||
resolve(Buffer.concat(buffers));
|
||||
},
|
||||
prependListener() {},
|
||||
on() {},
|
||||
once() {},
|
||||
emit() {},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function basename(fn) {
|
||||
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
|
||||
return m && m[1];
|
||||
}
|
||||
|
||||
async function diffFromPPK(origin, next, output) {
|
||||
fs.ensureDirSync(path.dirname(output));
|
||||
|
||||
const originEntries = {};
|
||||
const originMap = {};
|
||||
|
||||
let originSource;
|
||||
|
||||
await enumZipEntries(origin, (entry, zipFile) => {
|
||||
originEntries[entry.fileName] = entry;
|
||||
if (!/\/$/.test(entry.fileName)) {
|
||||
// isFile
|
||||
originMap[entry.crc32] = entry.fileName;
|
||||
|
||||
if (entry.fileName === 'index.bundlejs') {
|
||||
// This is source.
|
||||
return readEntire(entry, zipFile).then(v => (originSource = v));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
originSource = originSource || new Buffer(0);
|
||||
|
||||
const copies = {};
|
||||
|
||||
var zipfile = new ZipFile();
|
||||
|
||||
const writePromise = new Promise((resolve, reject) => {
|
||||
zipfile.outputStream.on('error', err => {
|
||||
throw err;
|
||||
});
|
||||
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const addedEntry = {};
|
||||
|
||||
function addEntry(fn) {
|
||||
//console.log(fn);
|
||||
if (!fn || addedEntry[fn]) {
|
||||
return;
|
||||
}
|
||||
const base = basename(fn);
|
||||
if (base) {
|
||||
addEntry(base);
|
||||
}
|
||||
zipfile.addEmptyDirectory(fn);
|
||||
}
|
||||
|
||||
const newEntries = {};
|
||||
|
||||
await enumZipEntries(next, (entry, nextZipfile) => {
|
||||
newEntries[entry.fileName] = entry;
|
||||
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
// Directory
|
||||
if (!originEntries[entry.fileName]) {
|
||||
addEntry(entry.fileName);
|
||||
}
|
||||
} else if (entry.fileName === 'index.bundlejs') {
|
||||
//console.log('Found bundle');
|
||||
return readEntire(entry, nextZipfile).then(newSource => {
|
||||
//console.log('Begin diff');
|
||||
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
|
||||
//console.log('End diff');
|
||||
});
|
||||
} else {
|
||||
// If same file.
|
||||
const originEntry = originEntries[entry.fileName];
|
||||
if (originEntry && originEntry.crc32 === entry.crc32) {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
// If moved from other place
|
||||
if (originMap[entry.crc32]) {
|
||||
const base = basename(entry.fileName);
|
||||
if (!originEntries[base]) {
|
||||
addEntry(base);
|
||||
}
|
||||
copies[entry.fileName] = originMap[entry.crc32];
|
||||
return;
|
||||
}
|
||||
|
||||
// New file.
|
||||
addEntry(basename(entry.fileName));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
nextZipfile.openReadStream(entry, function(err, readStream) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
zipfile.addReadStream(readStream, entry.fileName);
|
||||
readStream.on('end', () => {
|
||||
//console.log('add finished');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const deletes = {};
|
||||
|
||||
for (var k in originEntries) {
|
||||
if (!newEntries[k]) {
|
||||
console.log('Delete ' + k);
|
||||
deletes[k] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
//console.log({copies, deletes});
|
||||
zipfile.addBuffer(new Buffer(JSON.stringify({ copies, deletes })), '__diff.json');
|
||||
zipfile.end();
|
||||
await writePromise;
|
||||
}
|
||||
|
||||
async function diffFromPackage(origin, next, output, originBundleName, transformPackagePath = v => v) {
|
||||
fs.ensureDirSync(path.dirname(output));
|
||||
|
||||
const originEntries = {};
|
||||
const originMap = {};
|
||||
|
||||
let originSource;
|
||||
|
||||
await enumZipEntries(origin, (entry, zipFile) => {
|
||||
if (!/\/$/.test(entry.fileName)) {
|
||||
const fn = transformPackagePath(entry.fileName);
|
||||
if (!fn) {
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(fn);
|
||||
// isFile
|
||||
originEntries[fn] = entry.crc32;
|
||||
originMap[entry.crc32] = fn;
|
||||
|
||||
if (fn === originBundleName) {
|
||||
// This is source.
|
||||
return readEntire(entry, zipFile).then(v => (originSource = v));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
originSource = originSource || new Buffer(0);
|
||||
|
||||
const copies = {};
|
||||
|
||||
var zipfile = new ZipFile();
|
||||
|
||||
const writePromise = new Promise((resolve, reject) => {
|
||||
zipfile.outputStream.on('error', err => {
|
||||
throw err;
|
||||
});
|
||||
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await enumZipEntries(next, (entry, nextZipfile) => {
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
// Directory
|
||||
zipfile.addEmptyDirectory(entry.fileName);
|
||||
} else if (entry.fileName === 'index.bundlejs') {
|
||||
//console.log('Found bundle');
|
||||
return readEntire(entry, nextZipfile).then(newSource => {
|
||||
//console.log('Begin diff');
|
||||
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
|
||||
//console.log('End diff');
|
||||
});
|
||||
} else {
|
||||
// If same file.
|
||||
if (originEntries[entry.fileName] === entry.crc32) {
|
||||
copies[entry.fileName] = '';
|
||||
return;
|
||||
}
|
||||
// If moved from other place
|
||||
if (originMap[entry.crc32]) {
|
||||
copies[entry.fileName] = originMap[entry.crc32];
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
nextZipfile.openReadStream(entry, function(err, readStream) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
zipfile.addReadStream(readStream, entry.fileName);
|
||||
readStream.on('end', () => {
|
||||
//console.log('add finished');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
zipfile.addBuffer(new Buffer(JSON.stringify({ copies })), '__diff.json');
|
||||
zipfile.end();
|
||||
await writePromise;
|
||||
}
|
||||
|
||||
function enumZipEntries(zipFn, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
openZipFile(zipFn, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
zipfile.on('end', resolve);
|
||||
zipfile.on('error', reject);
|
||||
zipfile.on('entry', entry => {
|
||||
const result = callback(entry, zipfile);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(() => zipfile.readEntry());
|
||||
} else {
|
||||
zipfile.readEntry();
|
||||
}
|
||||
});
|
||||
zipfile.readEntry();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const commands = {
|
||||
bundle: async function({ options }) {
|
||||
const platform = checkPlatform(options.platform || (await question('Platform(ios/android):')));
|
||||
|
||||
let { bundleName, entryFile, intermediaDir, output, dev, verbose } = translateOptions({
|
||||
...options,
|
||||
platform,
|
||||
});
|
||||
|
||||
// const sourcemapOutput = path.join(intermediaDir, bundleName + ".map");
|
||||
|
||||
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
|
||||
|
||||
if (!platform) {
|
||||
throw new Error('Platform must be specified.');
|
||||
}
|
||||
|
||||
const { version, major, minor } = getRNVersion();
|
||||
|
||||
console.log('Bundling with React Native version: ', version);
|
||||
|
||||
await runReactNativeBundleCommand(bundleName, dev, entryFile, intermediaDir, platform);
|
||||
|
||||
await pack(path.resolve(intermediaDir), realOutput);
|
||||
|
||||
const v = await question('Would you like to publish it?(Y/N)');
|
||||
if (v.toLowerCase() === 'y') {
|
||||
await this.publish({
|
||||
args: [realOutput],
|
||||
options: {
|
||||
platform,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async diff({ args, options }) {
|
||||
const [origin, next] = args;
|
||||
const { output } = options;
|
||||
|
||||
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
|
||||
|
||||
if (!origin || !next) {
|
||||
console.error('pushy diff <origin> <next>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await diffFromPPK(origin, next, realOutput, 'index.bundlejs');
|
||||
console.log(`${realOutput} generated.`);
|
||||
},
|
||||
|
||||
async diffFromApk({ args, options }) {
|
||||
const [origin, next] = args;
|
||||
const { output } = options;
|
||||
|
||||
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
|
||||
|
||||
if (!origin || !next) {
|
||||
console.error('pushy diffFromApk <origin> <next>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await diffFromPackage(origin, next, realOutput, 'assets/index.android.bundle');
|
||||
console.log(`${realOutput} generated.`);
|
||||
},
|
||||
|
||||
async diffFromIpa({ args, options }) {
|
||||
const [origin, next] = args;
|
||||
const { output } = options;
|
||||
|
||||
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
|
||||
|
||||
if (!origin || !next) {
|
||||
console.error('pushy diffFromIpa <origin> <next>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', v => {
|
||||
const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
|
||||
return m && m[1];
|
||||
});
|
||||
|
||||
console.log(`${realOutput} generated.`);
|
||||
},
|
||||
};
|
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
const {loadSession} = require('./api');
|
||||
|
||||
function printUsage({args}) {
|
||||
// const commandName = args[0];
|
||||
// TODO: print usage of commandName, or print global usage.
|
||||
|
||||
console.log('Usage is under development now.')
|
||||
console.log('Visit `https://github.com/reactnativecn/react-native-pushy` for early document.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const commands = {
|
||||
...require('./user').commands,
|
||||
...require('./bundle').commands,
|
||||
...require('./app').commands,
|
||||
...require('./package').commands,
|
||||
...require('./versions').commands,
|
||||
help: printUsage,
|
||||
};
|
||||
|
||||
exports.run = function () {
|
||||
const argv = require('cli-arguments').parse(require('../cli.json'));
|
||||
global.NO_INTERACTIVE = argv.options['no-interactive'];
|
||||
|
||||
loadSession()
|
||||
.then(()=>commands[argv.command](argv))
|
||||
.catch(err=>{
|
||||
if (err.status === 401) {
|
||||
console.log('Not loggined.\nRun `pushy login` at your project directory to login.');
|
||||
return;
|
||||
}
|
||||
console.error(err.stack);
|
||||
process.exit(-1);
|
||||
});
|
||||
};
|
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 4/2/16.
|
||||
*/
|
||||
|
||||
const { get, post, uploadFile } = require('./api');
|
||||
import { question } from './utils';
|
||||
|
||||
import { checkPlatform, getSelectedApp } from './app';
|
||||
|
||||
import { getApkInfo, getIpaInfo } from './utils';
|
||||
const Table = require('tty-table');
|
||||
|
||||
export async function listPackage(appId) {
|
||||
const { data } = await get(`/app/${appId}/package/list?limit=1000`);
|
||||
|
||||
const header = [{ value: 'Package Id' }, { value: 'Version' }];
|
||||
const rows = [];
|
||||
for (const pkg of data) {
|
||||
const { version } = pkg;
|
||||
let versionInfo = '';
|
||||
if (version) {
|
||||
versionInfo = ` - ${version.id} ${version.hash.slice(0, 8)} ${version.name}`;
|
||||
} else {
|
||||
versionInfo = ' (newest)';
|
||||
}
|
||||
|
||||
rows.push([pkg.id, `${pkg.name}(${pkg.status})${versionInfo}`]);
|
||||
}
|
||||
|
||||
console.log(Table(header, rows).render());
|
||||
console.log(`\nTotal ${data.length} package(s).`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function choosePackage(appId) {
|
||||
const list = await listPackage(appId);
|
||||
|
||||
while (true) {
|
||||
const id = await question('Enter Package Id:');
|
||||
const app = list.find(v => v.id === (id | 0));
|
||||
if (app) {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const commands = {
|
||||
uploadIpa: async function({ args }) {
|
||||
const fn = args[0];
|
||||
if (!fn) {
|
||||
throw new Error('Usage: pushy uploadIpa <ipaFile>');
|
||||
}
|
||||
const { versionName, buildTime } = await getIpaInfo(fn);
|
||||
const { appId } = await getSelectedApp('ios');
|
||||
|
||||
const { hash } = await uploadFile(fn);
|
||||
|
||||
const { id } = await post(`/app/${appId}/package/create`, {
|
||||
name: versionName,
|
||||
hash,
|
||||
buildTime,
|
||||
});
|
||||
console.log(`Ipa uploaded: ${id}`);
|
||||
},
|
||||
uploadApk: async function({ args }) {
|
||||
const fn = args[0];
|
||||
if (!fn) {
|
||||
throw new Error('Usage: pushy uploadApk <apkFile>');
|
||||
}
|
||||
const { versionName, buildTime } = await getApkInfo(fn);
|
||||
const { appId } = await getSelectedApp('android');
|
||||
|
||||
const { hash } = await uploadFile(fn);
|
||||
|
||||
const { id } = await post(`/app/${appId}/package/create`, {
|
||||
name: versionName,
|
||||
hash,
|
||||
buildTime,
|
||||
});
|
||||
console.log(`Apk uploaded: ${id}`);
|
||||
},
|
||||
packages: async function({ options }) {
|
||||
const platform = checkPlatform(options.platform || (await question('Platform(ios/android):')));
|
||||
const { appId } = await getSelectedApp(platform);
|
||||
await listPackage(appId);
|
||||
},
|
||||
};
|
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
import {question} from './utils';
|
||||
const {
|
||||
post,
|
||||
get,
|
||||
replaceSession,
|
||||
saveSession,
|
||||
closeSession,
|
||||
} = require('./api');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function md5(str) {
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
exports.commands = {
|
||||
login: async function ({args}){
|
||||
const email = args[0] || await question('email:');
|
||||
const pwd = args[1] || await question('password:', true);
|
||||
const {token, info} = await post('/user/login', {
|
||||
email,
|
||||
pwd: md5(pwd),
|
||||
});
|
||||
replaceSession({token});
|
||||
await saveSession();
|
||||
console.log(`Welcome, ${info.name}.`);
|
||||
},
|
||||
logout: async function (){
|
||||
await closeSession();
|
||||
console.log('Logged out.');
|
||||
},
|
||||
me: async function (){
|
||||
const me = await get('/user/me');
|
||||
for (const k in me) {
|
||||
if (k !== 'ok') {
|
||||
console.log(`${k}: ${me[k]}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
const AppInfoParser = require('app-info-parser');
|
||||
|
||||
var read = require('read');
|
||||
|
||||
export function question(query, password) {
|
||||
if (NO_INTERACTIVE) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
return new Promise((resolve, reject) =>
|
||||
read(
|
||||
{
|
||||
prompt: query,
|
||||
silent: password,
|
||||
replace: password ? '*' : undefined,
|
||||
},
|
||||
(err, result) => (err ? reject(err) : resolve(result)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function translateOptions(options) {
|
||||
const ret = {};
|
||||
for (let key in options) {
|
||||
const v = options[key];
|
||||
if (typeof v === 'string') {
|
||||
ret[key] = v.replace(/\$\{(\w+)\}/g, function(v, n) {
|
||||
return options[n] || process.env[n] || v;
|
||||
});
|
||||
} else {
|
||||
ret[key] = v;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getRNVersion() {
|
||||
const version = JSON.parse(fs.readFileSync(path.resolve('node_modules/react-native/package.json'))).version;
|
||||
|
||||
// We only care about major and minor version.
|
||||
const match = /^(\d+)\.(\d+)\./.exec(version);
|
||||
return {
|
||||
version,
|
||||
major: match[1] | 0,
|
||||
minor: match[2] | 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getApkInfo(fn) {
|
||||
const appInfoParser = new AppInfoParser(fn);
|
||||
const { versionName, application } = await appInfoParser.parse();
|
||||
let buildTime = 0;
|
||||
if (Array.isArray(application.metaData)) {
|
||||
for (const meta of application.metaData) {
|
||||
if (meta.name === 'pushy_build_time') {
|
||||
buildTime = meta.value[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buildTime == 0) {
|
||||
throw new Error('Can not get build time for this app.');
|
||||
}
|
||||
return { versionName, buildTime };
|
||||
}
|
||||
|
||||
export async function getIpaInfo(fn) {
|
||||
const appInfoParser = new AppInfoParser(fn);
|
||||
const { CFBundleShortVersionString: versionName } = await appInfoParser.parse();
|
||||
let buildTimeTxtBuffer = await appInfoParser.parser.getEntry(/payload\/.+?\.app\/pushy_build_time.txt/);
|
||||
if (!buildTimeTxtBuffer) {
|
||||
// Not in root bundle when use `use_frameworks`
|
||||
buildTimeTxtBuffer = await appInfoParser.parser.getEntry(/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/);
|
||||
}
|
||||
if (!buildTimeTxtBuffer) {
|
||||
throw new Error('Can not get build time for this app.');
|
||||
}
|
||||
const buildTime = buildTimeTxtBuffer.toString().replace('\n', '');
|
||||
return { versionName, buildTime };
|
||||
}
|
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Created by tdzl2003 on 4/2/16.
|
||||
*/
|
||||
|
||||
const {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
uploadFile,
|
||||
} = require('./api');
|
||||
import { question } from './utils';
|
||||
|
||||
import { checkPlatform, getSelectedApp } from './app';
|
||||
import { choosePackage } from './package';
|
||||
|
||||
async function showVersion(appId, offset) {
|
||||
const { data, count } = await get(`/app/${appId}/version/list`);
|
||||
console.log(`Offset ${offset}`);
|
||||
for (const version of data) {
|
||||
let packageInfo = version.packages.slice(0, 3).map(v=>v.name).join(', ');
|
||||
const count = version.packages.length;
|
||||
if (count > 3) {
|
||||
packageInfo += `...and ${count-3} more`;
|
||||
}
|
||||
if (count === 0) {
|
||||
packageInfo = `(no package)`;
|
||||
} else {
|
||||
packageInfo = `[${packageInfo}]`;
|
||||
}
|
||||
console.log(`${version.id}) ${version.hash.slice(0, 8)} ${version.name} ${packageInfo}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function listVersions(appId) {
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
await showVersion(appId, offset);
|
||||
const cmd = await question('page Up/page Down/Begin/Quit(U/D/B/Q)');
|
||||
switch (cmd.toLowerCase()) {
|
||||
case 'u': offset = Math.max(0, offset - 10); break;
|
||||
case 'd': offset += 10; break;
|
||||
case 'b': offset = 0; break;
|
||||
case 'q': return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseVersion(appId) {
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const data = await showVersion(appId, offset);
|
||||
const cmd = await question('Enter versionId or page Up/page Down/Begin(U/D/B)');
|
||||
switch (cmd.toLowerCase()) {
|
||||
case 'U': offset = Math.max(0, offset - 10); break;
|
||||
case 'D': offset += 10; break;
|
||||
case 'B': offset = 0; break;
|
||||
default:
|
||||
{
|
||||
const v = data.find(v=>v.id === (cmd | 0));
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const commands = {
|
||||
publish: async function({args, options}) {
|
||||
const fn = args[0];
|
||||
const {name, description, metaInfo } = options;
|
||||
|
||||
if (!fn) {
|
||||
throw new Error('Usage: pushy publish <ppkFile> --platform ios|android');
|
||||
}
|
||||
|
||||
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
|
||||
const { appId } = await getSelectedApp(platform);
|
||||
|
||||
const { hash } = await uploadFile(fn);
|
||||
|
||||
const { id } = await post(`/app/${appId}/version/create`, {
|
||||
name: name || await question('Enter version name:') || '(未命名)',
|
||||
hash,
|
||||
description: description || await question('Enter description:'),
|
||||
metaInfo: metaInfo || await question('Enter meta info:'),
|
||||
});
|
||||
console.log(`Version published: ${id}`);
|
||||
|
||||
const v = await question('Would you like to bind packages to this version?(Y/N)');
|
||||
if (v.toLowerCase() === 'y') {
|
||||
await this.update({args:[], options:{versionId: id, platform}});
|
||||
}
|
||||
},
|
||||
versions: async function({options}) {
|
||||
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
|
||||
const { appId } = await getSelectedApp(platform);
|
||||
await listVersions(appId);
|
||||
},
|
||||
update: async function({args, options}) {
|
||||
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
|
||||
const { appId } = await getSelectedApp(platform);
|
||||
const versionId = options.versionId || (await chooseVersion(appId)).id;
|
||||
const pkgId = options.packageId || (await choosePackage(appId)).id;
|
||||
await put(`/app/${appId}/package/${pkgId}`, {
|
||||
versionId,
|
||||
});
|
||||
console.log('Ok.');
|
||||
}
|
||||
};
|
33
package.json
33
package.json
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "react-native-update",
|
||||
"version": "5.5.4",
|
||||
"version": "5.6.0",
|
||||
"description": "react-native hot update",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build-lib": "$ANDROID_HOME/ndk-bundle/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_LIBS_OUT=android/lib",
|
||||
"prepare": "node_modules/.bin/babel local-cli/src --out-dir local-cli/lib"
|
||||
"build-lib": "$ANDROID_HOME/ndk-bundle/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_LIBS_OUT=android/lib"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,31 +26,5 @@
|
||||
"react-native": ">=0.27.0"
|
||||
},
|
||||
"homepage": "https://github.com/reactnativecn/react-native-pushy#readme",
|
||||
"dependencies": {
|
||||
"app-info-parser": "^0.3.5",
|
||||
"cli-arguments": "^0.2.1",
|
||||
"fs-extra": "^8.1.0",
|
||||
"gradle-to-js": "^2.0.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"progress": "^1.1.8",
|
||||
"read": "^1.0.7",
|
||||
"request": "^2.69.0",
|
||||
"tty-table": "^2.7.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.5.1",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-plugin-syntax-async-functions": "^6.5.0",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.5.0",
|
||||
"babel-plugin-transform-async-to-generator": "^6.3.13",
|
||||
"babel-plugin-transform-es2015-arrow-functions": "^6.5.2",
|
||||
"babel-plugin-transform-es2015-destructuring": "^6.3.15",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.3.16",
|
||||
"babel-plugin-transform-es2015-parameters": "^6.5.0",
|
||||
"babel-plugin-transform-es2015-spread": "^6.5.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.5.0",
|
||||
"babel-plugin-transform-strict-mode": "^6.5.2"
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-strict-mode"
|
||||
]
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
/.idea
|
||||
/src
|
||||
/.babelrc
|
||||
/.npmignore
|
||||
/.eslintrc
|
||||
/.nvmrc
|
||||
/.travis.yml
|
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "react-native-update-cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Command tools for javaScript updater with `pushy` service for react native apps.",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"pushy": "lib/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepublish": "node_modules/.bin/babel src --out-dir lib"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/reactnativecn/react-native-pushy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"ios",
|
||||
"android",
|
||||
"update"
|
||||
],
|
||||
"author": "reactnativecn",
|
||||
"license": "BSD-3-Clause",
|
||||
"bugs": {
|
||||
"url": "https://github.com/reactnativecn/react-native-pushy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/reactnativecn/react-native-pushy/tree/master/react-native-pushy-cli",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.5.1",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.5.2",
|
||||
"babel-plugin-transform-strict-mode": "^6.5.2"
|
||||
}
|
||||
}
|
53
react-native-pushy-cli/src/cli.js
vendored
53
react-native-pushy-cli/src/cli.js
vendored
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Created by tdzl2003 on 2/13/16.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
const CLI_MODULE_PATH = function() {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'react-native-update',
|
||||
'local-cli'
|
||||
);
|
||||
};
|
||||
|
||||
const PACKAGE_JSON_PATH = function() {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'react-native-update',
|
||||
'package.json'
|
||||
);
|
||||
};
|
||||
|
||||
checkForVersionCommand();
|
||||
|
||||
let cli;
|
||||
const cliPath = CLI_MODULE_PATH();
|
||||
if (fs.existsSync(cliPath)) {
|
||||
cli = require(cliPath);
|
||||
}
|
||||
|
||||
if (cli) {
|
||||
cli.run();
|
||||
} else {
|
||||
console.error('Are you at home directory of a react-native project?');
|
||||
console.error('`pushy install` is under development, please run `npm install react-native-update` to install pushy manually.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function checkForVersionCommand() {
|
||||
if (process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version') {
|
||||
console.log('react-native-update-cli: ' + require('../package.json').version);
|
||||
try {
|
||||
console.log('react-native-update: ' + require(PACKAGE_JSON_PATH()).version);
|
||||
} catch (e) {
|
||||
console.log('react-native-update: n/a - not inside a React Native project directory')
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
|
||||
s.vendored_libraries = 'RCTPushy/libRCTPushy.a'
|
||||
s.pod_target_xcconfig = { 'USER_HEADER_SEARCH_PATHS' => '"$(SRCROOT)/../node_modules/react-native-update/ios"' }
|
||||
s.resource = 'ios/pushy_build_time.txt'
|
||||
s.script_phase = { :name => 'Generate build time', :script => '../../node_modules/react-native-update/scripts/generateiOSBuildTime.sh', :execution_position => :before_compile }
|
||||
s.script_phase = { :name => 'Generate build time', :script => 'set -x;date +%s > ${PODS_ROOT}/../../node_modules/react-native-update/ios/pushy_build_time.txt', :execution_position => :before_compile }
|
||||
|
||||
s.dependency 'React'
|
||||
s.dependency 'SSZipArchive'
|
||||
|
@@ -1,5 +0,0 @@
|
||||
|
||||
#!/bin/bash
|
||||
set -x
|
||||
DEST="../../node_modules/react-native-update/ios"
|
||||
date +%s > "$DEST/pushy_build_time.txt"
|
Reference in New Issue
Block a user