Skip to content

Commit 39c26cd

Browse files
committed
Optimize CLI runtime compatibility
1 parent 17d9aa6 commit 39c26cd

9 files changed

Lines changed: 326 additions & 34 deletions

File tree

bun.lock

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@
8383
"read": "^4.1.0",
8484
"registry-auth-token": "^5.1.1",
8585
"semver": "^7.7.4",
86-
"tcp-ping": "^0.1.1",
8786
"tty-table": "5.0",
8887
"yauzl": "^3.3.0",
8988
"yazl": "3.3.1"
@@ -105,7 +104,6 @@
105104
"@types/node-fetch": "^2.6.13",
106105
"@types/progress": "^2.0.7",
107106
"@types/semver": "^7.7.1",
108-
"@types/tcp-ping": "^0.1.6",
109107
"@types/yauzl": "^2.10.3",
110108
"@types/yazl": "^2.4.6",
111109
"typescript": "^6.0.2"

src/api.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import fs from 'fs';
44
import fetch from 'node-fetch';
55
import path from 'path';
66
import ProgressBar from 'progress';
7-
import tcpp from 'tcp-ping';
8-
import util from 'util';
97
import { getBaseUrl } from 'utils/http-helper';
108
import packageJson from '../package.json';
119
import type { Package, Session } from './types';
1210
import { credentialFile, IS_CRESC, pricingPageUrl } from './utils/constants';
1311
import { t } from './utils/i18n';
14-
15-
const tcpPing = util.promisify(tcpp.ping);
12+
import {
13+
measureTcpLatency,
14+
type RuntimeRequestInit,
15+
type RuntimeResponse,
16+
runtimeFetch,
17+
} from './utils/runtime';
1618

1719
let session: Session | undefined;
1820
let savedSession: Session | undefined;
@@ -85,12 +87,12 @@ function createRequestError(error: unknown, requestUrl: string) {
8587
return new Error(`${message}\nURL: ${requestUrl}`);
8688
}
8789

88-
async function query(url: string, options: fetch.RequestInit) {
90+
async function query(url: string, options: RuntimeRequestInit) {
8991
const baseUrl = await getBaseUrl;
9092
const fullUrl = `${baseUrl}${url}`;
91-
let resp: fetch.Response;
93+
let resp: RuntimeResponse;
9294
try {
93-
resp = await fetch(fullUrl, options);
95+
resp = await runtimeFetch(fullUrl, options);
9496
} catch (error) {
9597
throw createRequestError(error, fullUrl);
9698
}
@@ -166,13 +168,11 @@ export async function uploadFile(fn: string, key?: string) {
166168
if (global.USE_ACC_OSS) {
167169
realUrl = backupUrl;
168170
} else {
169-
const pingResult = await tcpPing({
170-
address: url.replace('https://', ''),
171+
const latency = await measureTcpLatency(url, {
171172
attempts: 4,
172173
timeout: 1000,
173174
});
174-
// console.log({pingResult});
175-
if (Number.isNaN(pingResult.avg) || pingResult.avg > 150) {
175+
if (!Number.isFinite(latency) || latency > 150) {
176176
realUrl = backupUrl;
177177
}
178178
}

src/bundle-runner.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { spawn, spawnSync } from 'child_process';
1+
import { spawnSync } from 'child_process';
22
import { satisfies } from 'compare-versions';
33
import * as fs from 'fs-extra';
44
import os from 'os';
55
import path from 'path';
66
import { t } from './utils/i18n';
7+
import {
8+
getJavaScriptRuntime,
9+
spawnJavaScript,
10+
spawnJavaScriptSync,
11+
} from './utils/runtime';
712

813
const g2js = require('gradle-to-js/lib/parser');
914
const properties = require('properties');
@@ -242,9 +247,10 @@ export async function runReactNativeBundleCommand({
242247
reactNativeBundleArgs.push('--config', config);
243248
}
244249

245-
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
250+
const jsRuntime = getJavaScriptRuntime();
251+
const reactNativeBundleProcess = spawnJavaScript(reactNativeBundleArgs);
246252
console.log(
247-
`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`,
253+
`Running bundle command: ${jsRuntime} ${reactNativeBundleArgs.join(' ')}`,
248254
);
249255

250256
await new Promise<void>((resolve, reject) => {
@@ -463,8 +469,7 @@ async function compileHermesByteCode(
463469
return;
464470
}
465471
console.log(t('composingSourceMap'));
466-
spawnSync(
467-
'node',
472+
spawnJavaScriptSync(
468473
[
469474
composerPath,
470475
path.join(outputFolder, `${bundleName}.txt.map`),
@@ -505,8 +510,7 @@ export async function copyDebugidForSentry(
505510
return;
506511
}
507512
console.log(t('copyingDebugId'));
508-
spawnSync(
509-
'node',
513+
spawnJavaScriptSync(
510514
[
511515
copyDebugidPath,
512516
path.join(outputFolder, `${bundleName}.txt.map`),
@@ -544,8 +548,7 @@ export async function uploadSourcemapForSentry(
544548
return;
545549
}
546550

547-
spawnSync(
548-
'node',
551+
spawnJavaScriptSync(
549552
[sentryCliPath, 'releases', 'set-commits', version, '--auto'],
550553
{
551554
stdio: 'inherit',
@@ -554,8 +557,7 @@ export async function uploadSourcemapForSentry(
554557
console.log(t('sentryReleaseCreated', { version }));
555558

556559
console.log(t('uploadingSourcemap'));
557-
spawnSync(
558-
'node',
560+
spawnJavaScriptSync(
559561
[
560562
sentryCliPath,
561563
'releases',

src/install.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawnSync } from 'child_process';
22
import path from 'path';
33
import type { CommandContext } from './types';
4+
import { getInstallCommand } from './utils/runtime';
45

56
export const installCommands = {
67
install: async ({ args }: CommandContext) => {
@@ -9,8 +10,9 @@ export const installCommands = {
910
}
1011

1112
const cliDir = path.resolve(__dirname, '..');
13+
const installCommand = getInstallCommand(args, cliDir);
1214

13-
spawnSync('npm', ['install', ...args], {
15+
spawnSync(installCommand.command, installCommand.args, {
1416
cwd: cliDir,
1517
stdio: 'inherit',
1618
shell: true,

src/utils/http-helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import fetch from 'node-fetch';
21
import { defaultEndpoints } from './constants';
2+
import { runtimeFetch } from './runtime';
33

44
// const baseUrl = `http://localhost:9000`;
55
// let baseUrl = SERVER.main[0];
@@ -25,7 +25,7 @@ export function promiseAny<T>(promises: Promise<T>[]) {
2525
export const ping = async (url: string) => {
2626
let pingFinished = false;
2727
return Promise.race([
28-
fetch(url, {
28+
runtimeFetch(url, {
2929
method: 'HEAD',
3030
})
3131
.then(({ status }) => {

src/utils/runtime.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import {
2+
type ChildProcessWithoutNullStreams,
3+
type SpawnOptionsWithoutStdio,
4+
type SpawnSyncOptions,
5+
spawn,
6+
spawnSync,
7+
} from 'child_process';
8+
import fs from 'fs';
9+
import { createConnection } from 'net';
10+
import nodeFetch from 'node-fetch';
11+
import path from 'path';
12+
13+
export type RuntimeRequestInit = {
14+
method?: string;
15+
headers?: Record<string, string>;
16+
body?: any;
17+
signal?: AbortSignal;
18+
};
19+
20+
export type RuntimeResponse = {
21+
status: number;
22+
statusText: string;
23+
text: () => Promise<string>;
24+
};
25+
26+
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn';
27+
export type JavaScriptRuntime = 'bun' | 'node';
28+
29+
export const isBunRuntime =
30+
typeof (process.versions as NodeJS.ProcessVersions & { bun?: string }).bun ===
31+
'string';
32+
33+
export function runtimeFetch(
34+
url: string,
35+
options?: RuntimeRequestInit,
36+
): Promise<RuntimeResponse> {
37+
const fetchImpl =
38+
isBunRuntime && typeof globalThis.fetch === 'function'
39+
? globalThis.fetch.bind(globalThis)
40+
: nodeFetch;
41+
42+
return fetchImpl(url, options as any) as Promise<RuntimeResponse>;
43+
}
44+
45+
function resolveTcpTarget(input: string): { host: string; port: number } {
46+
try {
47+
const parsed = new URL(input);
48+
const port = parsed.port || (parsed.protocol === 'http:' ? '80' : '443');
49+
return {
50+
host: parsed.hostname,
51+
port: Number(port),
52+
};
53+
} catch {
54+
const [host, port] = input.split(':');
55+
return {
56+
host,
57+
port: port ? Number(port) : 443,
58+
};
59+
}
60+
}
61+
62+
function measureTcpConnectOnce(
63+
host: string,
64+
port: number,
65+
timeout: number,
66+
): Promise<number> {
67+
return new Promise((resolve) => {
68+
const startedAt = Date.now();
69+
const socket = createConnection({ host, port });
70+
71+
const finish = (latency: number) => {
72+
socket.removeAllListeners();
73+
socket.destroy();
74+
resolve(latency);
75+
};
76+
77+
socket.setTimeout(timeout);
78+
socket.once('connect', () => {
79+
finish(Date.now() - startedAt);
80+
});
81+
socket.once('timeout', () => {
82+
finish(Number.POSITIVE_INFINITY);
83+
});
84+
socket.once('error', () => {
85+
finish(Number.POSITIVE_INFINITY);
86+
});
87+
});
88+
}
89+
90+
export async function measureTcpLatency(
91+
input: string,
92+
{
93+
attempts = 4,
94+
timeout = 1000,
95+
}: {
96+
attempts?: number;
97+
timeout?: number;
98+
} = {},
99+
): Promise<number> {
100+
const { host, port } = resolveTcpTarget(input);
101+
const latencies: number[] = [];
102+
103+
for (let i = 0; i < attempts; i++) {
104+
const latency = await measureTcpConnectOnce(host, port, timeout);
105+
if (Number.isFinite(latency)) {
106+
latencies.push(latency);
107+
}
108+
}
109+
110+
if (latencies.length === 0) {
111+
return Number.POSITIVE_INFINITY;
112+
}
113+
return (
114+
latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length
115+
);
116+
}
117+
118+
export function detectPackageManager(
119+
cwd = process.cwd(),
120+
env: NodeJS.ProcessEnv = process.env,
121+
): PackageManager {
122+
const userAgent = env.npm_config_user_agent ?? '';
123+
if (userAgent.startsWith('bun/')) {
124+
return 'bun';
125+
}
126+
if (userAgent.startsWith('pnpm/')) {
127+
return 'pnpm';
128+
}
129+
if (userAgent.startsWith('yarn/')) {
130+
return 'yarn';
131+
}
132+
if (userAgent.startsWith('npm/')) {
133+
return 'npm';
134+
}
135+
136+
const lockFiles: Array<[string, PackageManager]> = [
137+
['bun.lock', 'bun'],
138+
['bun.lockb', 'bun'],
139+
['pnpm-lock.yaml', 'pnpm'],
140+
['yarn.lock', 'yarn'],
141+
['package-lock.json', 'npm'],
142+
];
143+
for (const [lockFile, manager] of lockFiles) {
144+
if (fs.existsSync(path.join(cwd, lockFile))) {
145+
return manager;
146+
}
147+
}
148+
149+
return isBunRuntime ? 'bun' : 'npm';
150+
}
151+
152+
export function getInstallCommand(
153+
installArgs: string[],
154+
cwd = process.cwd(),
155+
): { command: string; args: string[] } {
156+
const packageManager = detectPackageManager(cwd);
157+
if (packageManager === 'npm') {
158+
return { command: 'npm', args: ['install', ...installArgs] };
159+
}
160+
return { command: packageManager, args: ['add', ...installArgs] };
161+
}
162+
163+
export function getJavaScriptRuntime(
164+
env: NodeJS.ProcessEnv = process.env,
165+
): JavaScriptRuntime {
166+
const configured = env.RNU_JS_RUNTIME?.toLowerCase();
167+
if (configured === 'bun') {
168+
return 'bun';
169+
}
170+
if (configured === 'auto') {
171+
return isBunRuntime ? 'bun' : 'node';
172+
}
173+
return 'node';
174+
}
175+
176+
export function spawnJavaScript(
177+
args: string[],
178+
options?: SpawnOptionsWithoutStdio,
179+
env: NodeJS.ProcessEnv = process.env,
180+
): ChildProcessWithoutNullStreams {
181+
return spawn(getJavaScriptRuntime(env), args, options ?? {});
182+
}
183+
184+
export function spawnJavaScriptSync(
185+
args: string[],
186+
options?: SpawnSyncOptions,
187+
env: NodeJS.ProcessEnv = process.env,
188+
) {
189+
return spawnSync(getJavaScriptRuntime(env), args, options ?? {});
190+
}

0 commit comments

Comments
 (0)