Skip to content

Commit

Permalink
Rewrite the CLI internals (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperisager authored Jun 23, 2020
1 parent d76016e commit 434c3da
Show file tree
Hide file tree
Showing 26 changed files with 2,102 additions and 621 deletions.
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"),

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");
});
}

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

0 comments on commit 434c3da

Please sign in to comment.