@revopush/code-push-cli 0.0.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/.eslintrc.json +17 -0
- package/.idea/cli.iml +9 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +774 -0
- package/bin/script/acquisition-sdk.js +178 -0
- package/bin/script/cli.js +23 -0
- package/bin/script/command-executor.js +1290 -0
- package/bin/script/command-parser.js +1097 -0
- package/bin/script/commands/debug.js +125 -0
- package/bin/script/hash-utils.js +203 -0
- package/bin/script/index.js +5 -0
- package/bin/script/management-sdk.js +419 -0
- package/bin/script/react-native-utils.js +249 -0
- package/bin/script/sign.js +69 -0
- package/bin/script/types/cli.js +40 -0
- package/bin/script/types/rest-definitions.js +19 -0
- package/bin/script/types.js +4 -0
- package/bin/script/utils/file-utils.js +50 -0
- package/bin/test/acquisition-rest-mock.js +108 -0
- package/bin/test/acquisition-sdk.js +188 -0
- package/bin/test/cli.js +1342 -0
- package/bin/test/hash-utils.js +149 -0
- package/bin/test/management-sdk.js +338 -0
- package/package.json +68 -0
- package/prettier.config.js +7 -0
- package/script/acquisition-sdk.ts +273 -0
- package/script/cli.ts +27 -0
- package/script/command-executor.ts +1610 -0
- package/script/command-parser.ts +1310 -0
- package/script/commands/debug.ts +148 -0
- package/script/hash-utils.ts +241 -0
- package/script/index.ts +5 -0
- package/script/management-sdk.ts +575 -0
- package/script/react-native-utils.ts +283 -0
- package/script/sign.ts +80 -0
- package/script/types/cli.ts +232 -0
- package/script/types/rest-definitions.ts +152 -0
- package/script/types.ts +35 -0
- package/script/utils/file-utils.ts +46 -0
- package/test/acquisition-rest-mock.ts +125 -0
- package/test/acquisition-sdk.ts +272 -0
- package/test/cli.ts +1692 -0
- package/test/hash-utils.ts +170 -0
- package/test/management-sdk.ts +438 -0
- package/test/resources/TestApp/android/app/build.gradle +56 -0
- package/test/resources/TestApp/iOS/TestApp/Info.plist +49 -0
- package/test/resources/TestApp/index.android.js +2 -0
- package/test/resources/TestApp/index.ios.js +2 -0
- package/test/resources/TestApp/index.windows.js +2 -0
- package/test/resources/TestApp/package.json +6 -0
- package/test/resources/TestApp/windows/TestApp/Package.appxmanifest +46 -0
- package/test/resources/ignoredMetadata.zip +0 -0
- package/test/resources/test.zip +0 -0
- package/test/superagent-mock-config.js +58 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation.
|
|
3
|
+
// Licensed under the MIT License.
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const Q = require("q");
|
|
8
|
+
const superagent = require("superagent");
|
|
9
|
+
const recursiveFs = require("recursive-fs");
|
|
10
|
+
const yazl = require("yazl");
|
|
11
|
+
const slash = require("slash");
|
|
12
|
+
var Promise = Q.Promise;
|
|
13
|
+
const packageJson = require("../../package.json");
|
|
14
|
+
// A template string tag function that URL encodes the substituted values
|
|
15
|
+
function urlEncode(strings, ...values) {
|
|
16
|
+
let result = "";
|
|
17
|
+
for (let i = 0; i < strings.length; i++) {
|
|
18
|
+
result += strings[i];
|
|
19
|
+
if (i < values.length) {
|
|
20
|
+
result += encodeURIComponent(values[i]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
class AccountManager {
|
|
26
|
+
static AppPermission = {
|
|
27
|
+
OWNER: "Owner",
|
|
28
|
+
COLLABORATOR: "Collaborator",
|
|
29
|
+
};
|
|
30
|
+
static SERVER_URL = "http://localhost:3000";
|
|
31
|
+
static API_VERSION = 2;
|
|
32
|
+
static ERROR_GATEWAY_TIMEOUT = 504; // Used if there is a network error
|
|
33
|
+
static ERROR_INTERNAL_SERVER = 500;
|
|
34
|
+
static ERROR_NOT_FOUND = 404;
|
|
35
|
+
static ERROR_CONFLICT = 409; // Used if the resource already exists
|
|
36
|
+
static ERROR_UNAUTHORIZED = 401;
|
|
37
|
+
_accessKey;
|
|
38
|
+
_serverUrl;
|
|
39
|
+
_customHeaders;
|
|
40
|
+
constructor(accessKey, customHeaders, serverUrl) {
|
|
41
|
+
if (!accessKey)
|
|
42
|
+
throw new Error("An access key must be specified.");
|
|
43
|
+
this._accessKey = accessKey;
|
|
44
|
+
this._customHeaders = customHeaders;
|
|
45
|
+
this._serverUrl = serverUrl || AccountManager.SERVER_URL;
|
|
46
|
+
}
|
|
47
|
+
get accessKey() {
|
|
48
|
+
return this._accessKey;
|
|
49
|
+
}
|
|
50
|
+
isAuthenticated(throwIfUnauthorized) {
|
|
51
|
+
return Promise((resolve, reject, notify) => {
|
|
52
|
+
const request = superagent.get(`${this._serverUrl}${urlEncode(["/authenticated"])}`);
|
|
53
|
+
this.attachCredentials(request);
|
|
54
|
+
request.end((err, res) => {
|
|
55
|
+
const status = this.getErrorStatus(err, res);
|
|
56
|
+
if (err && status !== AccountManager.ERROR_UNAUTHORIZED) {
|
|
57
|
+
reject(this.getCodePushError(err, res));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const authenticated = status === 200;
|
|
61
|
+
if (!authenticated && throwIfUnauthorized) {
|
|
62
|
+
reject(this.getCodePushError(err, res));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(authenticated);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
addAccessKey(friendlyName, ttl) {
|
|
70
|
+
if (!friendlyName) {
|
|
71
|
+
throw new Error("A name must be specified when adding an access key.");
|
|
72
|
+
}
|
|
73
|
+
const accessKeyRequest = {
|
|
74
|
+
createdBy: os.hostname(),
|
|
75
|
+
friendlyName,
|
|
76
|
+
ttl,
|
|
77
|
+
};
|
|
78
|
+
return this.post(urlEncode(["/accessKeys/"]), JSON.stringify(accessKeyRequest), /*expectResponseBody=*/ true).then((response) => {
|
|
79
|
+
return {
|
|
80
|
+
createdTime: response.body.accessKey.createdTime,
|
|
81
|
+
expires: response.body.accessKey.expires,
|
|
82
|
+
key: response.body.accessKey.name,
|
|
83
|
+
name: response.body.accessKey.friendlyName,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
getAccessKey(accessKeyName) {
|
|
88
|
+
return this.get(urlEncode([`/accessKeys/${accessKeyName}`])).then((res) => {
|
|
89
|
+
return {
|
|
90
|
+
createdTime: res.body.accessKey.createdTime,
|
|
91
|
+
expires: res.body.accessKey.expires,
|
|
92
|
+
name: res.body.accessKey.friendlyName,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
getAccessKeys() {
|
|
97
|
+
return this.get(urlEncode(["/accessKeys"])).then((res) => {
|
|
98
|
+
const accessKeys = [];
|
|
99
|
+
res.body.accessKeys.forEach((serverAccessKey) => {
|
|
100
|
+
!serverAccessKey.isSession &&
|
|
101
|
+
accessKeys.push({
|
|
102
|
+
createdTime: serverAccessKey.createdTime,
|
|
103
|
+
expires: serverAccessKey.expires,
|
|
104
|
+
name: serverAccessKey.friendlyName,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return accessKeys;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
getSessions() {
|
|
111
|
+
return this.get(urlEncode(["/accessKeys"])).then((res) => {
|
|
112
|
+
// A machine name might be associated with multiple session keys,
|
|
113
|
+
// but we should only return one per machine name.
|
|
114
|
+
const sessionMap = {};
|
|
115
|
+
const now = new Date().getTime();
|
|
116
|
+
res.body.accessKeys.forEach((serverAccessKey) => {
|
|
117
|
+
if (serverAccessKey.isSession && serverAccessKey.expires > now) {
|
|
118
|
+
sessionMap[serverAccessKey.createdBy] = {
|
|
119
|
+
loggedInTime: serverAccessKey.createdTime,
|
|
120
|
+
machineName: serverAccessKey.createdBy,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const sessions = Object.keys(sessionMap).map((machineName) => sessionMap[machineName]);
|
|
125
|
+
return sessions;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
patchAccessKey(oldName, newName, ttl) {
|
|
129
|
+
const accessKeyRequest = {
|
|
130
|
+
friendlyName: newName,
|
|
131
|
+
ttl,
|
|
132
|
+
};
|
|
133
|
+
return this.patch(urlEncode([`/accessKeys/${oldName}`]), JSON.stringify(accessKeyRequest)).then((res) => {
|
|
134
|
+
return {
|
|
135
|
+
createdTime: res.body.accessKey.createdTime,
|
|
136
|
+
expires: res.body.accessKey.expires,
|
|
137
|
+
name: res.body.accessKey.friendlyName,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
removeAccessKey(name) {
|
|
142
|
+
return this.del(urlEncode([`/accessKeys/${name}`])).then(() => null);
|
|
143
|
+
}
|
|
144
|
+
removeSession(machineName) {
|
|
145
|
+
return this.del(urlEncode([`/sessions/${machineName}`])).then(() => null);
|
|
146
|
+
}
|
|
147
|
+
// Account
|
|
148
|
+
getAccountInfo() {
|
|
149
|
+
return this.get(urlEncode(["/account"])).then((res) => res.body.account);
|
|
150
|
+
}
|
|
151
|
+
// Apps
|
|
152
|
+
getApps() {
|
|
153
|
+
return this.get(urlEncode(["/apps"])).then((res) => res.body.apps);
|
|
154
|
+
}
|
|
155
|
+
getApp(appName) {
|
|
156
|
+
return this.get(urlEncode([`/apps/${appName}`])).then((res) => res.body.app);
|
|
157
|
+
}
|
|
158
|
+
addApp(appName) {
|
|
159
|
+
const app = { name: appName };
|
|
160
|
+
return this.post(urlEncode(["/apps/"]), JSON.stringify(app), /*expectResponseBody=*/ false).then(() => app);
|
|
161
|
+
}
|
|
162
|
+
removeApp(appName) {
|
|
163
|
+
return this.del(urlEncode([`/apps/${appName}`])).then(() => null);
|
|
164
|
+
}
|
|
165
|
+
renameApp(oldAppName, newAppName) {
|
|
166
|
+
return this.patch(urlEncode([`/apps/${oldAppName}`]), JSON.stringify({ name: newAppName })).then(() => null);
|
|
167
|
+
}
|
|
168
|
+
transferApp(appName, email) {
|
|
169
|
+
return this.post(urlEncode([`/apps/${appName}/transfer/${email}`]), /*requestBody=*/ null, /*expectResponseBody=*/ false).then(() => null);
|
|
170
|
+
}
|
|
171
|
+
// Collaborators
|
|
172
|
+
getCollaborators(appName) {
|
|
173
|
+
return this.get(urlEncode([`/apps/${appName}/collaborators`])).then((res) => res.body.collaborators);
|
|
174
|
+
}
|
|
175
|
+
addCollaborator(appName, email) {
|
|
176
|
+
return this.post(urlEncode([`/apps/${appName}/collaborators/${email}`]),
|
|
177
|
+
/*requestBody=*/ null,
|
|
178
|
+
/*expectResponseBody=*/ false).then(() => null);
|
|
179
|
+
}
|
|
180
|
+
removeCollaborator(appName, email) {
|
|
181
|
+
return this.del(urlEncode([`/apps/${appName}/collaborators/${email}`])).then(() => null);
|
|
182
|
+
}
|
|
183
|
+
// Deployments
|
|
184
|
+
addDeployment(appName, deploymentName) {
|
|
185
|
+
const deployment = { name: deploymentName };
|
|
186
|
+
return this.post(urlEncode([`/apps/${appName}/deployments/`]), JSON.stringify(deployment), /*expectResponseBody=*/ true).then((res) => res.body.deployment);
|
|
187
|
+
}
|
|
188
|
+
clearDeploymentHistory(appName, deploymentName) {
|
|
189
|
+
return this.del(urlEncode([`/apps/${appName}/deployments/${deploymentName}/history`])).then(() => null);
|
|
190
|
+
}
|
|
191
|
+
getDeployments(appName) {
|
|
192
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/`])).then((res) => res.body.deployments);
|
|
193
|
+
}
|
|
194
|
+
getDeployment(appName, deploymentName) {
|
|
195
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then((res) => res.body.deployment);
|
|
196
|
+
}
|
|
197
|
+
renameDeployment(appName, oldDeploymentName, newDeploymentName) {
|
|
198
|
+
return this.patch(urlEncode([`/apps/${appName}/deployments/${oldDeploymentName}`]), JSON.stringify({ name: newDeploymentName })).then(() => null);
|
|
199
|
+
}
|
|
200
|
+
removeDeployment(appName, deploymentName) {
|
|
201
|
+
return this.del(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then(() => null);
|
|
202
|
+
}
|
|
203
|
+
getDeploymentMetrics(appName, deploymentName) {
|
|
204
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/metrics`])).then((res) => res.body.metrics);
|
|
205
|
+
}
|
|
206
|
+
getDeploymentHistory(appName, deploymentName) {
|
|
207
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/history`])).then((res) => res.body.history);
|
|
208
|
+
}
|
|
209
|
+
release(appName, deploymentName, filePath, targetBinaryVersion, updateMetadata, uploadProgressCallback) {
|
|
210
|
+
return Promise((resolve, reject, notify) => {
|
|
211
|
+
updateMetadata.appVersion = targetBinaryVersion;
|
|
212
|
+
const request = superagent.post(this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]));
|
|
213
|
+
this.attachCredentials(request);
|
|
214
|
+
const getPackageFilePromise = Q.Promise((resolve, reject) => {
|
|
215
|
+
this.packageFileFromPath(filePath)
|
|
216
|
+
.then((result) => {
|
|
217
|
+
resolve(result);
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
reject(error);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
getPackageFilePromise.then((packageFile) => {
|
|
224
|
+
const file = fs.createReadStream(packageFile.path);
|
|
225
|
+
request
|
|
226
|
+
.attach("package", file)
|
|
227
|
+
.field("packageInfo", JSON.stringify(updateMetadata))
|
|
228
|
+
.on("progress", (event) => {
|
|
229
|
+
if (uploadProgressCallback && event && event.total > 0) {
|
|
230
|
+
const currentProgress = (event.loaded / event.total) * 100;
|
|
231
|
+
uploadProgressCallback(currentProgress);
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.end((err, res) => {
|
|
235
|
+
if (packageFile.isTemporary) {
|
|
236
|
+
fs.unlinkSync(packageFile.path);
|
|
237
|
+
}
|
|
238
|
+
if (err) {
|
|
239
|
+
reject(this.getCodePushError(err, res));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (res.ok) {
|
|
243
|
+
resolve(null);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
let body;
|
|
247
|
+
try {
|
|
248
|
+
body = JSON.parse(res.text);
|
|
249
|
+
}
|
|
250
|
+
catch (err) { }
|
|
251
|
+
if (body) {
|
|
252
|
+
reject({
|
|
253
|
+
message: body.message,
|
|
254
|
+
statusCode: res && res.status,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
reject({
|
|
259
|
+
message: res.text,
|
|
260
|
+
statusCode: res && res.status,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
patchRelease(appName, deploymentName, label, updateMetadata) {
|
|
269
|
+
updateMetadata.label = label;
|
|
270
|
+
const requestBody = JSON.stringify({ packageInfo: updateMetadata });
|
|
271
|
+
return this.patch(urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]), requestBody,
|
|
272
|
+
/*expectResponseBody=*/ false).then(() => null);
|
|
273
|
+
}
|
|
274
|
+
promote(appName, sourceDeploymentName, destinationDeploymentName, updateMetadata) {
|
|
275
|
+
const requestBody = JSON.stringify({ packageInfo: updateMetadata });
|
|
276
|
+
return this.post(urlEncode([`/apps/${appName}/deployments/${sourceDeploymentName}/promote/${destinationDeploymentName}`]), requestBody,
|
|
277
|
+
/*expectResponseBody=*/ false).then(() => null);
|
|
278
|
+
}
|
|
279
|
+
rollback(appName, deploymentName, targetRelease) {
|
|
280
|
+
return this.post(urlEncode([`/apps/${appName}/deployments/${deploymentName}/rollback/${targetRelease || ``}`]),
|
|
281
|
+
/*requestBody=*/ null,
|
|
282
|
+
/*expectResponseBody=*/ false).then(() => null);
|
|
283
|
+
}
|
|
284
|
+
packageFileFromPath(filePath) {
|
|
285
|
+
let getPackageFilePromise;
|
|
286
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
287
|
+
getPackageFilePromise = Promise((resolve, reject) => {
|
|
288
|
+
const directoryPath = filePath;
|
|
289
|
+
recursiveFs.readdirr(directoryPath, (error, directories, files) => {
|
|
290
|
+
if (error) {
|
|
291
|
+
reject(error);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const baseDirectoryPath = path.dirname(directoryPath);
|
|
295
|
+
const fileName = this.generateRandomFilename(15) + ".zip";
|
|
296
|
+
const zipFile = new yazl.ZipFile();
|
|
297
|
+
const writeStream = fs.createWriteStream(fileName);
|
|
298
|
+
zipFile.outputStream
|
|
299
|
+
.pipe(writeStream)
|
|
300
|
+
.on("error", (error) => {
|
|
301
|
+
reject(error);
|
|
302
|
+
})
|
|
303
|
+
.on("close", () => {
|
|
304
|
+
filePath = path.join(process.cwd(), fileName);
|
|
305
|
+
resolve({ isTemporary: true, path: filePath });
|
|
306
|
+
});
|
|
307
|
+
for (let i = 0; i < files.length; ++i) {
|
|
308
|
+
const file = files[i];
|
|
309
|
+
// yazl does not like backslash (\) in the metadata path.
|
|
310
|
+
const relativePath = slash(path.relative(baseDirectoryPath, file));
|
|
311
|
+
zipFile.addFile(file, relativePath);
|
|
312
|
+
}
|
|
313
|
+
zipFile.end();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
getPackageFilePromise = Q({ isTemporary: false, path: filePath });
|
|
319
|
+
}
|
|
320
|
+
return getPackageFilePromise;
|
|
321
|
+
}
|
|
322
|
+
generateRandomFilename(length) {
|
|
323
|
+
let filename = "";
|
|
324
|
+
const validChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
325
|
+
for (let i = 0; i < length; i++) {
|
|
326
|
+
filename += validChar.charAt(Math.floor(Math.random() * validChar.length));
|
|
327
|
+
}
|
|
328
|
+
return filename;
|
|
329
|
+
}
|
|
330
|
+
get(endpoint, expectResponseBody = true) {
|
|
331
|
+
return this.makeApiRequest("get", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null);
|
|
332
|
+
}
|
|
333
|
+
post(endpoint, requestBody, expectResponseBody, contentType = "application/json;charset=UTF-8") {
|
|
334
|
+
return this.makeApiRequest("post", endpoint, requestBody, expectResponseBody, contentType);
|
|
335
|
+
}
|
|
336
|
+
patch(endpoint, requestBody, expectResponseBody = false, contentType = "application/json;charset=UTF-8") {
|
|
337
|
+
return this.makeApiRequest("patch", endpoint, requestBody, expectResponseBody, contentType);
|
|
338
|
+
}
|
|
339
|
+
del(endpoint, expectResponseBody = false) {
|
|
340
|
+
return this.makeApiRequest("del", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null);
|
|
341
|
+
}
|
|
342
|
+
makeApiRequest(method, endpoint, requestBody, expectResponseBody, contentType) {
|
|
343
|
+
return Promise((resolve, reject, notify) => {
|
|
344
|
+
let request = superagent[method](this._serverUrl + endpoint);
|
|
345
|
+
this.attachCredentials(request);
|
|
346
|
+
if (requestBody) {
|
|
347
|
+
if (contentType) {
|
|
348
|
+
request = request.set("Content-Type", contentType);
|
|
349
|
+
}
|
|
350
|
+
request = request.send(requestBody);
|
|
351
|
+
}
|
|
352
|
+
request.end((err, res) => {
|
|
353
|
+
if (err) {
|
|
354
|
+
reject(this.getCodePushError(err, res));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
let body;
|
|
358
|
+
try {
|
|
359
|
+
body = JSON.parse(res.text);
|
|
360
|
+
}
|
|
361
|
+
catch (err) { }
|
|
362
|
+
if (res.ok) {
|
|
363
|
+
if (expectResponseBody && !body) {
|
|
364
|
+
reject({
|
|
365
|
+
message: `Could not parse response: ${res.text}`,
|
|
366
|
+
statusCode: AccountManager.ERROR_INTERNAL_SERVER,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
resolve({
|
|
371
|
+
headers: res.header,
|
|
372
|
+
body: body,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
if (body) {
|
|
378
|
+
reject({
|
|
379
|
+
message: body.message,
|
|
380
|
+
statusCode: this.getErrorStatus(err, res),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
reject({
|
|
385
|
+
message: res.text,
|
|
386
|
+
statusCode: this.getErrorStatus(err, res),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
getCodePushError(error, response) {
|
|
394
|
+
if (error.syscall === "getaddrinfo") {
|
|
395
|
+
error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`;
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
message: this.getErrorMessage(error, response),
|
|
399
|
+
statusCode: this.getErrorStatus(error, response),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
getErrorStatus(error, response) {
|
|
403
|
+
return (error && error.status) || (response && response.status) || AccountManager.ERROR_GATEWAY_TIMEOUT;
|
|
404
|
+
}
|
|
405
|
+
getErrorMessage(error, response) {
|
|
406
|
+
return response && response.text ? response.text : error.message;
|
|
407
|
+
}
|
|
408
|
+
attachCredentials(request) {
|
|
409
|
+
if (this._customHeaders) {
|
|
410
|
+
for (const headerName in this._customHeaders) {
|
|
411
|
+
request.set(headerName, this._customHeaders[headerName]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
request.set("Accept", `application/vnd.code-push.v${AccountManager.API_VERSION}+json`);
|
|
415
|
+
request.set("Authorization", `Bearer ${this._accessKey}`);
|
|
416
|
+
request.set("X-CodePush-SDK-Version", packageJson.version);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
module.exports = AccountManager;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getReactNativeVersion = exports.directoryExistsSync = exports.getiOSHermesEnabled = exports.getAndroidHermesEnabled = exports.runHermesEmitBinaryCommand = exports.isValidVersion = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const childProcess = require("child_process");
|
|
8
|
+
const semver_1 = require("semver");
|
|
9
|
+
const file_utils_1 = require("./utils/file-utils");
|
|
10
|
+
const g2js = require("gradle-to-js/lib/parser");
|
|
11
|
+
function isValidVersion(version) {
|
|
12
|
+
return !!(0, semver_1.valid)(version) || /^\d+\.\d+$/.test(version);
|
|
13
|
+
}
|
|
14
|
+
exports.isValidVersion = isValidVersion;
|
|
15
|
+
async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOutput, extraHermesFlags, gradleFile) {
|
|
16
|
+
const hermesArgs = [];
|
|
17
|
+
const envNodeArgs = process.env.CODE_PUSH_NODE_ARGS;
|
|
18
|
+
if (typeof envNodeArgs !== "undefined") {
|
|
19
|
+
Array.prototype.push.apply(hermesArgs, envNodeArgs.trim().split(/\s+/));
|
|
20
|
+
}
|
|
21
|
+
Array.prototype.push.apply(hermesArgs, [
|
|
22
|
+
"-emit-binary",
|
|
23
|
+
"-out",
|
|
24
|
+
path.join(outputFolder, bundleName + ".hbc"),
|
|
25
|
+
path.join(outputFolder, bundleName),
|
|
26
|
+
...extraHermesFlags,
|
|
27
|
+
]);
|
|
28
|
+
if (sourcemapOutput) {
|
|
29
|
+
hermesArgs.push("-output-source-map");
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
|
|
32
|
+
const hermesCommand = await getHermesCommand(gradleFile);
|
|
33
|
+
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
|
|
34
|
+
console.log(`${hermesCommand} ${hermesArgs.join(" ")}`);
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
hermesProcess.stdout.on("data", (data) => {
|
|
37
|
+
console.log(data.toString().trim());
|
|
38
|
+
});
|
|
39
|
+
hermesProcess.stderr.on("data", (data) => {
|
|
40
|
+
console.error(data.toString().trim());
|
|
41
|
+
});
|
|
42
|
+
hermesProcess.on("close", (exitCode, signal) => {
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`));
|
|
45
|
+
}
|
|
46
|
+
// Copy HBC bundle to overwrite JS bundle
|
|
47
|
+
const source = path.join(outputFolder, bundleName + ".hbc");
|
|
48
|
+
const destination = path.join(outputFolder, bundleName);
|
|
49
|
+
fs.copyFile(source, destination, (err) => {
|
|
50
|
+
if (err) {
|
|
51
|
+
console.error(err);
|
|
52
|
+
reject(new Error(`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`));
|
|
53
|
+
}
|
|
54
|
+
fs.unlink(source, (err) => {
|
|
55
|
+
if (err) {
|
|
56
|
+
console.error(err);
|
|
57
|
+
reject(err);
|
|
58
|
+
}
|
|
59
|
+
resolve(null);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}).then(() => {
|
|
64
|
+
if (!sourcemapOutput) {
|
|
65
|
+
// skip source map compose if source map is not enabled
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const composeSourceMapsPath = getComposeSourceMapsPath();
|
|
69
|
+
if (!composeSourceMapsPath) {
|
|
70
|
+
throw new Error("react-native compose-source-maps.js scripts is not found");
|
|
71
|
+
}
|
|
72
|
+
const jsCompilerSourceMapFile = path.join(outputFolder, bundleName + ".hbc" + ".map");
|
|
73
|
+
if (!fs.existsSync(jsCompilerSourceMapFile)) {
|
|
74
|
+
throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`);
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const composeSourceMapsArgs = [composeSourceMapsPath, sourcemapOutput, jsCompilerSourceMapFile, "-o", sourcemapOutput];
|
|
78
|
+
// https://github.com/facebook/react-native/blob/master/react.gradle#L211
|
|
79
|
+
// https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178
|
|
80
|
+
// packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map
|
|
81
|
+
const composeSourceMapsProcess = childProcess.spawn("node", composeSourceMapsArgs);
|
|
82
|
+
console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(" ")}`);
|
|
83
|
+
composeSourceMapsProcess.stdout.on("data", (data) => {
|
|
84
|
+
console.log(data.toString().trim());
|
|
85
|
+
});
|
|
86
|
+
composeSourceMapsProcess.stderr.on("data", (data) => {
|
|
87
|
+
console.error(data.toString().trim());
|
|
88
|
+
});
|
|
89
|
+
composeSourceMapsProcess.on("close", (exitCode, signal) => {
|
|
90
|
+
if (exitCode !== 0) {
|
|
91
|
+
reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`));
|
|
92
|
+
}
|
|
93
|
+
// Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well
|
|
94
|
+
fs.unlink(jsCompilerSourceMapFile, (err) => {
|
|
95
|
+
if (err) {
|
|
96
|
+
console.error(err);
|
|
97
|
+
reject(err);
|
|
98
|
+
}
|
|
99
|
+
resolve(null);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
exports.runHermesEmitBinaryCommand = runHermesEmitBinaryCommand;
|
|
106
|
+
function parseBuildGradleFile(gradleFile) {
|
|
107
|
+
let buildGradlePath = path.join("android", "app");
|
|
108
|
+
if (gradleFile) {
|
|
109
|
+
buildGradlePath = gradleFile;
|
|
110
|
+
}
|
|
111
|
+
if (fs.lstatSync(buildGradlePath).isDirectory()) {
|
|
112
|
+
buildGradlePath = path.join(buildGradlePath, "build.gradle");
|
|
113
|
+
}
|
|
114
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(buildGradlePath)) {
|
|
115
|
+
throw new Error(`Unable to find gradle file "${buildGradlePath}".`);
|
|
116
|
+
}
|
|
117
|
+
return g2js.parseFile(buildGradlePath).catch(() => {
|
|
118
|
+
throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function getHermesCommandFromGradle(gradleFile) {
|
|
122
|
+
const buildGradle = await parseBuildGradleFile(gradleFile);
|
|
123
|
+
const hermesCommandProperty = Array.from(buildGradle["project.ext.react"] || []).find((prop) => prop.trim().startsWith("hermesCommand:"));
|
|
124
|
+
if (hermesCommandProperty) {
|
|
125
|
+
return hermesCommandProperty.replace("hermesCommand:", "").trim().slice(1, -1);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getAndroidHermesEnabled(gradleFile) {
|
|
132
|
+
return parseBuildGradleFile(gradleFile).then((buildGradle) => {
|
|
133
|
+
return Array.from(buildGradle["project.ext.react"] || []).some((line) => /^enableHermes\s{0,}:\s{0,}true/.test(line));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
exports.getAndroidHermesEnabled = getAndroidHermesEnabled;
|
|
137
|
+
function getiOSHermesEnabled(podFile) {
|
|
138
|
+
let podPath = path.join("ios", "Podfile");
|
|
139
|
+
if (podFile) {
|
|
140
|
+
podPath = podFile;
|
|
141
|
+
}
|
|
142
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(podPath)) {
|
|
143
|
+
throw new Error(`Unable to find Podfile file "${podPath}".`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const podFileContents = fs.readFileSync(podPath).toString();
|
|
147
|
+
return /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.getiOSHermesEnabled = getiOSHermesEnabled;
|
|
154
|
+
function getHermesOSBin() {
|
|
155
|
+
switch (process.platform) {
|
|
156
|
+
case "win32":
|
|
157
|
+
return "win64-bin";
|
|
158
|
+
case "darwin":
|
|
159
|
+
return "osx-bin";
|
|
160
|
+
case "freebsd":
|
|
161
|
+
case "linux":
|
|
162
|
+
case "sunos":
|
|
163
|
+
default:
|
|
164
|
+
return "linux64-bin";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function getHermesOSExe() {
|
|
168
|
+
const react63orAbove = (0, semver_1.compare)((0, semver_1.coerce)(getReactNativeVersion()).version, "0.63.0") !== -1;
|
|
169
|
+
const hermesExecutableName = react63orAbove ? "hermesc" : "hermes";
|
|
170
|
+
switch (process.platform) {
|
|
171
|
+
case "win32":
|
|
172
|
+
return hermesExecutableName + ".exe";
|
|
173
|
+
default:
|
|
174
|
+
return hermesExecutableName;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function getHermesCommand(gradleFile) {
|
|
178
|
+
const fileExists = (file) => {
|
|
179
|
+
try {
|
|
180
|
+
return fs.statSync(file).isFile();
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
// Hermes is bundled with react-native since 0.69
|
|
187
|
+
const bundledHermesEngine = path.join(getReactNativePackagePath(), "sdks", "hermesc", getHermesOSBin(), getHermesOSExe());
|
|
188
|
+
if (fileExists(bundledHermesEngine)) {
|
|
189
|
+
return bundledHermesEngine;
|
|
190
|
+
}
|
|
191
|
+
const gradleHermesCommand = await getHermesCommandFromGradle(gradleFile);
|
|
192
|
+
if (gradleHermesCommand) {
|
|
193
|
+
return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin()));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// assume if hermes-engine exists it should be used instead of hermesvm
|
|
197
|
+
const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe());
|
|
198
|
+
if (fileExists(hermesEngine)) {
|
|
199
|
+
return hermesEngine;
|
|
200
|
+
}
|
|
201
|
+
return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function getComposeSourceMapsPath() {
|
|
205
|
+
// detect if compose-source-maps.js script exists
|
|
206
|
+
const composeSourceMaps = path.join(getReactNativePackagePath(), "scripts", "compose-source-maps.js");
|
|
207
|
+
if (fs.existsSync(composeSourceMaps)) {
|
|
208
|
+
return composeSourceMaps;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function getReactNativePackagePath() {
|
|
213
|
+
const result = childProcess.spawnSync("node", ["--print", "require.resolve('react-native/package.json')"]);
|
|
214
|
+
const packagePath = path.dirname(result.stdout.toString());
|
|
215
|
+
if (result.status === 0 && directoryExistsSync(packagePath)) {
|
|
216
|
+
return packagePath;
|
|
217
|
+
}
|
|
218
|
+
return path.join("node_modules", "react-native");
|
|
219
|
+
}
|
|
220
|
+
function directoryExistsSync(dirname) {
|
|
221
|
+
try {
|
|
222
|
+
return fs.statSync(dirname).isDirectory();
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
if (err.code !== "ENOENT") {
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
exports.directoryExistsSync = directoryExistsSync;
|
|
232
|
+
function getReactNativeVersion() {
|
|
233
|
+
let packageJsonFilename;
|
|
234
|
+
let projectPackageJson;
|
|
235
|
+
try {
|
|
236
|
+
packageJsonFilename = path.join(process.cwd(), "package.json");
|
|
237
|
+
projectPackageJson = JSON.parse(fs.readFileSync(packageJsonFilename, "utf-8"));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
throw new Error(`Unable to find or read "package.json" in the CWD. The "release-react" command must be executed in a React Native project folder.`);
|
|
241
|
+
}
|
|
242
|
+
const projectName = projectPackageJson.name;
|
|
243
|
+
if (!projectName) {
|
|
244
|
+
throw new Error(`The "package.json" file in the CWD does not have the "name" field set.`);
|
|
245
|
+
}
|
|
246
|
+
return ((projectPackageJson.dependencies && projectPackageJson.dependencies["react-native"]) ||
|
|
247
|
+
(projectPackageJson.devDependencies && projectPackageJson.devDependencies["react-native"]));
|
|
248
|
+
}
|
|
249
|
+
exports.getReactNativeVersion = getReactNativeVersion;
|