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

Rewrite the CLI internals #265

Merged
merged 30 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f7808be
Progress on CLI rewrite
kasperisager Jun 13, 2020
0fcf857
Clean up flag parsing
kasperisager Jun 15, 2020
12c4962
Merge branch 'master' into cli-overhaul
kasperisager Jun 15, 2020
1546f3b
Don't rely on Node.js globals
kasperisager Jun 15, 2020
cc653a5
Update lockfile
kasperisager Jun 16, 2020
6438dd2
`repeat()` -> `repeatable()`
kasperisager Jun 16, 2020
56a9e78
Merge branch 'master' into cli-overhaul
kasperisager Jun 17, 2020
c69cbe8
Add `Flag#filter()` and `Flag#choices()`
kasperisager Jun 17, 2020
6e1300b
Fix typo
kasperisager Jun 17, 2020
2f65742
Merge branch 'master' into cli-overhaul
kasperisager Jun 17, 2020
154283f
Progress
kasperisager Jun 18, 2020
3b967d1
Allow passing new `Joiner` to `Flag#map()`
kasperisager Jun 18, 2020
f139ef4
Add `Argument#repeatable()`
kasperisager Jun 18, 2020
def57de
Remove unneeded reference
kasperisager Jun 18, 2020
1479eb7
Break on unknown flag
kasperisager Jun 18, 2020
afd16c0
Avoid invariant `Flag<T>` type
kasperisager Jun 18, 2020
fe9babc
Add version specifiers to commands
kasperisager Jun 18, 2020
5c92ba9
Move command types to separate package
kasperisager Jun 18, 2020
2c6982f
Add missing reference
kasperisager Jun 18, 2020
bf4930f
Merge branch 'master' into cli-overhaul
kasperisager Jun 19, 2020
ef6e56b
Internalize some `Command` methods
kasperisager Jun 19, 2020
caedaef
Clean up flag and argument parsing
kasperisager Jun 19, 2020
362f723
Clean up error handling
kasperisager Jun 19, 2020
d9afff0
Rework some flag and argument internals
kasperisager Jun 22, 2020
806de3c
Merge branch 'master' into cli-overhaul
kasperisager Jun 23, 2020
c311f75
Never store empty default labels
kasperisager Jun 23, 2020
d942103
Add initial flag spec
kasperisager Jun 23, 2020
fec454c
Merge branch 'master' into cli-overhaul
kasperisager Jun 23, 2020
20e5838
Use `Mapper` default
kasperisager Jun 23, 2020
8f2b3ec
Merge branch 'master' into cli-overhaul
kasperisager Jun 23, 2020
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
72 changes: 60 additions & 12 deletions packages/alfa-cli/bin/alfa.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,62 @@
#!/usr/bin/env node

import * as command from "@oclif/command";
import * as errors from "@oclif/errors";

async function alfa() {
try {
await command.run();
} catch (err) {
errors.handle(err);
}
}

alfa();
/// <reference types="node" />

import * as path from "path";
import * as process from "process";
import * as tty from "tty";

import { Command, Flag } from "@siteimprove/alfa-command";
import { None } from "@siteimprove/alfa-option";
import { Err } from "@siteimprove/alfa-result";

import * as pkg from "../package.json";

import audit from "./alfa/command/audit";
import scrape from "./alfa/command/scrape";

const {
argv: [node, bin],
platform,
arch,
version,
} = process;

const application = Command.withSubcommands(
path.basename(bin),
`${pkg.name}/${pkg.version} ${platform}-${arch} node-${version}`,
`The tool for all your accessibility needs on the command line.`,
{
help: Flag.help("Display the help information."),
version: Flag.version("Output the current version."),
},
(self) => ({
audit: audit(self),
scrape: scrape(self),
}),
None
);

application
.run(process.argv.slice(2))
.catch((err: Error) => Err.of(`${err.message}`))
.then((result) => {
let stream: tty.WriteStream;
let output: string;

if (result.isOk()) {
stream = process.stdout;
output = result.get();
} else {
stream = process.stderr;
output = result.getErr();
}

output = output.trimRight();

if (output.length > 0) {
stream.write(output + "\n");
}

process.exit(result.isOk() ? 0 : 1);
});
22 changes: 22 additions & 0 deletions packages/alfa-cli/bin/alfa/command/audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Command } from "@siteimprove/alfa-command";
import { Option } from "@siteimprove/alfa-option";

import { Arguments } from "./audit/arguments";
import { Flags } from "./audit/flags";

/**
* @internal
*/
export default (parent: Command) =>
Command.withArguments(
"audit",
parent.version,
"Perform an accessibility audit of a page.",
Flags,
Arguments,
Option.of(parent),
() => async (...args) => {
const { run } = await import("./audit/run");
return run(...args);
}
);
11 changes: 11 additions & 0 deletions packages/alfa-cli/bin/alfa/command/audit/arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Argument } from "@siteimprove/alfa-command";

export const Arguments = {
url: Argument.string(
"url",
`The URL of the page to audit. Both remote and local protocols are
supported so the URL can either be an address of a remote page or a path to
a local file. If no URL is provided, an already serialised page will be read
from stdin.`
).optional(),
};
40 changes: 40 additions & 0 deletions packages/alfa-cli/bin/alfa/command/audit/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Flag } from "@siteimprove/alfa-command";

import * as scrape from "../scrape/flags";

export const Flags = {
help: Flag.help("Display the help information."),

interactive: Flag.boolean(
"interactive",
"Whether or not to run an interactive audit."
)
.alias("i")
.default(false),

format: Flag.string("format", "The reporting format to use.")
.type("format or package")
.alias("f")
.default("earl"),
kasperisager marked this conversation as resolved.
Show resolved Hide resolved

output: Flag.string(
"output",
`The path to write results to. If no path is provided, results are written
to stdout.`
)
.type("path")
.alias("o")
.optional(),

outcomes: Flag.string(
"outcome",
`The type of outcome to include in the results. If not provided, all types
of outcomes are included. This flag can be repeated to include multiple
types of outcomes.`
)
.choices("passed", "failed", "inapplicable", "cantTell")
.repeatable()
.optional(),

...scrape.Flags,
};
97 changes: 97 additions & 0 deletions packages/alfa-cli/bin/alfa/command/audit/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/// <reference types="node" />

import * as fs from "fs";

import { Audit, Outcome } from "@siteimprove/alfa-act";
import { Command } from "@siteimprove/alfa-command";
import { Node } from "@siteimprove/alfa-dom";
import { Formatter } from "@siteimprove/alfa-formatter";
import { Iterable } from "@siteimprove/alfa-iterable";
import { None } from "@siteimprove/alfa-option";
import { Ok } from "@siteimprove/alfa-result";
import { Rules, Question } from "@siteimprove/alfa-rules";
import { Page } from "@siteimprove/alfa-web";

import { Oracle } from "../../oracle";

import type { Arguments } from "./arguments";
import type { Flags } from "./flags";

import * as scrape from "../scrape/run";

type Input = Page;
type Target = Node | Iterable<Node>;

export const run: Command.Runner<typeof Flags, typeof Arguments> = async ({
flags,
args: { url: target },
}) => {
const formatter = Formatter.load<Input, Target, Question>(flags.format);

if (formatter.isErr()) {
return formatter;
}

let json: string;

if (target.isNone()) {
json = fs.readFileSync(0, "utf-8");
} else {
const result = await scrape.run({
flags: {
...flags,
output: None,
},
args: {
url: target.get(),
},
});

if (result.isErr()) {
return result;
}

json = result.get();
}

const page = Page.from(JSON.parse(json));

const audit = Rules.reduce(
(audit, rule) => audit.add(rule),
Audit.of<Input, Target, Question>(
page,
flags.interactive ? Oracle(page) : undefined
)
);

let outcomes = await audit.evaluate();

if (flags.outcomes.isSome()) {
const filter = new Set(flags.outcomes.get());

outcomes = Iterable.filter(outcomes, (outcome) => {
if (Outcome.isPassed(outcome)) {
return filter.has("passed");
}

if (Outcome.isFailed(outcome)) {
return filter.has("failed");
}

if (Outcome.isInapplicable(outcome)) {
return filter.has("inapplicable");
}

return filter.has("cantTell");
});
}
kasperisager marked this conversation as resolved.
Show resolved Hide resolved

const output = formatter.get()(page, outcomes);

if (flags.output.isNone()) {
return Ok.of(output);
} else {
fs.writeFileSync(flags.output.get() + "\n", output);
return Ok.of("");
}
};
22 changes: 22 additions & 0 deletions packages/alfa-cli/bin/alfa/command/scrape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Command } from "@siteimprove/alfa-command";
import { Option } from "@siteimprove/alfa-option";

import { Arguments } from "./scrape/arguments";
import { Flags } from "./scrape/flags";

/**
* @internal
*/
export default (parent: Command) =>
Command.withArguments(
"scrape",
parent.version,
"Scrape a page and output it in a serialisable format.",
Flags,
Arguments,
Option.of(parent),
() => async (...args) => {
const { run } = await import("./scrape/run");
return run(...args);
}
);
10 changes: 10 additions & 0 deletions packages/alfa-cli/bin/alfa/command/scrape/arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Argument } from "@siteimprove/alfa-command";

export const Arguments = {
url: Argument.string(
"url",
`The URL of the page to scrape. Both remote and local protocols are
supported so the URL can either be an address of a remote page or a path to
a local file.`
),
};
Loading