Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 55 additions & 14 deletions packages/app/scripts/utils/parseargs.mjs
Original file line number Diff line number Diff line change
@@ -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" };
Expand All @@ -17,36 +16,78 @@ 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<string, { short?: string; description: string; }>} 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<string, { short?: string; description: string; }>} options
* @returns {string}
*/
function formatHelp(description, options) {
const script = path.basename(process.argv[1]);
return [
`usage: ${script} [options]`,
"",
description,
"",
"Options:",
ui.toString(),
formatOptionsTable(options, process.stdout.columns ?? 80),
"",
].join("\n");
}
Expand Down
78 changes: 78 additions & 0 deletions packages/app/test/utils/parseargs.test.ts
Original file line number Diff line number Diff line change
@@ -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.",
]);
});
});
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading