Skip to content

Commit 5eba16f

Browse files
committed
Implement new binary installer
1 parent 1d59779 commit 5eba16f

File tree

4 files changed

+251
-574
lines changed

4 files changed

+251
-574
lines changed

install.mjs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { access, mkdir, writeFile, readFile, chmod } from "node:fs/promises";
2+
import { createWriteStream } from "node:fs";
3+
import follow_redirects from "follow-redirects";
4+
import { resolve } from "node:path";
5+
const { https } = follow_redirects;
6+
7+
const PREFIX = "[protoc-tools-grpc-web-plugin]";
8+
const DEFAULT_VERSION = "latest";
9+
10+
function unreachable() {
11+
console.error(`${PREFIX} ERROR: Reached an unreachable state!\n`)
12+
process.exit(1);
13+
}
14+
15+
async function exists(filename) {
16+
try {
17+
await access(filename);
18+
return true;
19+
} catch (err) {
20+
if (err.code === "ENOENT")
21+
return false;
22+
throw err;
23+
}
24+
}
25+
26+
function get_binary_filename() {
27+
if (process.platform === "win32")
28+
return "bin/protoc-gen-grpc-web.exe";
29+
return "bin/protoc-gen-grpc-web";
30+
}
31+
32+
function get_binary_download(version) {
33+
const asset = (() => {
34+
switch (process.platform) {
35+
case "linux": return "linux-x86_64"
36+
case "darwin": return "darwin-x86_64"
37+
case "win32": return "windows-x86_64.exe"
38+
default: unreachable();
39+
}
40+
})();
41+
42+
return `https://github.com/grpc/grpc-web/releases/download/${version}/protoc-gen-grpc-web-${version}-${asset}`;
43+
}
44+
45+
function get_releases() {
46+
return new Promise((resolve, reject) => {
47+
https.request("https://api.github.com/repos/grpc/grpc-web/releases", {
48+
headers: {
49+
Accept: "application/vnd.github.v3+json",
50+
"User-Agent": "protoc_tools_grpc_web_plugin"
51+
}
52+
}, (response) => {
53+
response.on("error", reject);
54+
55+
if (!(response.statusCode >= 200 && response.statusCode < 300)) {
56+
console.error(`\n${PREFIX} ERROR: Unable to fetch releases (${response.statusCode}). Please submit an issue at https://github.com/tscpp/protoc-tools-grpc-web-plugin/issues/new.`);
57+
return;
58+
}
59+
60+
let data = "";
61+
response.on("data", chunk => data += chunk.toString());
62+
response.on("close", () =>
63+
resolve(
64+
JSON.parse(data)
65+
.filter(release => release.assets.length > 0)
66+
)
67+
);
68+
})
69+
.on("error", reject)
70+
.end();
71+
});
72+
}
73+
74+
async function get_version(input) {
75+
let match;
76+
if (/^([0-9]+)\.([0-9]+)\.([0-9]+)$/.test(input)) /* exact */ {
77+
return input;
78+
} else {
79+
if (input === "latest" || input === "*" || input === "x") /* next stable version*/ {
80+
const releases = await get_releases();
81+
return releases.find(release => !release.prerelease)?.tag_name;
82+
} else if (input === "next") /* next version */ {
83+
const releases = await get_releases();
84+
return releases[0].tag_name;
85+
} else if (
86+
(match = /^~([0-9]+)\.([0-9]+)\.[0-9]+$/.exec(input))
87+
|| (match = /^([0-9]+)\.([0-9]+)(?:\.x)?$/.exec(input))
88+
) /* patch */ {
89+
const [major, minor] = match.slice(1);
90+
const releases = await get_releases();
91+
return releases.find(release =>
92+
release.tag_name.startsWith(`${major}.${minor}`)
93+
&& !release.prerelease
94+
)?.tag_name;
95+
} else if (
96+
(match = /^\^([0-9]+)\.([0-9]+)\.[0-9]+$/.exec(input))
97+
|| (match = /([0-9]+)(?:\.x)?/.exec(input))
98+
) /* minor */ {
99+
const [major] = match.slice(1);
100+
const releases = await get_releases();
101+
return releases.find(release =>
102+
release.tag_name.startsWith(major)
103+
&& !release.prerelease
104+
)?.tag_name;
105+
} else {
106+
// console.error(`${PREFIX} ERROR: Invalid version syntax ("${input}").\n`);
107+
// process.exit(1);
108+
return input;
109+
}
110+
}
111+
}
112+
113+
function format_version(version) {
114+
return /^[0-9]/.test(version) ? `v${version}` : version;
115+
}
116+
117+
async function get_input_version() {
118+
if (process.env.PROTOC_GEN_GRPC_WEB_VERSION)
119+
return process.env.PROTOC_GEN_GRPC_WEB_VERSION;
120+
121+
const pkg = await find_package(process.cwd());
122+
if (pkg?.config?.["protoc-gen-grpc-web-version"])
123+
return pkg.config["protoc-gen-grpc-web-version"];
124+
125+
return DEFAULT_VERSION;
126+
}
127+
128+
async function find_package(dir) {
129+
const path = resolve(dir, "package.json");
130+
131+
if (await exists(path))
132+
return JSON.parse(await readFile(path, "utf-8"));
133+
134+
let next;
135+
if ((next = resolve(dir, "..")) !== dir) {
136+
return await find_package(next);
137+
}
138+
}
139+
140+
if (process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_INSTALL) {
141+
console.log(`${PREFIX} NOTE: Enviroment variable PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_INSTALL was set. This will cause the installer to always exit.`);
142+
}
143+
144+
if (process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_CACHE) {
145+
console.log(`${PREFIX} NOTE: Enviroment variable PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_CACHE was set. This will cause the installer to always install the binary, whether or not the binary is already installed.`);
146+
}
147+
148+
if (process.env.PROTOC_GEN_GRPC_WEB_VERSION) {
149+
console.log(`${PREFIX} NOTE: Enviroment variable PROTOC_GEN_GRPC_WEB_VERSION was set. This will cause the installer to try install the version specified in the variable.`);
150+
}
151+
152+
if (
153+
process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_INSTALL
154+
|| process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_CACHE
155+
|| process.env.PROTOC_GEN_GRPC_WEB_VERSION
156+
) {
157+
console.log("");
158+
}
159+
160+
if (process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_INSTALL) {
161+
console.log(`${PREFIX} Skipping installation...`);
162+
process.exit(0);
163+
}
164+
165+
const SUPPORTED_PLATFORMS = new Set(["darwin", "linux", "win32"]);
166+
if (!SUPPORTED_PLATFORMS.has(process.platform)) {
167+
console.error(`${PREFIX} ERROR: Build for current platform (${process.platform}) is unavailable.\n`)
168+
process.exit(1);
169+
}
170+
171+
const input_version = await get_input_version();
172+
const version = await get_version(input_version);
173+
const formatted_version = format_version(version);
174+
const binary_filename = get_binary_filename();
175+
176+
if (await exists("bin")) {
177+
if (
178+
!process.env.PROTOC_TOOLS_GRPC_WEB_PLUGIN_NO_CACHE
179+
&& await exists("bin/version.txt")
180+
&& await exists(binary_filename)
181+
&& await readFile("bin/version.txt", "utf-8") === version
182+
) {
183+
// console.log(`${PREFIX} Binary is already installed. Skipping installation... `);
184+
process.exit(0);
185+
}
186+
} else {
187+
await mkdir("bin");
188+
}
189+
190+
const binary_dest = createWriteStream(binary_filename);
191+
const binary_download = get_binary_download(version);
192+
193+
process.stdout.write(`${PREFIX} Downloading binary ${formatted_version}...`);
194+
195+
await new Promise((resolve, reject) => {
196+
const request = https.request(binary_download, (response) => {
197+
if (!(response.statusCode >= 200 && response.statusCode < 300)) {
198+
console.error(`\n${PREFIX} ERROR: Unable to download binary from "${binary_download}" (${response.statusCode}).\n`);
199+
process.exit(1);
200+
}
201+
202+
response.on("error", (err) => {
203+
console.log("");
204+
reject(err);
205+
});
206+
207+
response.on("close", () => {
208+
console.log("");
209+
resolve()
210+
});
211+
212+
if (response.headers["content-length"]) {
213+
const length = parseInt(response.headers["content-length"]);
214+
let prevProgress = "0";
215+
let currentLength = 0;
216+
217+
process.stdout.write("\x1b[3D (0%)...");
218+
219+
response.on("data", chunk => {
220+
currentLength += chunk.length;
221+
const progress = (currentLength / length * 100).toFixed(0);
222+
process.stdout.write(`\x1b[${prevProgress.length + 5}D${progress}${prevProgress.length === progress.length ? "\x1b[5C" : "%)..."}`)
223+
prevProgress = progress;
224+
binary_dest.write(chunk);
225+
});
226+
} else {
227+
response.pipe(binary_dest);
228+
}
229+
});
230+
request.on("error", err => { throw err });
231+
request.end();
232+
});
233+
234+
await new Promise((resolve) => binary_dest.close(resolve));
235+
await chmod(binary_filename, "0775");
236+
await writeFile("bin/version.txt", version);

0 commit comments

Comments
 (0)