fast-cli
Version:
Test your download and upload speed using fast.com
145 lines (144 loc) • 7.92 kB
JavaScript
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import dns from 'node:dns/promises';
import process from 'node:process';
import { useState, useEffect } from 'react';
import { Box, Text, Newline, useApp, useStdout, } from 'ink';
import Spinner from 'ink-spinner';
import api from './api.js';
import { convertToMbps } from './utilities.js';
const FixedSpacer = ({ size }) => _jsx(_Fragment, { children: ' '.repeat(size) });
const Spacer = ({ singleLine }) => (singleLine ? null : _jsx(Text, { children: _jsx(Newline, { count: 1 }) }));
const DownloadSpeed = ({ isDone, downloadSpeed, uploadSpeed, downloadUnit }) => {
const color = (isDone ?? uploadSpeed) ? 'green' : 'cyan';
return (_jsxs(Text, { color: color, children: [downloadSpeed, _jsx(FixedSpacer, { size: 1 }), _jsx(Text, { dimColor: true, children: downloadUnit }), _jsx(FixedSpacer, { size: 1 }), "\u2193"] }));
};
const UploadSpeed = ({ isDone, uploadSpeed, uploadUnit }) => {
const color = isDone ? 'green' : 'cyan';
if (uploadSpeed) {
return (_jsxs(Text, { color: color, children: [uploadSpeed, _jsx(Text, { dimColor: true, children: ` ${uploadUnit} ↑` })] }));
}
return _jsx(Text, { dimColor: true, color: color, children: ' - Mbps ↑' });
};
const Speed = ({ upload, data }) => upload ? (_jsxs(_Fragment, { children: [_jsx(DownloadSpeed, { ...data }), _jsx(Text, { dimColor: true, children: ' / ' }), _jsx(UploadSpeed, { ...data })] })) : (_jsx(DownloadSpeed, { ...data }));
const VerboseInfo = ({ data, singleLine }) => {
const hasLatencyData = data.latency !== undefined || data.bufferBloat !== undefined;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasClientData = Boolean(data.userLocation || data.userIp);
return (_jsxs(_Fragment, { children: [!singleLine && _jsx(Newline, {}), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Box, { children: [_jsx(Text, { children: _jsx(FixedSpacer, { size: 4 }) }), _jsx(Text, { dimColor: true, children: "Latency: " }), hasLatencyData ? (_jsxs(_Fragment, { children: [data.latency !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'white', children: data.latency }), _jsx(Text, { dimColor: true, children: " ms (unloaded)" })] })), data.latency !== undefined && data.bufferBloat !== undefined && (_jsx(Text, { dimColor: true, children: " / " })), data.bufferBloat !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'white', children: data.bufferBloat }), _jsx(Text, { dimColor: true, children: " ms (loaded)" })] }))] })) : (_jsx(Text, { dimColor: true, children: "Measuring..." }))] }), _jsxs(Box, { children: [_jsx(Text, { children: _jsx(FixedSpacer, { size: 4 }) }), _jsx(Text, { dimColor: true, children: "Client: " }), hasClientData ? (_jsxs(_Fragment, { children: [data.userLocation && (_jsx(Text, { color: 'white', children: data.userLocation })), data.userLocation && data.userIp && (_jsx(Text, { dimColor: true, children: " \u2022 " })), data.userIp && (_jsx(Text, { color: 'white', children: data.userIp }))] })) : (_jsx(Text, { dimColor: true, children: "Detecting..." }))] })] })] }));
};
function formatVerboseText(data) {
const lines = [];
if (data.latency !== undefined || data.bufferBloat !== undefined) {
let latencyLine = 'Latency: ';
if (data.latency !== undefined) {
latencyLine += `${data.latency} ms (unloaded)`;
}
if (data.latency !== undefined && data.bufferBloat !== undefined) {
latencyLine += ' / ';
}
if (data.bufferBloat !== undefined) {
latencyLine += `${data.bufferBloat} ms (loaded)`;
}
lines.push(latencyLine);
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (data.userLocation || data.userIp) {
let clientLine = 'Client: ';
if (data.userLocation) {
clientLine += data.userLocation;
}
if (data.userLocation && data.userIp) {
clientLine += ' • ';
}
if (data.userIp) {
clientLine += data.userIp;
}
lines.push(clientLine);
}
return lines;
}
function createJsonOutput(data, upload) {
return {
downloadSpeed: convertToMbps(data.downloadSpeed ?? 0, data.downloadUnit ?? 'Mbps'),
uploadSpeed: upload ? convertToMbps(data.uploadSpeed ?? 0, data.uploadUnit ?? 'Mbps') : undefined,
downloadUnit: 'Mbps',
uploadUnit: upload ? 'Mbps' : undefined,
downloaded: data.downloaded,
uploaded: data.uploaded,
latency: data.latency,
bufferBloat: data.bufferBloat,
userLocation: data.userLocation,
userIp: data.userIp,
};
}
function formatTextOutput(data, upload, verbose) {
let output = `${data.downloadSpeed ?? 0} ${data.downloadUnit ?? 'Mbps'}`;
if (upload && data.uploadSpeed) {
output += `\n${data.uploadSpeed} ${data.uploadUnit ?? 'Mbps'}`;
}
if (verbose) {
const verboseLines = formatVerboseText(data);
if (verboseLines.length > 0) {
output += '\n\n' + verboseLines.join('\n');
}
}
return output;
}
const Ui = ({ singleLine, upload, json, verbose }) => {
const [data, setData] = useState({});
const [isDone, setIsDone] = useState(false);
const { exit } = useApp();
const { write } = useStdout();
useEffect(() => {
(async () => {
try {
await dns.lookup('fast.com');
}
catch (error) {
const message = error.code === 'ENOTFOUND'
? 'Please check your internet connection'
: 'Failed to connect to fast.com';
process.stderr.write(message + '\n');
process.exit(1);
}
try {
for await (const result of api({ measureUpload: upload })) {
setData(result);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
process.stderr.write(errorMessage + '\n');
process.exit(1);
}
})();
}, [upload]);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (data.isDone || (!upload && data.uploadSpeed)) {
setIsDone(true);
}
}, [data.isDone, upload, data.uploadSpeed]);
useEffect(() => {
if (!isDone) {
return;
}
if (json) {
const jsonData = createJsonOutput(data, Boolean(upload));
write(JSON.stringify(jsonData, (_key, value) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
value === undefined ? undefined : value, '\t'));
}
else if (!process.stdout.isTTY) {
write(formatTextOutput(data, Boolean(upload), Boolean(verbose)));
}
exit();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDone, exit]);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (json || !process.stdout.isTTY) {
return null;
}
return (_jsxs(_Fragment, { children: [_jsx(Spacer, { singleLine: singleLine }), _jsxs(Box, { children: [!isDone && (_jsxs(_Fragment, { children: [!singleLine && _jsx(Text, { children: _jsx(FixedSpacer, { size: 2 }) }), _jsx(Text, { color: 'cyan', children: _jsx(Spinner, {}) }), _jsx(Text, { children: _jsx(FixedSpacer, { size: 1 }) })] })), isDone && _jsx(Text, { children: _jsx(FixedSpacer, { size: 4 }) }), Object.keys(data).length > 0 && _jsx(Speed, { upload: upload, data: data })] }), verbose && _jsx(VerboseInfo, { data: data, singleLine: singleLine }), _jsx(Spacer, { singleLine: singleLine })] }));
};
export default Ui;