react-native-update-cli 1.37.0 → 1.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.json CHANGED
@@ -33,6 +33,8 @@
33
33
  },
34
34
  "uploadIpa": {},
35
35
  "uploadApk": {},
36
+ "uploadApp": {},
37
+ "parseApp": {},
36
38
  "parseIpa": {},
37
39
  "parseApk": {},
38
40
  "packages": {
package/lib/api.js CHANGED
@@ -61,13 +61,11 @@ let savedSession;
61
61
  const defaultEndpoint = 'https://update.reactnative.cn/api';
62
62
  let host = process.env.PUSHY_REGISTRY || defaultEndpoint;
63
63
  const userAgent = `react-native-update-cli/${_packagejson.default.version}`;
64
- const getSession = function() {
65
- return session;
66
- };
67
- const replaceSession = function(newSession) {
64
+ const getSession = ()=>session;
65
+ const replaceSession = (newSession)=>{
68
66
  session = newSession;
69
67
  };
70
- const loadSession = async function() {
68
+ const loadSession = async ()=>{
71
69
  if (_nodefs.default.existsSync('.update')) {
72
70
  try {
73
71
  replaceSession(JSON.parse(_nodefs.default.readFileSync('.update', 'utf8')));
@@ -78,7 +76,7 @@ const loadSession = async function() {
78
76
  }
79
77
  }
80
78
  };
81
- const saveSession = function() {
79
+ const saveSession = ()=>{
82
80
  // Only save on change.
83
81
  if (session !== savedSession) {
84
82
  const current = session;
@@ -87,7 +85,7 @@ const saveSession = function() {
87
85
  savedSession = current;
88
86
  }
89
87
  };
90
- const closeSession = function() {
88
+ const closeSession = ()=>{
91
89
  if (_nodefs.default.existsSync('.update')) {
92
90
  _nodefs.default.unlinkSync('.update');
93
91
  savedSession = undefined;
@@ -101,32 +99,27 @@ async function query(url, options) {
101
99
  let json;
102
100
  try {
103
101
  json = JSON.parse(text);
104
- } catch (e) {
105
- if (resp.statusText.includes('Unauthorized')) {
102
+ } catch (e) {}
103
+ if (resp.status !== 200) {
104
+ const message = (json == null ? void 0 : json.message) || resp.statusText;
105
+ if (resp.status === 401) {
106
106
  throw new Error('登录信息已过期,请使用 pushy login 命令重新登录');
107
- } else {
108
- throw new Error(`Server error: ${resp.statusText}`);
109
107
  }
110
- }
111
- if (resp.status !== 200) {
112
- throw new Error(`${resp.status}: ${resp.statusText}`);
108
+ throw new Error(message);
113
109
  }
114
110
  return json;
115
111
  }
116
112
  function queryWithoutBody(method) {
117
- return function(api) {
118
- return query(host + api, {
113
+ return (api)=>query(host + api, {
119
114
  method,
120
115
  headers: {
121
116
  'User-Agent': userAgent,
122
117
  'X-AccessToken': session ? session.token : ''
123
118
  }
124
119
  });
125
- };
126
120
  }
127
121
  function queryWithBody(method) {
128
- return function(api, body) {
129
- return query(host + api, {
122
+ return (api, body)=>query(host + api, {
130
123
  method,
131
124
  headers: {
132
125
  'User-Agent': userAgent,
@@ -135,7 +128,6 @@ function queryWithBody(method) {
135
128
  },
136
129
  body: JSON.stringify(body)
137
130
  });
138
- };
139
131
  }
140
132
  const get = queryWithoutBody('GET');
141
133
  const post = queryWithBody('POST');
@@ -176,7 +168,7 @@ async function uploadFile(fn, key) {
176
168
  form.append(k, v);
177
169
  });
178
170
  const fileStream = _nodefs.default.createReadStream(fn);
179
- fileStream.on('data', function(data) {
171
+ fileStream.on('data', (data)=>{
180
172
  bar.tick(data.length);
181
173
  });
182
174
  if (key) {
package/lib/app.js CHANGED
@@ -26,7 +26,7 @@ _export(exports, {
26
26
  }
27
27
  });
28
28
  const _utils = require("./utils");
29
- const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
29
+ const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
30
30
  const _ttytable = /*#__PURE__*/ _interop_require_default(require("tty-table"));
31
31
  const _api = require("./api");
32
32
  function _interop_require_default(obj) {
@@ -47,10 +47,10 @@ function checkPlatform(platform) {
47
47
  }
48
48
  function getSelectedApp(platform) {
49
49
  checkPlatform(platform);
50
- if (!_fs.default.existsSync('update.json')) {
50
+ if (!_nodefs.default.existsSync('update.json')) {
51
51
  throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
52
52
  }
53
- const updateInfo = JSON.parse(_fs.default.readFileSync('update.json', 'utf8'));
53
+ const updateInfo = JSON.parse(_nodefs.default.readFileSync('update.json', 'utf8'));
54
54
  if (!updateInfo[platform]) {
55
55
  throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
56
56
  }
@@ -116,7 +116,7 @@ const commands = {
116
116
  }
117
117
  });
118
118
  },
119
- deleteApp: async function({ args, options }) {
119
+ deleteApp: async ({ args, options })=>{
120
120
  const { platform } = options;
121
121
  const id = args[0] || chooseApp(platform);
122
122
  if (!id) {
@@ -125,17 +125,17 @@ const commands = {
125
125
  await (0, _api.doDelete)(`/app/${id}`);
126
126
  console.log('操作成功');
127
127
  },
128
- apps: async function({ options }) {
128
+ apps: async ({ options })=>{
129
129
  const { platform } = options;
130
130
  listApp(platform);
131
131
  },
132
- selectApp: async function({ args, options }) {
132
+ selectApp: async ({ args, options })=>{
133
133
  const platform = checkPlatform(options.platform || await (0, _utils.question)('平台(ios/android/harmony):'));
134
- const id = args[0] ? parseInt(args[0]) : (await chooseApp(platform)).id;
134
+ const id = args[0] ? Number.parseInt(args[0]) : (await chooseApp(platform)).id;
135
135
  let updateInfo = {};
136
- if (_fs.default.existsSync('update.json')) {
136
+ if (_nodefs.default.existsSync('update.json')) {
137
137
  try {
138
- updateInfo = JSON.parse(_fs.default.readFileSync('update.json', 'utf8'));
138
+ updateInfo = JSON.parse(_nodefs.default.readFileSync('update.json', 'utf8'));
139
139
  } catch (e) {
140
140
  console.error('Failed to parse file `update.json`. Try to remove it manually.');
141
141
  throw e;
@@ -146,6 +146,6 @@ const commands = {
146
146
  appId: id,
147
147
  appKey
148
148
  };
149
- _fs.default.writeFileSync('update.json', JSON.stringify(updateInfo, null, 4), 'utf8');
149
+ _nodefs.default.writeFileSync('update.json', JSON.stringify(updateInfo, null, 4), 'utf8');
150
150
  }
151
151
  };
package/lib/bundle.js CHANGED
@@ -2,10 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- Object.defineProperty(exports, "commands", {
6
- enumerable: true,
7
- get: function() {
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: all[name]
9
+ });
10
+ }
11
+ _export(exports, {
12
+ commands: function() {
8
13
  return commands;
14
+ },
15
+ enumZipEntries: function() {
16
+ return enumZipEntries;
17
+ },
18
+ readEntire: function() {
19
+ return readEntire;
9
20
  }
10
21
  });
11
22
  const _utils = require("./utils");
@@ -194,11 +205,19 @@ async function runReactNativeBundleCommand(bundleName, development, entryFile, o
194
205
  async function copyHarmonyBundle(outputFolder) {
195
206
  const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile';
196
207
  try {
208
+ await _fsextra.ensureDir(harmonyRawPath);
209
+ try {
210
+ await _fsextra.access(harmonyRawPath, _fsextra.constants.W_OK);
211
+ } catch (error) {
212
+ await _fsextra.chmod(harmonyRawPath, 0o755);
213
+ }
214
+ await _fsextra.remove(path.join(harmonyRawPath, 'update.json'));
215
+ await _fsextra.copy('update.json', path.join(harmonyRawPath, 'update.json'));
197
216
  await _fsextra.ensureDir(outputFolder);
198
217
  await _fsextra.copy(harmonyRawPath, outputFolder);
199
- console.log(`Successfully copied from ${harmonyRawPath} to ${outputFolder}`);
200
218
  } catch (error) {
201
- console.error('Error in copyHarmonyBundle:', error);
219
+ console.error('copyHarmonyBundle 错误:', error);
220
+ throw new Error(`复制文件失败: ${error.message}`);
202
221
  }
203
222
  }
204
223
  function getHermesOSBin() {
package/lib/package.js CHANGED
@@ -119,6 +119,35 @@ const commands = {
119
119
  (0, _utils.saveToLocal)(fn, `${appId}/package/${id}.apk`);
120
120
  console.log(`已成功上传apk原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`);
121
121
  },
122
+ uploadApp: async function({ args }) {
123
+ const fn = args[0];
124
+ if (!fn || !fn.endsWith('.app')) {
125
+ throw new Error('使用方法: pushy uploadApp app后缀文件');
126
+ }
127
+ const { versionName, buildTime, appId: appIdInPkg, appKey: appKeyInPkg } = await (0, _utils.getAppInfo)(fn);
128
+ const { appId, appKey } = await (0, _app.getSelectedApp)('harmony');
129
+ if (appIdInPkg && appIdInPkg != appId) {
130
+ throw new Error(`appId不匹配!当前app: ${appIdInPkg}, 当前update.json: ${appId}`);
131
+ }
132
+ if (appKeyInPkg && appKeyInPkg !== appKey) {
133
+ throw new Error(`appKey不匹配!当前app: ${appKeyInPkg}, 当前update.json: ${appKey}`);
134
+ }
135
+ const { hash } = await (0, _api.uploadFile)(fn);
136
+ const { id } = await (0, _api.post)(`/app/${appId}/package/create`, {
137
+ name: versionName,
138
+ hash,
139
+ buildTime
140
+ });
141
+ (0, _utils.saveToLocal)(fn, `${appId}/package/${id}.app`);
142
+ console.log(`已成功上传app原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`);
143
+ },
144
+ parseApp: async function({ args }) {
145
+ const fn = args[0];
146
+ if (!fn || !fn.endsWith('.app')) {
147
+ throw new Error('使用方法: pushy parseApp app后缀文件');
148
+ }
149
+ console.log(await (0, _utils.getAppInfo)(fn));
150
+ },
122
151
  parseIpa: async function({ args }) {
123
152
  const fn = args[0];
124
153
  if (!fn || !fn.endsWith('.ipa')) {
package/lib/user.js CHANGED
@@ -10,17 +10,17 @@ Object.defineProperty(exports, "commands", {
10
10
  });
11
11
  const _utils = require("./utils");
12
12
  const _api = require("./api");
13
- const _crypto = /*#__PURE__*/ _interop_require_default(require("crypto"));
13
+ const _nodecrypto = /*#__PURE__*/ _interop_require_default(require("node:crypto"));
14
14
  function _interop_require_default(obj) {
15
15
  return obj && obj.__esModule ? obj : {
16
16
  default: obj
17
17
  };
18
18
  }
19
19
  function md5(str) {
20
- return _crypto.default.createHash('md5').update(str).digest('hex');
20
+ return _nodecrypto.default.createHash('md5').update(str).digest('hex');
21
21
  }
22
22
  const commands = {
23
- login: async function({ args }) {
23
+ login: async ({ args })=>{
24
24
  const email = args[0] || await (0, _utils.question)('email:');
25
25
  const pwd = args[1] || await (0, _utils.question)('password:', true);
26
26
  const { token, info } = await (0, _api.post)('/user/login', {
@@ -33,11 +33,11 @@ const commands = {
33
33
  await (0, _api.saveSession)();
34
34
  console.log(`欢迎使用 pushy 热更新服务, ${info.name}.`);
35
35
  },
36
- logout: async function() {
36
+ logout: async ()=>{
37
37
  await (0, _api.closeSession)();
38
38
  console.log('已退出登录');
39
39
  },
40
- me: async function() {
40
+ me: async ()=>{
41
41
  const me = await (0, _api.get)('/user/me');
42
42
  for(const k in me){
43
43
  if (k !== 'ok') {
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ const Zip = require('./zip');
3
+ class AppParser extends Zip {
4
+ /**
5
+ * parser for parsing .apk file
6
+ * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
7
+ */ constructor(file){
8
+ super(file);
9
+ if (!(this instanceof AppParser)) {
10
+ return new AppParser(file);
11
+ }
12
+ }
13
+ }
14
+ module.exports = AppParser;
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  const ApkParser = require('./apk');
3
3
  const IpaParser = require('./ipa');
4
+ const AppParser = require('./app');
4
5
  const supportFileTypes = [
5
6
  'ipa',
6
- 'apk'
7
+ 'apk',
8
+ 'app'
7
9
  ];
8
10
  class AppInfoParser {
9
11
  parse() {
@@ -14,12 +16,12 @@ class AppInfoParser {
14
16
  * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
15
17
  */ constructor(file){
16
18
  if (!file) {
17
- throw new Error('Param miss: file(file\'s path in Node, instance of File or Blob in browser).');
19
+ throw new Error("Param miss: file(file's path in Node, instance of File or Blob in browser).");
18
20
  }
19
21
  const splits = (file.name || file).split('.');
20
22
  const fileType = splits[splits.length - 1].toLowerCase();
21
23
  if (!supportFileTypes.includes(fileType)) {
22
- throw new Error('Unsupported file type, only support .ipa or .apk file.');
24
+ throw new Error('Unsupported file type, only support .ipa or .apk or .app file.');
23
25
  }
24
26
  this.file = file;
25
27
  switch(fileType){
@@ -29,6 +31,9 @@ class AppInfoParser {
29
31
  case 'apk':
30
32
  this.parser = new ApkParser(this.file);
31
33
  break;
34
+ case 'app':
35
+ this.parser = new AppParser(this.file);
36
+ break;
32
37
  }
33
38
  }
34
39
  }
@@ -1,4 +1,8 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _bundle = require("../../bundle");
2
6
  const Unzip = require('isomorphic-unzip');
3
7
  const { isBrowser, decodeNullUnicode } = require('./utils');
4
8
  class Zip {
@@ -28,10 +32,24 @@ class Zip {
28
32
  ], {
29
33
  type
30
34
  }, (err, buffers)=>{
35
+ console.log(buffers);
31
36
  err ? reject(err) : resolve(buffers[regex]);
32
37
  });
33
38
  });
34
39
  }
40
+ async getEntryFromHarmonyApp(regex) {
41
+ try {
42
+ let originSource;
43
+ await (0, _bundle.enumZipEntries)(this.file, (entry, zipFile)=>{
44
+ if (regex.test(entry.fileName)) {
45
+ return (0, _bundle.readEntire)(entry, zipFile).then((v)=>originSource = v);
46
+ }
47
+ });
48
+ return originSource;
49
+ } catch (error) {
50
+ console.error('Error in getEntryFromHarmonyApp:', error);
51
+ }
52
+ }
35
53
  constructor(file){
36
54
  if (isBrowser()) {
37
55
  if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) {
@@ -12,6 +12,9 @@ _export(exports, {
12
12
  getApkInfo: function() {
13
13
  return getApkInfo;
14
14
  },
15
+ getAppInfo: function() {
16
+ return getAppInfo;
17
+ },
15
18
  getIpaInfo: function() {
16
19
  return getIpaInfo;
17
20
  },
@@ -115,6 +118,36 @@ async function getApkInfo(fn) {
115
118
  ...appCredential
116
119
  };
117
120
  }
121
+ async function getAppInfo(fn) {
122
+ const appInfoParser = new _appinfoparser.default(fn);
123
+ const bundleFile = await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/bundle.harmony.js/);
124
+ if (!bundleFile) {
125
+ throw new Error('找不到bundle文件。请确保此app为release版本,且bundle文件名为默认的bundle.harmony.js');
126
+ }
127
+ const updateJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/update.json/);
128
+ let appCredential = {};
129
+ if (updateJsonFile) {
130
+ appCredential = JSON.parse(updateJsonFile.toString()).harmony;
131
+ }
132
+ const metaJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/meta.json/);
133
+ let metaData = {};
134
+ if (metaJsonFile) {
135
+ metaData = JSON.parse(metaJsonFile.toString());
136
+ }
137
+ const { versionName, pushy_build_time } = metaData;
138
+ let buildTime = 0;
139
+ if (pushy_build_time) {
140
+ buildTime = pushy_build_time;
141
+ }
142
+ if (buildTime == 0) {
143
+ throw new Error('无法获取此包的编译时间戳。请更新 react-native-update 到最新版本后重新打包上传。');
144
+ }
145
+ return {
146
+ versionName,
147
+ buildTime,
148
+ ...appCredential
149
+ };
150
+ }
118
151
  async function getIpaInfo(fn) {
119
152
  const appInfoParser = new _appinfoparser.default(fn);
120
153
  const bundleFile = await appInfoParser.parser.getEntry(/payload\/.+?\.app\/main.jsbundle/);
package/lib/versions.js CHANGED
@@ -106,12 +106,12 @@ const commands = {
106
106
  });
107
107
  }
108
108
  },
109
- versions: async function({ options }) {
109
+ versions: async ({ options })=>{
110
110
  const platform = (0, _app.checkPlatform)(options.platform || await (0, _utils.question)('平台(ios/android/harmony):'));
111
111
  const { appId } = await (0, _app.getSelectedApp)(platform);
112
112
  await listVersions(appId);
113
113
  },
114
- update: async function({ args, options }) {
114
+ update: async ({ args, options })=>{
115
115
  const platform = (0, _app.checkPlatform)(options.platform || await (0, _utils.question)('平台(ios/android/harmony):'));
116
116
  const { appId } = await (0, _app.getSelectedApp)(platform);
117
117
  let versionId = options.versionId || (await chooseVersion(appId)).id;
@@ -127,7 +127,7 @@ const commands = {
127
127
  rollout = null;
128
128
  } else {
129
129
  try {
130
- rollout = parseInt(rollout);
130
+ rollout = Number.parseInt(rollout);
131
131
  } catch (e) {
132
132
  throw new Error('rollout 必须是 1-100 的整数');
133
133
  }
@@ -228,7 +228,7 @@ const commands = {
228
228
  });
229
229
  console.log(`已将热更版本 ${versionId} 绑定到原生版本 ${pkgVersion} (id: ${pkgId})`);
230
230
  },
231
- updateVersionInfo: async function({ args, options }) {
231
+ updateVersionInfo: async ({ args, options })=>{
232
232
  const platform = (0, _app.checkPlatform)(options.platform || await (0, _utils.question)('平台(ios/android/harmony):'));
233
233
  const { appId } = await (0, _app.getSelectedApp)(platform);
234
234
  const versionId = options.versionId || (await chooseVersion(appId)).id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update-cli",
3
- "version": "1.37.0",
3
+ "version": "1.38.1",
4
4
  "description": "Command tools for javaScript updater with `pushy` service for react native apps.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "swc src -d lib --strip-leading-paths",
16
- "prepare": "npm run build && chmod +x lib/index.js"
16
+ "prepare": "npm run build && chmod +x lib/index.js",
17
+ "lint": "tsc --noEmit & biome check --write ."
17
18
  },
18
19
  "repository": {
19
20
  "type": "git",
@@ -62,11 +63,10 @@
62
63
  "node": ">= 10"
63
64
  },
64
65
  "devDependencies": {
66
+ "@biomejs/biome": "^1.9.4",
65
67
  "@swc/cli": "^0.5.1",
66
68
  "@swc/core": "^1.9.3",
67
69
  "@types/node": "^22.9.3",
68
- "oxlint": "^0.13.1",
69
70
  "typescript": "^5.7.2"
70
- },
71
- "packageManager": "yarn@1.22.22"
71
+ }
72
72
  }
package/src/api.ts CHANGED
@@ -7,7 +7,7 @@ import packageJson from '../package.json';
7
7
  import tcpp from 'tcp-ping';
8
8
  import filesizeParser from 'filesize-parser';
9
9
  import { pricingPageUrl } from './utils';
10
- import { Session } from 'types';
10
+ import type { Session } from 'types';
11
11
  import FormData from 'form-data';
12
12
 
13
13
  const tcpPing = util.promisify(tcpp.ping);
@@ -20,15 +20,13 @@ let host = process.env.PUSHY_REGISTRY || defaultEndpoint;
20
20
 
21
21
  const userAgent = `react-native-update-cli/${packageJson.version}`;
22
22
 
23
- export const getSession = function () {
24
- return session;
25
- };
23
+ export const getSession = () => session;
26
24
 
27
- export const replaceSession = function (newSession: { token: string }) {
25
+ export const replaceSession = (newSession: { token: string }) => {
28
26
  session = newSession;
29
27
  };
30
28
 
31
- export const loadSession = async function () {
29
+ export const loadSession = async () => {
32
30
  if (fs.existsSync('.update')) {
33
31
  try {
34
32
  replaceSession(JSON.parse(fs.readFileSync('.update', 'utf8')));
@@ -42,7 +40,7 @@ export const loadSession = async function () {
42
40
  }
43
41
  };
44
42
 
45
- export const saveSession = function () {
43
+ export const saveSession = () => {
46
44
  // Only save on change.
47
45
  if (session !== savedSession) {
48
46
  const current = session;
@@ -52,7 +50,7 @@ export const saveSession = function () {
52
50
  }
53
51
  };
54
52
 
55
- export const closeSession = function () {
53
+ export const closeSession = () => {
56
54
  if (fs.existsSync('.update')) {
57
55
  fs.unlinkSync('.update');
58
56
  savedSession = undefined;
@@ -64,38 +62,35 @@ export const closeSession = function () {
64
62
  async function query(url: string, options: fetch.RequestInit) {
65
63
  const resp = await fetch(url, options);
66
64
  const text = await resp.text();
67
- let json;
65
+ let json: any;
68
66
  try {
69
67
  json = JSON.parse(text);
70
- } catch (e) {
71
- if (resp.statusText.includes('Unauthorized')) {
72
- throw new Error('登录信息已过期,请使用 pushy login 命令重新登录');
73
- } else {
74
- throw new Error(`Server error: ${resp.statusText}`);
75
- }
76
- }
68
+ } catch (e) {}
77
69
 
78
70
  if (resp.status !== 200) {
79
- throw new Error(`${resp.status}: ${resp.statusText}`);
71
+ const message = json?.message || resp.statusText;
72
+ if (resp.status === 401) {
73
+ throw new Error('登录信息已过期,请使用 pushy login 命令重新登录');
74
+ }
75
+ throw new Error(message);
80
76
  }
81
77
  return json;
82
78
  }
83
79
 
84
80
  function queryWithoutBody(method: string) {
85
- return function (api: string) {
86
- return query(host + api, {
81
+ return (api: string) =>
82
+ query(host + api, {
87
83
  method,
88
84
  headers: {
89
85
  'User-Agent': userAgent,
90
86
  'X-AccessToken': session ? session.token : '',
91
87
  },
92
88
  });
93
- };
94
89
  }
95
90
 
96
91
  function queryWithBody(method: string) {
97
- return function (api: string, body: Record<string, any>) {
98
- return query(host + api, {
92
+ return (api: string, body: Record<string, any>) =>
93
+ query(host + api, {
99
94
  method,
100
95
  headers: {
101
96
  'User-Agent': userAgent,
@@ -104,7 +99,6 @@ function queryWithBody(method: string) {
104
99
  },
105
100
  body: JSON.stringify(body),
106
101
  });
107
- };
108
102
  }
109
103
 
110
104
  export const get = queryWithoutBody('GET');
@@ -155,7 +149,7 @@ export async function uploadFile(fn: string, key?: string) {
155
149
  form.append(k, v);
156
150
  });
157
151
  const fileStream = fs.createReadStream(fn);
158
- fileStream.on('data', function (data) {
152
+ fileStream.on('data', (data) => {
159
153
  bar.tick(data.length);
160
154
  });
161
155
 
package/src/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { question } from './utils';
2
- import fs from 'fs';
2
+ import fs from 'node:fs';
3
3
  import Table from 'tty-table';
4
4
 
5
5
  import { post, get, doDelete } from './api';
@@ -84,7 +84,7 @@ export const commands = {
84
84
  options: { platform, downloadUrl },
85
85
  });
86
86
  },
87
- deleteApp: async function ({ args, options }) {
87
+ deleteApp: async ({ args, options }) => {
88
88
  const { platform } = options;
89
89
  const id = args[0] || chooseApp(platform);
90
90
  if (!id) {
@@ -93,15 +93,17 @@ export const commands = {
93
93
  await doDelete(`/app/${id}`);
94
94
  console.log('操作成功');
95
95
  },
96
- apps: async function ({ options }) {
96
+ apps: async ({ options }) => {
97
97
  const { platform } = options;
98
98
  listApp(platform);
99
99
  },
100
- selectApp: async function ({ args, options }) {
100
+ selectApp: async ({ args, options }) => {
101
101
  const platform = checkPlatform(
102
102
  options.platform || (await question('平台(ios/android/harmony):')),
103
103
  );
104
- const id = args[0] ? parseInt(args[0]) : (await chooseApp(platform)).id;
104
+ const id = args[0]
105
+ ? Number.parseInt(args[0])
106
+ : (await chooseApp(platform)).id;
105
107
 
106
108
  let updateInfo = {};
107
109
  if (fs.existsSync('update.json')) {
package/src/bundle.js CHANGED
@@ -184,16 +184,21 @@ async function runReactNativeBundleCommand(
184
184
 
185
185
  async function copyHarmonyBundle(outputFolder) {
186
186
  const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile';
187
-
188
187
  try {
188
+ await fs.ensureDir(harmonyRawPath);
189
+ try {
190
+ await fs.access(harmonyRawPath, fs.constants.W_OK);
191
+ } catch (error) {
192
+ await fs.chmod(harmonyRawPath, 0o755);
193
+ }
194
+ await fs.remove(path.join(harmonyRawPath, 'update.json'));
195
+ await fs.copy('update.json', path.join(harmonyRawPath, 'update.json'));
196
+
189
197
  await fs.ensureDir(outputFolder);
190
198
  await fs.copy(harmonyRawPath, outputFolder);
191
-
192
- console.log(
193
- `Successfully copied from ${harmonyRawPath} to ${outputFolder}`,
194
- );
195
199
  } catch (error) {
196
- console.error('Error in copyHarmonyBundle:', error);
200
+ console.error('copyHarmonyBundle 错误:', error);
201
+ throw new Error(`复制文件失败: ${error.message}`);
197
202
  }
198
203
  }
199
204
 
@@ -333,7 +338,7 @@ async function pack(dir, output) {
333
338
  console.log('ppk热更包已生成并保存到: ' + output);
334
339
  }
335
340
 
336
- function readEntire(entry, zipFile) {
341
+ export function readEntire(entry, zipFile) {
337
342
  const buffers = [];
338
343
  return new Promise((resolve, reject) => {
339
344
  zipFile.openReadStream(entry, (err, stream) => {
@@ -608,7 +613,7 @@ async function diffFromPackage(
608
613
  await writePromise;
609
614
  }
610
615
 
611
- async function enumZipEntries(zipFn, callback, nestedPath = '') {
616
+ export async function enumZipEntries(zipFn, callback, nestedPath = '') {
612
617
  return new Promise((resolve, reject) => {
613
618
  openZipFile(zipFn, { lazyEntries: true }, async (err, zipfile) => {
614
619
  if (err) {
package/src/package.js CHANGED
@@ -3,7 +3,7 @@ import { question, saveToLocal } from './utils';
3
3
 
4
4
  import { checkPlatform, getSelectedApp } from './app';
5
5
 
6
- import { getApkInfo, getIpaInfo } from './utils';
6
+ import { getApkInfo, getIpaInfo, getAppInfo } from './utils';
7
7
  import Table from 'tty-table';
8
8
 
9
9
  export async function listPackage(appId) {
@@ -122,6 +122,51 @@ export const commands = {
122
122
  `已成功上传apk原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`,
123
123
  );
124
124
  },
125
+ uploadApp: async function ({ args }) {
126
+ const fn = args[0];
127
+ if (!fn || !fn.endsWith('.app')) {
128
+ throw new Error('使用方法: pushy uploadApp app后缀文件');
129
+ }
130
+ const {
131
+ versionName,
132
+ buildTime,
133
+ appId: appIdInPkg,
134
+ appKey: appKeyInPkg,
135
+ } = await getAppInfo(fn);
136
+ const { appId, appKey } = await getSelectedApp('harmony');
137
+
138
+
139
+ if (appIdInPkg && appIdInPkg != appId) {
140
+ throw new Error(
141
+ `appId不匹配!当前app: ${appIdInPkg}, 当前update.json: ${appId}`,
142
+ );
143
+ }
144
+
145
+ if (appKeyInPkg && appKeyInPkg !== appKey) {
146
+ throw new Error(
147
+ `appKey不匹配!当前app: ${appKeyInPkg}, 当前update.json: ${appKey}`,
148
+ );
149
+ }
150
+
151
+ const { hash } = await uploadFile(fn);
152
+
153
+ const { id } = await post(`/app/${appId}/package/create`, {
154
+ name: versionName,
155
+ hash,
156
+ buildTime,
157
+ });
158
+ saveToLocal(fn, `${appId}/package/${id}.app`);
159
+ console.log(
160
+ `已成功上传app原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`,
161
+ );
162
+ },
163
+ parseApp: async function ({ args }) {
164
+ const fn = args[0];
165
+ if (!fn || !fn.endsWith('.app')) {
166
+ throw new Error('使用方法: pushy parseApp app后缀文件');
167
+ }
168
+ console.log(await getAppInfo(fn));
169
+ },
125
170
  parseIpa: async function ({ args }) {
126
171
  const fn = args[0];
127
172
  if (!fn || !fn.endsWith('.ipa')) {
package/src/user.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { question } from './utils';
2
2
  import { post, get, replaceSession, saveSession, closeSession } from './api';
3
- import crypto from 'crypto';
3
+ import crypto from 'node:crypto';
4
4
 
5
5
  function md5(str) {
6
6
  return crypto.createHash('md5').update(str).digest('hex');
7
7
  }
8
8
 
9
9
  export const commands = {
10
- login: async function ({ args }) {
10
+ login: async ({ args }) => {
11
11
  const email = args[0] || (await question('email:'));
12
12
  const pwd = args[1] || (await question('password:', true));
13
13
  const { token, info } = await post('/user/login', {
@@ -18,11 +18,11 @@ export const commands = {
18
18
  await saveSession();
19
19
  console.log(`欢迎使用 pushy 热更新服务, ${info.name}.`);
20
20
  },
21
- logout: async function () {
21
+ logout: async () => {
22
22
  await closeSession();
23
23
  console.log('已退出登录');
24
24
  },
25
- me: async function () {
25
+ me: async () => {
26
26
  const me = await get('/user/me');
27
27
  for (const k in me) {
28
28
  if (k !== 'ok') {
@@ -0,0 +1,16 @@
1
+ const Zip = require('./zip')
2
+
3
+ class AppParser extends Zip {
4
+ /**
5
+ * parser for parsing .apk file
6
+ * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
7
+ */
8
+ constructor (file) {
9
+ super(file)
10
+ if (!(this instanceof AppParser)) {
11
+ return new AppParser(file)
12
+ }
13
+ }
14
+ }
15
+
16
+ module.exports = AppParser
@@ -1,35 +1,43 @@
1
- const ApkParser = require('./apk')
2
- const IpaParser = require('./ipa')
3
- const supportFileTypes = ['ipa', 'apk']
1
+ const ApkParser = require('./apk');
2
+ const IpaParser = require('./ipa');
3
+ const AppParser = require('./app');
4
+ const supportFileTypes = ['ipa', 'apk', 'app'];
4
5
 
5
6
  class AppInfoParser {
6
7
  /**
7
8
  * parser for parsing .ipa or .apk file
8
9
  * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
9
10
  */
10
- constructor (file) {
11
+ constructor(file) {
11
12
  if (!file) {
12
- throw new Error('Param miss: file(file\'s path in Node, instance of File or Blob in browser).')
13
+ throw new Error(
14
+ "Param miss: file(file's path in Node, instance of File or Blob in browser).",
15
+ );
13
16
  }
14
- const splits = (file.name || file).split('.')
15
- const fileType = splits[splits.length - 1].toLowerCase()
17
+ const splits = (file.name || file).split('.');
18
+ const fileType = splits[splits.length - 1].toLowerCase();
16
19
  if (!supportFileTypes.includes(fileType)) {
17
- throw new Error('Unsupported file type, only support .ipa or .apk file.')
20
+ throw new Error(
21
+ 'Unsupported file type, only support .ipa or .apk or .app file.',
22
+ );
18
23
  }
19
- this.file = file
24
+ this.file = file;
20
25
 
21
26
  switch (fileType) {
22
27
  case 'ipa':
23
- this.parser = new IpaParser(this.file)
24
- break
28
+ this.parser = new IpaParser(this.file);
29
+ break;
25
30
  case 'apk':
26
- this.parser = new ApkParser(this.file)
27
- break
31
+ this.parser = new ApkParser(this.file);
32
+ break;
33
+ case 'app':
34
+ this.parser = new AppParser(this.file);
35
+ break;
28
36
  }
29
37
  }
30
- parse () {
31
- return this.parser.parse()
38
+ parse() {
39
+ return this.parser.parse();
32
40
  }
33
41
  }
34
42
 
35
- module.exports = AppInfoParser
43
+ module.exports = AppInfoParser;
@@ -1,20 +1,23 @@
1
- const Unzip = require('isomorphic-unzip')
2
- const { isBrowser, decodeNullUnicode } = require('./utils')
1
+ const Unzip = require('isomorphic-unzip');
2
+ const { isBrowser, decodeNullUnicode } = require('./utils');
3
+ import { enumZipEntries, readEntire } from '../../bundle';
3
4
 
4
5
  class Zip {
5
- constructor (file) {
6
+ constructor(file) {
6
7
  if (isBrowser()) {
7
8
  if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) {
8
- throw new Error('Param error: [file] must be an instance of Blob or File in browser.')
9
+ throw new Error(
10
+ 'Param error: [file] must be an instance of Blob or File in browser.',
11
+ );
9
12
  }
10
- this.file = file
13
+ this.file = file;
11
14
  } else {
12
15
  if (typeof file !== 'string') {
13
- throw new Error('Param error: [file] must be file path in Node.')
16
+ throw new Error('Param error: [file] must be file path in Node.');
14
17
  }
15
- this.file = require('path').resolve(file)
18
+ this.file = require('path').resolve(file);
16
19
  }
17
- this.unzip = new Unzip(this.file)
20
+ this.unzip = new Unzip(this.file);
18
21
  }
19
22
 
20
23
  /**
@@ -22,27 +25,42 @@ class Zip {
22
25
  * @param {Array} regexps // regexps for matching files
23
26
  * @param {String} type // return type, can be buffer or blob, default buffer
24
27
  */
25
- getEntries (regexps, type = 'buffer') {
26
- regexps = regexps.map(regex => decodeNullUnicode(regex))
28
+ getEntries(regexps, type = 'buffer') {
29
+ regexps = regexps.map((regex) => decodeNullUnicode(regex));
27
30
  return new Promise((resolve, reject) => {
28
31
  this.unzip.getBuffer(regexps, { type }, (err, buffers) => {
29
- err ? reject(err) : resolve(buffers)
30
- })
31
- })
32
+ err ? reject(err) : resolve(buffers);
33
+ });
34
+ });
32
35
  }
33
36
  /**
34
37
  * get entry by regex, return an instance of Buffer or Blob
35
38
  * @param {Regex} regex // regex for matching file
36
39
  * @param {String} type // return type, can be buffer or blob, default buffer
37
40
  */
38
- getEntry (regex, type = 'buffer') {
39
- regex = decodeNullUnicode(regex)
41
+ getEntry(regex, type = 'buffer') {
42
+ regex = decodeNullUnicode(regex);
40
43
  return new Promise((resolve, reject) => {
41
44
  this.unzip.getBuffer([regex], { type }, (err, buffers) => {
42
- err ? reject(err) : resolve(buffers[regex])
43
- })
44
- })
45
+ console.log(buffers);
46
+ err ? reject(err) : resolve(buffers[regex]);
47
+ });
48
+ });
49
+ }
50
+
51
+ async getEntryFromHarmonyApp(regex) {
52
+ try {
53
+ let originSource;
54
+ await enumZipEntries(this.file, (entry, zipFile) => {
55
+ if (regex.test(entry.fileName)) {
56
+ return readEntire(entry, zipFile).then((v) => (originSource = v));
57
+ }
58
+ });
59
+ return originSource;
60
+ } catch (error) {
61
+ console.error('Error in getEntryFromHarmonyApp:', error);
62
+ }
45
63
  }
46
64
  }
47
65
 
48
- module.exports = Zip
66
+ module.exports = Zip;
@@ -87,6 +87,43 @@ export async function getApkInfo(fn) {
87
87
  return { versionName, buildTime, ...appCredential };
88
88
  }
89
89
 
90
+ export async function getAppInfo(fn) {
91
+ const appInfoParser = new AppInfoParser(fn);
92
+ const bundleFile = await appInfoParser.parser.getEntryFromHarmonyApp(
93
+ /rawfile\/bundle.harmony.js/,
94
+ );
95
+ if (!bundleFile) {
96
+ throw new Error(
97
+ '找不到bundle文件。请确保此app为release版本,且bundle文件名为默认的bundle.harmony.js',
98
+ );
99
+ }
100
+ const updateJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp(
101
+ /rawfile\/update.json/,
102
+ );
103
+ let appCredential = {};
104
+ if (updateJsonFile) {
105
+ appCredential = JSON.parse(updateJsonFile.toString()).harmony;
106
+ }
107
+ const metaJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp(
108
+ /rawfile\/meta.json/,
109
+ );
110
+ let metaData = {};
111
+ if (metaJsonFile) {
112
+ metaData = JSON.parse(metaJsonFile.toString());
113
+ }
114
+ const { versionName, pushy_build_time } = metaData;
115
+ let buildTime = 0;
116
+ if (pushy_build_time) {
117
+ buildTime = pushy_build_time;
118
+ }
119
+ if (buildTime == 0) {
120
+ throw new Error(
121
+ '无法获取此包的编译时间戳。请更新 react-native-update 到最新版本后重新打包上传。',
122
+ );
123
+ }
124
+ return { versionName, buildTime, ...appCredential };
125
+ }
126
+
90
127
  export async function getIpaInfo(fn) {
91
128
  const appInfoParser = new AppInfoParser(fn);
92
129
  const bundleFile = await appInfoParser.parser.getEntry(
package/src/versions.js CHANGED
@@ -112,14 +112,14 @@ export const commands = {
112
112
  await this.update({ args: [], options: { versionId: id, platform } });
113
113
  }
114
114
  },
115
- versions: async function ({ options }) {
115
+ versions: async ({ options }) => {
116
116
  const platform = checkPlatform(
117
117
  options.platform || (await question('平台(ios/android/harmony):')),
118
118
  );
119
119
  const { appId } = await getSelectedApp(platform);
120
120
  await listVersions(appId);
121
121
  },
122
- update: async function ({ args, options }) {
122
+ update: async ({ args, options }) => {
123
123
  const platform = checkPlatform(
124
124
  options.platform || (await question('平台(ios/android/harmony):')),
125
125
  );
@@ -138,7 +138,7 @@ export const commands = {
138
138
  rollout = null;
139
139
  } else {
140
140
  try {
141
- rollout = parseInt(rollout);
141
+ rollout = Number.parseInt(rollout);
142
142
  } catch (e) {
143
143
  throw new Error('rollout 必须是 1-100 的整数');
144
144
  }
@@ -173,7 +173,9 @@ export const commands = {
173
173
  await put(`/app/${appId}/package/${pkg.id}`, {
174
174
  versionId,
175
175
  });
176
- console.log(`已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`);
176
+ console.log(
177
+ `已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`,
178
+ );
177
179
  }
178
180
  console.log(`操作完成,共已绑定 ${pkgs.length} 个原生版本`);
179
181
  return;
@@ -205,7 +207,9 @@ export const commands = {
205
207
  await put(`/app/${appId}/package/${pkg.id}`, {
206
208
  versionId,
207
209
  });
208
- console.log(`已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`);
210
+ console.log(
211
+ `已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`,
212
+ );
209
213
  }
210
214
  console.log(`操作完成,共已绑定 ${pkgs.length} 个原生版本`);
211
215
  return;
@@ -228,14 +232,14 @@ export const commands = {
228
232
  if (!pkgId) {
229
233
  throw new Error('请提供 packageId 或 packageVersion 参数');
230
234
  }
231
-
235
+
232
236
  if (!pkgVersion) {
233
237
  const pkg = data.find((d) => d.id === pkgId);
234
238
  if (pkg) {
235
239
  pkgVersion = pkg.name;
236
240
  }
237
241
  }
238
-
242
+
239
243
  if (rollout) {
240
244
  await put(`/app/${appId}/version/${versionId}`, {
241
245
  config: {
@@ -251,9 +255,11 @@ export const commands = {
251
255
  await put(`/app/${appId}/package/${pkgId}`, {
252
256
  versionId,
253
257
  });
254
- console.log(`已将热更版本 ${versionId} 绑定到原生版本 ${pkgVersion} (id: ${pkgId})`);
258
+ console.log(
259
+ `已将热更版本 ${versionId} 绑定到原生版本 ${pkgVersion} (id: ${pkgId})`,
260
+ );
255
261
  },
256
- updateVersionInfo: async function ({ args, options }) {
262
+ updateVersionInfo: async ({ args, options }) => {
257
263
  const platform = checkPlatform(
258
264
  options.platform || (await question('平台(ios/android/harmony):')),
259
265
  );