Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: persist all output, add --output flag #12

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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: 1 addition & 0 deletions .github/workflows/test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npx tsimp --start
- run: npm test
1 change: 1 addition & 0 deletions .github/workflows/test-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npx tsimp --start
- run: npm test
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,16 @@
"--import=tsimp"
]
},
"tsimp": {
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true
}
},
"dependencies": {
"ci-info": "^3.8.0",
"execa": "^7.1.1",
"line-transform-stream": "^1.0.1",
"listr2": "^6.6.0",
"ci-info": "^4.0.0",
"execa": "^8.0.1",
"listr2": "^8.0.1",
"meow": "^13.1.0"
},
"devDependencies": {
Expand Down
35 changes: 34 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ npm install --save-dev listr-cli
```sh
yarn add --dev listr-cli
```

</details>

## Usage
Expand Down Expand Up @@ -80,6 +81,30 @@ $ npx listr xo ava --no-persist

</details>

#### `--output`

Choices: `all` | `last`\
Default: `all`

Control the length of task output. `all` shows all output, `last` shows only the last line of output. By default, all lines are shown.

<details>
<summary>Example</summary>

```sh
$ npx listr ava
⠼ ava
› ✔ cli › main
› ✔ cli › output flag
› ...

$ npx listr ava --output last
✔ ava [2s]
› 7 tests passed
```

</details>

#### `--all-optional` (`--opt`)

Continue executing tasks if one fails. By default, the task list will cancel early.
Expand All @@ -99,14 +124,22 @@ $ listr xo 'ava --tap | node parse.js' tsd --all-optional

#### `--environment` (`--env`, `-e`)

Set environment variables cross-platform via `process.env`. Follows the same syntax as [Rollup](https://rollupjs.org/command-line-interface/#environment-values):
Set environment variables cross-platform via `process.env` (see [`execa`](https://github.com/sindresorhus/execa#env)). Follows the same syntax as [Rollup](https://rollupjs.org/command-line-interface/#environment-values):

```sh
$ listr ava --env CI,NODE_OPTIONS:'--loader=tsx'
#=> process.env.CI = "true"
#=> process.env.NODE_OPTIONS = "--loader=tsx"
```

#### `NO_COLOR`

To disable colors, set the `NO_COLOR` environment variable to any value:

```sh
$ NO_COLOR=1 listr xo ava
```

## Related

- [listr2](https://github.com/cenk1cenk2/listr2) - Create beautiful CLI interfaces via easy and logical to implement task lists that feel alive and interactive.
27 changes: 20 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import process from "node:process";
import meow from "meow";
import { $ } from "execa";
import { parseEnvironmentVariables, getCommands } from "./helpers/index.js";
import { getTasks } from "./tasks.js";
import { getTasks, outputTypes, type OutputType } from "./tasks.js";

const cli = meow(`
Usage
Expand Down Expand Up @@ -46,6 +46,11 @@ const cli = meow(`
type: "boolean",
default: true,
},
output: {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be changed to --output-type

type: "string",
default: "all",
choices: outputTypes as unknown as string[],
},
allOptional: {
type: "boolean",
aliases: ["opt"],
Expand All @@ -67,27 +72,35 @@ if (input.length === 0 || helpShortFlag) {
cli.showHelp(0);
}

const { hideTimer, persist: persistentOutput, allOptional, environment } = cli.flags;
const { hideTimer, persist: persistentOutput, output, allOptional, environment } = cli.flags;
const outputType = output as OutputType;

const env = parseEnvironmentVariables(environment);

process.env = {
...process.env,
...env,
};
if (!process.env["NO_COLOR"]) {
env["FORCE_COLOR"] = "true";
}

const tasks = getTasks({
commands: getCommands(input),
exitOnError: !allOptional,
showTimer: !hideTimer,
persistentOutput,
outputType,
});

const $$ = $({
shell: true,
reject: false,
all: true,
stripFinalNewline: false, // Keep command output formatting
env,
});

const ci = $({
shell: true,
stdio: "inherit",
env,
});

await tasks.run({ $$ }).catch(() => process.exit(1));
await tasks.run({ $$, ci }).catch(() => process.exit(1));
39 changes: 24 additions & 15 deletions src/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import process from "node:process";
import { Listr, PRESET_TIMER, type ListrTask } from "listr2";
import { $, type ExecaReturnValue } from "execa";
import type { TupleToUnion } from "type-fest";
import { Listr, createWritable, PRESET_TIMER, type ListrTask, type DefaultRenderer } from "listr2";
import { type $, type ExecaReturnValue } from "execa";
import { isCI } from "ci-info";
import LineTransformStream from "line-transform-stream";
import { type Command, trimIfNeeded } from "./helpers/index.js";

/**
Expand All @@ -17,44 +17,53 @@ export const endTask = (output = "") => {

type ListrContext = {
$$: typeof $;
ci: typeof $;
};

export const outputTypes = ["all", "last"] as const;
export type OutputType = TupleToUnion<typeof outputTypes>;

type TaskContext = {
commands: Command[];
exitOnError: boolean;
showTimer: boolean;
persistentOutput: boolean;
outputType: OutputType;
};

export const getTasks = ({ commands, exitOnError, showTimer, persistentOutput }: TaskContext) => {
const tasks: Array<ListrTask<ListrContext>> = [];
export const getTasks = ({ commands, exitOnError, showTimer, persistentOutput, outputType }: TaskContext) => {
const tasks: Array<ListrTask<ListrContext, typeof DefaultRenderer>> = [];

for (const { taskTitle, command, commandName } of commands) {
tasks.push({
title: taskTitle,
// @ts-expect-error: return works
task: async ({ $$ }, task) => {
task: async ({ $$, ci }, task) => {
if (isCI) {
return $({ shell: true, stdio: "inherit" })`${command}`;
return ci`${command}`;
}

task.title += `: running "${command}"...`;
const executeCommand = $$`${command}`;

let commandNotFound = false;

executeCommand.all?.pipe(new LineTransformStream(line => {
if (line.match(new RegExp(`${commandName}.*not found`))) {
const taskOutput = createWritable(chunk => {
// TODO: this isn't cross-platform
if (chunk.toString().match(new RegExp(`${commandName}.*not found`))) {
task.title = taskTitle === command
? `${taskTitle}: command not found.`
: `${taskTitle}: command "${command}" not found.`;

commandNotFound = true;
return "";
task.output = "";
return;
}

return line;
})).pipe(task.stdout());
task.output = chunk;
});

executeCommand.all?.pipe(taskOutput);

const { exitCode, all } = await executeCommand as ExecaReturnValue & { all: string };

Expand All @@ -73,15 +82,15 @@ export const getTasks = ({ commands, exitOnError, showTimer, persistentOutput }:
endTask();
}
},
options: {
rendererOptions: {
persistentOutput,
outputBar: outputType === "all" ? Number.POSITIVE_INFINITY : true,
},
});
}

return new Listr<ListrContext, "default", "verbose">(tasks, {
exitOnError,
forceColor: true,
rendererOptions: {
timer: {
...PRESET_TIMER,
Expand All @@ -93,7 +102,7 @@ export const getTasks = ({ commands, exitOnError, showTimer, persistentOutput }:
removeEmptyLines: false,
},
silentRendererCondition: isCI,
fallbackRenderer: "verbose", // TODO: maybe use test renderer, it can log failed states
fallbackRenderer: "verbose",
fallbackRendererCondition: process.env["NODE_ENV"] === "test",
fallbackRendererOptions: {
logTitleChange: true,
Expand Down
38 changes: 35 additions & 3 deletions test/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,27 @@ test.afterEach.always(async t => {

const trim = (stdout: string) => stdout.trim().split("\n").map(line => stripAnsi(line).trim());

type MacroArgs = [
commands: string[],
options?: { errorMessage: string },
];

const verifyCli = (shouldPass: boolean, setup = async () => "", teardown = async () => "") => (
test.macro(async (t, commands: string[]) => {
test.macro<MacroArgs>(async (t, commands, options) => {
await setup();

const { exitCode, stdout } = await execa(t.context.binPath, commands, { reject: false });
const { exitCode, stdout, stderr } = await execa(t.context.binPath, commands, { reject: false });
const receivedLines = trim(stdout);

const assertions = await t.try(tt => {
tt.snapshot(receivedLines);
tt.is(exitCode, shouldPass ? 0 : 1, "CLI exited with the wrong exit code!");

if (options?.errorMessage) {
tt.true(stderr.includes(options.errorMessage), "Error message did not match!");
tt.snapshot(options.errorMessage);
} else {
tt.snapshot(receivedLines);
}
});

if (!assertions.passed) {
Expand Down Expand Up @@ -158,6 +169,7 @@ test("flags: -h", cliPasses, [
test.todo("verify help text indentation is consistent");

test.todo("task output displays color");
test.todo("color can be disabled via NO_COLOR");

const envVarsFixture = {
envVars: "FOO,BAR:baz",
Expand Down Expand Up @@ -196,3 +208,23 @@ test("custom task names ignores quoted tasks", cliPasses, [
"\"echo ::\"",
"'echo ::'",
]);

test.todo("flags: --no-persist");

test("outputs all lines by default", cliPasses, [
"node -e 'console.log(1);console.log(2);console.log(3);'",
]);

test("--output=all outputs all lines", cliPasses, [
"--output=all",
"node -e 'console.log(1);console.log(2);console.log(3);'",
]);

test.todo("--output=last outputs only previous line");

test("flags: --output errors on invalid choice", cliFails, [
"--output=foo",
"node -e 'console.log(1);console.log(2);console.log(3);'",
], {
errorMessage: "Unknown value for flag `--output`: `foo`. Value must be one of: [`all`, `last`]",
});
38 changes: 38 additions & 0 deletions test/snapshots/cli.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,41 @@ Generated by [AVA](https://avajs.dev).
'[OUTPUT] ::',
'[COMPLETED] echo',
]

## outputs all lines by default

> Snapshot 1

[
'[STARTED] node',
'[TITLE] node: running "node -e \'console.log(1);console.log(2);console.log(3);\'"...',
'[OUTPUT] 1',
'',
'[OUTPUT] 2',
'[OUTPUT] 3',
'',
'[TITLE] node',
'[COMPLETED] node',
]

## --output=all outputs all lines

> Snapshot 1

[
'[STARTED] node',
'[TITLE] node: running "node -e \'console.log(1);console.log(2);console.log(3);\'"...',
'[OUTPUT] 1',
'',
'[OUTPUT] 2',
'[OUTPUT] 3',
'',
'[TITLE] node',
'[COMPLETED] node',
]

## flags: --output errors on invalid choice

> Snapshot 1

'Unknown value for flag `--output`: `foo`. Value must be one of: [`all`, `last`]'
Binary file modified test/snapshots/cli.ts.snap
Binary file not shown.
Loading