UNPKG

fast-cli

Version:

Test your download and upload speed using fast.com

145 lines (144 loc) 7.92 kB
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;