From 83f2feb2ea1eaa52ab63b4eec02b5e767b7dce11 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:54:38 +0200 Subject: [PATCH] fix: replace `@isaacs/cliui` dependency --- packages/app/package.json | 1 - packages/app/scripts/utils/parseargs.mjs | 69 ++++++++++++++++---- packages/app/test/utils/parseargs.test.ts | 78 +++++++++++++++++++++++ yarn.lock | 1 - 4 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 packages/app/test/utils/parseargs.test.ts diff --git a/packages/app/package.json b/packages/app/package.json index 8b0ebc6c5..c42df2155 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -86,7 +86,6 @@ "test:rb": "bundle exec ruby -Ilib:test -e \"Dir.glob('./test/test_*.rb').each { |file| require(file) }\"" }, "dependencies": { - "@isaacs/cliui": "^9.0.0", "@rnx-kit/react-native-host": "^0.5.21", "@rnx-kit/tools-react-native": "^2.1.0", "ajv": "^8.0.0", diff --git a/packages/app/scripts/utils/parseargs.mjs b/packages/app/scripts/utils/parseargs.mjs index 49518769a..e98b55962 100644 --- a/packages/app/scripts/utils/parseargs.mjs +++ b/packages/app/scripts/utils/parseargs.mjs @@ -1,5 +1,4 @@ // @ts-check -import { cliui } from "@isaacs/cliui"; import * as path from "node:path"; import * as util from "node:util"; import manifest from "../../package.json" with { type: "json" }; @@ -17,28 +16,70 @@ function coerce(values, _options) { } /** - * Generates help message. - * @param {string} description + * Wraps plain text at the specified width. + * @note Words longer than specified width are not broken up and may overflow. + * @param {string} text + * @param {number} columns + * @returns {string[]} + */ +export function wordWrap(text, columns) { + const lines = []; + + const length = text.length; + let lineStart = 0; + let wordEnd = 0; + + for (let i = 0; i < length; ++i) { + if (wordEnd > lineStart && i - lineStart > columns) { + lines.push(text.slice(lineStart, wordEnd)); + lineStart = wordEnd + 1; + wordEnd = lineStart; + } else if (text[i] === " ") { + wordEnd = i; + } + } + + lines.push(text.slice(lineStart, length)); + return lines; +} + +/** + * Formats options for help message. * @param {Record} options + * @param {number} columns * @returns {string} */ -function formatHelp(description, options) { +export function formatOptionsTable(options, columns) { + /** @type {string[]} */ + const lines = []; + const flags = Object.entries(options); const indent = " "; - const minWidth = - Math.max(...flags.map(([flag]) => flag.length)) + indent.length * 2; - const padding = [0, 0, 0, 0]; + const minFlagLength = Math.max(...flags.map(([flag]) => flag.length)); + // ␣␣-f,␣--flag␣␣␣␣description/usage of flag + // ⇤── minCols ──⇥⇤─── descriptionCols ───⇥ + const minCols = 8 + minFlagLength + indent.length * 2; + const descriptionCols = columns - minCols; - const ui = cliui({ width: process.stdout.columns ?? 80 }); for (const [flag, config] of flags) { - ui.div( - { text: "", width: 2, padding }, - { text: config.short ? `-${config.short},` : "", width: 4, padding }, - { text: `--${flag}`, width: minWidth + 2, padding }, - { text: config.description, padding } + const short = config.short ? `-${config.short}, ` : " "; + const [first, ...rest] = wordWrap(config.description, descriptionCols); + lines.push( + `${indent}${short}--${flag.padEnd(minFlagLength)}${indent}${indent}${first}`, + ...rest.map((line) => line.padStart(minCols + line.length)) ); } + return lines.join("\n"); +} + +/** + * Generates help message. + * @param {string} description + * @param {Record} options + * @returns {string} + */ +function formatHelp(description, options) { const script = path.basename(process.argv[1]); return [ `usage: ${script} [options]`, @@ -46,7 +87,7 @@ function formatHelp(description, options) { description, "", "Options:", - ui.toString(), + formatOptionsTable(options, process.stdout.columns ?? 80), "", ].join("\n"); } diff --git a/packages/app/test/utils/parseargs.test.ts b/packages/app/test/utils/parseargs.test.ts new file mode 100644 index 000000000..9a5cd9d06 --- /dev/null +++ b/packages/app/test/utils/parseargs.test.ts @@ -0,0 +1,78 @@ +import { deepEqual, equal } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + formatOptionsTable, + wordWrap, +} from "../../scripts/utils/parseargs.mjs"; + +describe("wordWrap()", () => { + it("returns the text unchanged when it fits on one line", () => { + const text = "hello world"; + deepEqual(wordWrap(text, text.length), [text]); + }); + + it("wraps text greedily across multiple lines", () => { + deepEqual(wordWrap("the quick brown fox jumps", 10), [ + "the quick", + "brown fox", + "jumps", + ]); + }); + + it("does not break up words longer than the specified width", () => { + deepEqual(wordWrap("supercalifragilisticexpialidocious", 10), [ + "supercalifragilisticexpialidocious", + ]); + }); + + it("returns a single empty line for empty text", () => { + deepEqual(wordWrap("", 10), [""]); + }); +}); + +describe("formatOptionsTable()", () => { + const options = { + help: { + description: "Show this help message", + short: "h", + }, + version: { + description: "Show version number", + short: "v", + }, + platform: { + description: + "Target platform. This is a long description that should wrap across multiple lines when the width is small enough.", + }, + }; + + it("renders short and long flags with descriptions", () => { + const lines = formatOptionsTable(options, 80).split("\n"); + deepEqual(lines, [ + " -h, --help Show this help message", + " -v, --version Show version number", + " --platform Target platform. This is a long description that should wrap", + " across multiple lines when the width is small enough.", + ]); + }); + + it("uses blank spaces instead of a short flag when not provided", () => { + const simpleOpts = { platform: { description: "Target platform" } }; + const lines = formatOptionsTable(simpleOpts, 80).split("\n"); + equal(lines[0], " --platform Target platform"); + }); + + it("wraps long descriptions and aligns continuation lines", () => { + const simpleOpts = { platform: options.platform }; + const lines = formatOptionsTable(simpleOpts, 40).split("\n"); + deepEqual(lines, [ + " --platform Target platform.", + " This is a long", + " description that", + " should wrap across", + " multiple lines when", + " the width is small", + " enough.", + ]); + }); +}); diff --git a/yarn.lock b/yarn.lock index 01143e869..c7d39815a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13396,7 +13396,6 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@expo/config-plugins": "npm:^57.0.0" - "@isaacs/cliui": "npm:^9.0.0" "@react-native-community/cli": "npm:^20.1.0" "@react-native-community/cli-types": "npm:^20.1.0" "@react-native-community/template": "npm:^0.85.0"