Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/flat-pens-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arethetypeswrong/cli": minor
---

Automatically pick an output format that fits the terminal width (`--format auto`, the new default)
9 changes: 5 additions & 4 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ In the config file, `fromNpm` can be a boolean value.

#### Format

The format to print the output in. Defaults to `table`.
The format to print the output in. Defaults to `auto`.

The available values are:

- `table`
- `table-flipped`, where the resolution kinds are the table's head, and the entry points label the table's rows
- `table`, where columns are entrypoints and rows are resolution kinds
- `table-flipped`, where columns are resolution kinds and rows are entrypoints
- `ascii`, for large tables where the output is clunky
- `raw`, outputs the raw JSON data (overriding all other rendering options)
- `auto`, which picks whichever of the above best fits the terminal width
- `json` outputs the raw JSON data (overriding all other rendering options)

In the CLI: `--format`, `-f`

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import * as render from "./render/index.js";
const packageJson = createRequire(import.meta.url)("../package.json");
const version = packageJson.version;

const formats = ["table", "table-flipped", "ascii", "json"] as const;
const formats = ["auto", "table", "table-flipped", "ascii", "json"] as const;

type Format = (typeof formats)[number];

Expand Down Expand Up @@ -55,7 +55,7 @@ particularly ESM-related module resolution issues.`
)
.option("-P, --pack", "Run `npm pack` in the specified directory and delete the resulting .tgz file afterwards")
.option("-p, --from-npm", "Read from the npm registry instead of a local file")
.addOption(new Option("-f, --format <format>", "Specify the print format").choices(formats).default("table"))
.addOption(new Option("-f, --format <format>", "Specify the print format").choices(formats).default("auto"))
.option("-q, --quiet", "Don't print anything to STDOUT (overrides all other options)")
.option(
"--entrypoints <entrypoints...>",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from "chalk";
import type { GenericTable, HorizontalTableRow } from "cli-table3";

export function tableFlipped(table: GenericTable<HorizontalTableRow>) {
export function asciiTable(table: GenericTable<HorizontalTableRow>) {
return table.options.head
.slice(1)
.map((entryPoint, i) => {
Expand Down
150 changes: 77 additions & 73 deletions packages/cli/src/render/typed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { marked } from "marked";
import { filterProblems, problemAffectsEntrypoint, problemKindInfo } from "@arethetypeswrong/core/problems";
import type { Opts } from "../index.js";
import { moduleKinds, problemFlags, resolutionKinds } from "../problemUtils.js";
import { tableFlipped } from "./tableFlipped.js";
import { asciiTable } from "./asciiTable.js";
import TerminalRenderer from "marked-terminal";

export async function typed(analysis: core.Analysis, opts: Opts) {
const problems = analysis.problems.filter((problem) => !opts.ignoreRules || !opts.ignoreRules.includes(problem.kind));
const grouped = groupProblemsByKind(problems);
const subpaths = Object.keys(analysis.entrypoints);
const entrypoints = Object.keys(analysis.entrypoints);
marked.setOptions({
// @ts-expect-error the types are wrong (haha)
renderer: new TerminalRenderer(),
Expand Down Expand Up @@ -43,86 +43,90 @@ export async function typed(analysis: core.Analysis, opts: Opts) {
console.log(summaryTexts.join("") || defaultSummary);
}

const entrypoints = subpaths.map((s) => {
const entrypointNames = entrypoints.map(
(s) => `"${s === "." ? analysis.packageName : `${analysis.packageName}/${s.substring(2)}`}"`
);
const entrypointHeaders = entrypoints.map((s, i) => {
const hasProblems = problems.some((p) => problemAffectsEntrypoint(p, s, analysis));
const color = hasProblems ? "redBright" : "greenBright";

if (s === ".") return chalk.bold[color](`"${analysis.packageName}"`);
else return chalk.bold[color](`"${analysis.packageName}/${s.substring(2)}"`);
return chalk.bold[color](entrypointNames[i]);
});

if (opts.format === "table-flipped") {
const table = new Table({
head: ["", ...allResolutionKinds.map((kind) => chalk.reset(resolutionKinds[kind]))],
colWidths: [20, ...allResolutionKinds.map(() => 25)],
});

subpaths.forEach((subpath, i) => {
const point = entrypoints[i];
let row = [point];

row = row.concat(
allResolutionKinds.map((kind) => {
const problemsForCell = groupProblemsByKind(
filterProblems(problems, analysis, { entrypoint: subpath, resolutionKind: kind })
);
const resolution = analysis.entrypoints[subpath].resolutions[kind].resolution;
const kinds = Object.keys(problemsForCell) as core.ProblemKind[];
if (kinds.length) {
return kinds
.map(
(kind) => (opts.emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription
)
.join("\n");
}
const getCellContents = memo((entrypoint: string, resolutionKind: core.ResolutionKind) => {
const problemsForCell = groupProblemsByKind(filterProblems(problems, analysis, { entrypoint, resolutionKind }));
const resolution = analysis.entrypoints[entrypoint].resolutions[resolutionKind].resolution;
const kinds = Object.keys(problemsForCell) as core.ProblemKind[];
if (kinds.length) {
return kinds
.map((kind) => (opts.emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription)
.join("\n");
}

const jsonResult = !opts.emoji ? "OK (JSON)" : "🟢 (JSON)";
const moduleResult = (!opts.emoji ? "OK " : "🟢 ") + moduleKinds[resolution?.moduleKind?.detectedKind || ""];
return resolution?.isJson ? jsonResult : moduleResult;
});

const jsonResult = !opts.emoji ? "OK (JSON)" : "🟢 (JSON)";
const moduleResult = (!opts.emoji ? "OK " : "🟢 ") + moduleKinds[resolution?.moduleKind?.detectedKind || ""];
return resolution?.isJson ? jsonResult : moduleResult;
const flippedTable =
opts.format === "auto" || opts.format === "table-flipped"
? new Table({
head: ["", ...allResolutionKinds.map((kind) => chalk.reset(resolutionKinds[kind]))],
})
);

table.push(row);
: undefined;
if (flippedTable) {
entrypoints.forEach((subpath, i) => {
flippedTable.push([
entrypointHeaders[i],
...allResolutionKinds.map((resolutionKind) => getCellContents(subpath, resolutionKind)),
]);
});
console.log(table.toString());
return;
}

const table = new Table({
head: ["", ...entrypoints],
colWidths: [20, ...entrypoints.map(() => 35)],
}) as GenericTable<HorizontalTableRow>;

allResolutionKinds.forEach((kind) => {
let row = [resolutionKinds[kind]];

row = row.concat(
subpaths.map((subpath) => {
const problemsForCell = groupProblemsByKind(
filterProblems(problems, analysis, { entrypoint: subpath, resolutionKind: kind })
);
const resolution = analysis.entrypoints[subpath].resolutions[kind].resolution;
const kinds = Object.keys(problemsForCell) as core.ProblemKind[];
if (kinds.length) {
return kinds
.map(
(kind) => (opts.emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription
)
.join("\n");
}

const jsonResult = !opts.emoji ? "OK (JSON)" : "🟢 (JSON)";
const moduleResult = (!opts.emoji ? "OK " : "🟢 ") + moduleKinds[resolution?.moduleKind?.detectedKind || ""];
return resolution?.isJson ? jsonResult : moduleResult;
})
);

table.push(row);
});
const table =
opts.format === "auto" || !flippedTable
? (new Table({
head: ["", ...entrypointHeaders],
}) as GenericTable<HorizontalTableRow>)
: undefined;
if (table) {
allResolutionKinds.forEach((kind) => {
table.push([resolutionKinds[kind], ...entrypoints.map((entrypoint) => getCellContents(entrypoint, kind))]);
});
}

if (opts.format === "ascii") {
console.log(tableFlipped(table));
} else {
console.log(table.toString());
switch (opts.format) {
case "table":
console.log(table!.toString());
break;
case "table-flipped":
console.log(flippedTable!.toString());
break;
case "ascii":
console.log(asciiTable(table!));
break;
case "auto":
const terminalWidth = process.stdout.columns || 133; // This looks like GitHub Actions' width
if (table!.width <= terminalWidth) {
console.log(table!.toString());
} else if (flippedTable!.width <= terminalWidth) {
console.log(flippedTable!.toString());
} else {
console.log(asciiTable(table!));
}
break;
}
}

function memo<Args extends (string | number)[], Result>(fn: (...args: Args) => Result): (...args: Args) => Result {
const cache = new Map();
return (...args) => {
const key = "" + args;
if (cache.has(key)) {
return cache.get(key);
}

const result = fn(...args);
cache.set(key, result);
return result;
};
}
Loading