Skip to content

Commit 9c71977

Browse files
authored
Implement package download helper and output channel progress bar (#2041)
* Implement package download helper and progress helper * Update
1 parent 7e8e3fc commit 9c71977

10 files changed

Lines changed: 209 additions & 6 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@
222222
"title": "%reactNative.command.openRNUpgradeHelper.title%",
223223
"category": "React Native"
224224
},
225+
{
226+
"command": "reactNative.installExpoGoApplication",
227+
"title": "%reactNative.command.installExpoGoApplication.title%",
228+
"category": "React Native"
229+
},
225230
{
226231
"command": "reactNative.showDevMenu",
227232
"title": "%reactNative.command.showDevMenu.title%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"reactNative.command.openEASProjectInWebPage.title": "Open the EAS project in a web page",
2222
"reactNative.command.revertOpenModule.title": "Revert extension input in open package module",
2323
"reactNative.command.openRNUpgradeHelper.title": "Open react native upgrade helper in web page",
24+
"reactNative.command.installExpoGoApplication.title": "Download and install Expo Go on simulator or device",
2425
"reactNative.command.showDevMenu.title": "Show Dev Menu",
2526
"reactNative.command.reloadApp.title": "Reload App",
2627
"reactNative.command.runInspector.title": "Run Element Inspector",

src/common/downloadHelper.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for details.
3+
import * as fs from "fs";
4+
import * as https from "https";
5+
import * as vscode from "vscode";
6+
import { OutputChannelLogger } from "../extension/log/OutputChannelLogger";
7+
8+
export async function downloadFile(url: any, targetFile: any) {
9+
const logger = OutputChannelLogger.getMainChannel();
10+
let progress = 0;
11+
let newProgress = 0;
12+
return await new Promise((resolve, reject) => {
13+
const request = https
14+
.get(url, response => {
15+
const code = response.statusCode ?? 0;
16+
17+
if (code >= 400) {
18+
return reject(new Error(response.statusMessage));
19+
}
20+
21+
const file = fs.createWriteStream(targetFile);
22+
const totalLength = parseInt(response.headers["content-length"] as string, 10);
23+
24+
response.pipe(file);
25+
response.on("data", async function (chunk) {
26+
newProgress += chunk.length;
27+
const currentProgress =
28+
parseFloat(getDownloadProgress(newProgress, totalLength)) * 100;
29+
if (currentProgress - progress >= 5) {
30+
progress = currentProgress;
31+
logger.logStream(
32+
`Current progress: ${currentProgress}%, please wait... \n`,
33+
);
34+
}
35+
});
36+
37+
file.on("finish", async () => {
38+
file.close();
39+
logger.logStream(`Download Expo Go Completed: ${targetFile as string}`);
40+
void vscode.window.showInformationMessage("Download Expo Go Completed.");
41+
});
42+
43+
response.on("end", function () {
44+
console.log("Progress end.");
45+
});
46+
})
47+
.on("error", error => {
48+
reject(error);
49+
});
50+
51+
request.end();
52+
});
53+
}
54+
55+
export async function downloadExpoGo(url: string, targetFile: string) {
56+
await downloadFile(url, targetFile);
57+
}
58+
59+
function getDownloadProgress(currentLength: number, totalLength: number): string {
60+
return (currentLength / totalLength).toFixed(2);
61+
}

src/common/error/errorStrings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ export const ERROR_STRINGS = {
154154
"FailedToOpenRNUpgradeHelper",
155155
"Failed to open react native upgrade helper",
156156
),
157+
[InternalErrorCode.FailedToInstallExpoGo]: localize(
158+
"FailedToInstallExpoGo",
159+
"Failed to download and install Expo Go application",
160+
),
157161
[InternalErrorCode.FailedToStartPackager]: localize(
158162
"FailedToStartPackager",
159163
"Failed to start the React Native packager",

src/common/error/internalErrorCode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum InternalErrorCode {
2525
FailedToOpenProjectPage = 120,
2626
FailedToRevertOpenModule = 121,
2727
FailedToOpenRNUpgradeHelper = 122,
28+
FailedToInstallExpoGo = 123,
2829

2930
// Device Deployer errors
3031
IOSDeployNotFound = 201,

src/common/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,14 @@ export async function wait(time?: number): Promise<void> {
9393
}, times);
9494
});
9595
}
96+
97+
export function getTimestamp(): string {
98+
const date = new Date(Date.now());
99+
const year = date.getFullYear();
100+
const month = date.getMonth() + 1;
101+
const time = `${date.getDate()}${String(date.getHours()).padStart(2, "0")}${String(
102+
date.getMinutes(),
103+
).padStart(2, "0")}${String(date.getSeconds()).padStart(2, "0")}`;
104+
105+
return `${year}${month}${time}`;
106+
}

src/extension/commands/configEASBuild.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ErrorHelper } from "../../common/error/errorHelper";
88
import { InternalErrorCode } from "../../common/error/internalErrorCode";
99
import { CommandExecutor } from "../../common/commandExecutor";
1010
import { FileSystem } from "../../common/node/fileSystem";
11-
import { ExponentHelper } from "../exponent/exponentHelper";
1211
import { ReactNativeCommand } from "./util/reactNativeCommand";
1312

1413
nls.config({
@@ -26,8 +25,9 @@ export class ConfigEASBuild extends ReactNativeCommand {
2625

2726
async baseFn(): Promise<void> {
2827
assert(this.project);
29-
const projectRootPath = this.project.getWorkspaceFolder().uri.fsPath;
30-
const expoHelper = new ExponentHelper(projectRootPath, projectRootPath);
28+
const projectRootPath = this.project.getPackager().getProjectPath();
29+
const expoHelper = this.project.getExponentHelper();
30+
logger.info(localize("CheckExpoEnvironment", "Checking Expo project environment."));
3131
const isExpo = await expoHelper.isExpoManagedApp(true);
3232

3333
if (isExpo) {

src/extension/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from "./configEASBuild";
2525
export * from "./openEASProject";
2626
export * from "./revertOpenModule";
2727
export * from "./openRNUpgradeHelper";
28+
export * from "./installExpoGoApplication";
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for details.
3+
4+
import * as assert from "assert";
5+
import * as https from "https";
6+
import * as os from "os";
7+
import * as vscode from "vscode";
8+
import * as nls from "vscode-nls";
9+
import { OutputChannelLogger } from "../log/OutputChannelLogger";
10+
import { ErrorHelper } from "../../common/error/errorHelper";
11+
import { InternalErrorCode } from "../../common/error/internalErrorCode";
12+
import { downloadExpoGo } from "../../common/downloadHelper";
13+
import { getTimestamp } from "../../common/utils";
14+
import { Command } from "./util/command";
15+
16+
nls.config({
17+
messageFormat: nls.MessageFormat.bundle,
18+
bundleFormat: nls.BundleFormat.standalone,
19+
})();
20+
const localize = nls.loadMessageBundle();
21+
const logger = OutputChannelLogger.getMainChannel();
22+
23+
export class InstallExpoGoApplication extends Command {
24+
codeName = "installExpoGoApplication";
25+
label = "Download and install Expo Go on simulator or device";
26+
error = ErrorHelper.getInternalError(InternalErrorCode.FailedToInstallExpoGo);
27+
28+
async baseFn(): Promise<void> {
29+
assert(this.project);
30+
const item = await vscode.window.showQuickPick(["Android", "iOS"], {
31+
placeHolder: "Select type for mobile OS",
32+
});
33+
const expoHelper = this.project.getExponentHelper();
34+
logger.info(localize("CheckExpoEnvironment", "Checking Expo project environment."));
35+
const isExpo = await expoHelper.isExpoManagedApp(true);
36+
37+
const expoGoListAPI = "https://api.expo.dev/v2/versions";
38+
const apiJson = await fetchJson(expoGoListAPI);
39+
const jsonContent = JSON.parse(apiJson);
40+
41+
if (isExpo) {
42+
const currentSdkVersion = await expoHelper.exponentSdk(true);
43+
const expoUrlInfo = jsonContent.sdkVersions[currentSdkVersion];
44+
45+
if (item == "Android") {
46+
void vscode.window.showInformationMessage("Downloading Expo Go for Android.");
47+
logger.logStream(
48+
localize("DownloadAndroidExpoGo", "\nDownloading Expo Go for Android. \n"),
49+
);
50+
51+
const targetUrl = expoUrlInfo.androidClientUrl;
52+
const androidClientVersion = expoUrlInfo.androidClientVersion as string;
53+
try {
54+
await downloadExpoGo(
55+
targetUrl,
56+
`${this.project
57+
.getPackager()
58+
.getProjectPath()}/expogo_${androidClientVersion}_${getTimestamp()}.apk`,
59+
);
60+
} catch {
61+
throw new Error(
62+
localize("FailedToDownloadExpoGo", "Failed to download Expo Go."),
63+
);
64+
}
65+
} else if (item == "iOS") {
66+
if (os.platform() != "darwin") {
67+
logger.warning(
68+
localize(
69+
"NotDarwinPlatform",
70+
"Current OS may not support iOS installer. The Expo Go may not be installed.\n",
71+
),
72+
);
73+
}
74+
void vscode.window.showInformationMessage("Downloading Expo Go for iOS.");
75+
logger.logStream(
76+
localize("DownloadiOSExpoGo", "\nDownloading Expo Go for iOS. \n"),
77+
);
78+
79+
const targetUrl = expoUrlInfo.iosClientUrl;
80+
const iOSClientVersion = expoUrlInfo.iosClientVersion as string;
81+
try {
82+
await downloadExpoGo(
83+
targetUrl,
84+
`${this.project
85+
.getPackager()
86+
.getProjectPath()}/expogo_${iOSClientVersion}_${getTimestamp()}.tar.gz`,
87+
);
88+
} catch {
89+
throw new Error(
90+
localize("FailedToDownloadExpoGo", "Failed to download Expo Go."),
91+
);
92+
}
93+
} else {
94+
return;
95+
}
96+
} else {
97+
throw new Error(localize("NotExpoProject", "Current project is not Expo managed."));
98+
}
99+
}
100+
}
101+
102+
async function fetchJson(url: string): Promise<string> {
103+
return new Promise<string>((fulfill, reject) => {
104+
const requestOptions: https.RequestOptions = {};
105+
requestOptions.rejectUnauthorized = false; // CodeQL [js/disabling-certificate-validation] Debug extension does not need to verify certificate
106+
107+
const request = https.get(url, requestOptions, response => {
108+
let data = "";
109+
response.setEncoding("utf8");
110+
response.on("data", (chunk: string) => {
111+
data += chunk;
112+
});
113+
response.on("end", () => fulfill(data));
114+
response.on("error", reject);
115+
});
116+
117+
request.on("error", reject);
118+
request.end();
119+
});
120+
}

src/extension/commands/openEASProject.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as vscode from "vscode";
77
import { OutputChannelLogger } from "../log/OutputChannelLogger";
88
import { ErrorHelper } from "../../common/error/errorHelper";
99
import { InternalErrorCode } from "../../common/error/internalErrorCode";
10-
import { ExponentHelper } from "../exponent/exponentHelper";
1110
import { ReactNativeCommand } from "./util/reactNativeCommand";
1211

1312
nls.config({
@@ -25,8 +24,8 @@ export class OpenEASProject extends ReactNativeCommand {
2524

2625
async baseFn(): Promise<void> {
2726
assert(this.project);
28-
const projectRootPath = this.project.getWorkspaceFolder().uri.fsPath;
29-
const expoHelper = new ExponentHelper(projectRootPath, projectRootPath);
27+
const expoHelper = this.project.getExponentHelper();
28+
logger.info(localize("CheckExpoEnvironment", "Checking Expo project environment."));
3029
const isExpo = await expoHelper.isExpoManagedApp(true);
3130

3231
if (isExpo) {

0 commit comments

Comments
 (0)