Skip to content

Commit

Permalink
Merge pull request #25 from BenoitZugmeyer/saner-parse-arguments-parsing
Browse files Browse the repository at this point in the history
improve: saner arguments parsing
  • Loading branch information
BenoitZugmeyer authored Sep 15, 2024
2 parents d88b0b7 + b7a9de1 commit 31e3f0e
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 188 deletions.
33 changes: 0 additions & 33 deletions node.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"lint": "eslint",
"test": "tools/test.sh",
"test:coverage": "tools/test.sh --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info && genhtml lcov.info --output-directory=coverage --rc branch_coverage=1",
"test:update-snapshots": "tools/test.sh --test-update-snapshots",
"typecheck": "tsc",
"format": "prettier --write .",
"check": "npm test && tsc && prettier --check . && eslint",
Expand Down
129 changes: 75 additions & 54 deletions src/__tests__/parseArguments.test.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,89 @@
import { test, mock } from "node:test";
import { test, describe } from "node:test";
import type { TestContext } from "node:test";

import parseArguments from "../parseArguments.ts";
import CLIError from "../CLIError.ts";

test("parseArguments", (t: TestContext) => {
{
const { stdout, exit } = withStdout(() => {
parseArguments(`x x`.split(" "));
describe("parseArguments", () => {
test("empty arguments", (t: TestContext) => {
t.assert.throws(
() => parseArguments([]),
new CLIError(
"Missing positional argument URL:LINE:COLUMN. Use --help for documentation.",
),
);
});

test("--version", (t: TestContext) => {
t.assert.deepStrictEqual(parseArguments(["--version"]), {
command: "version",
});
t.assert.snapshot(stdout);
t.assert.deepStrictEqual(exit, {
status: 1,
t.assert.deepStrictEqual(parseArguments(["-V"]), {
command: "version",
});
}
});

{
const { stdout, exit } = withStdout(() => {
parseArguments(`--version`.split(" "));
test("--help", (t: TestContext) => {
t.assert.deepStrictEqual(parseArguments(["--help"]), {
command: "help",
});
t.assert.deepStrictEqual(parseArguments(["--help", "toto"]), {
command: "help",
});
t.assert.deepStrictEqual(exit, { status: 0 });
t.assert.match(stdout, /^\d+\.\d+\.\d+.*\n$/);
}
t.assert.deepStrictEqual(parseArguments(["toto", "--help"]), {
command: "help",
});
t.assert.deepStrictEqual(parseArguments(["-h"]), {
command: "help",
});
});

t.assert.deepStrictEqual(parseArguments(`https://foo.com:1:1`.split(" ")), {
debug: false,
sourceURL: "https://foo.com",
position: { line: 1, column: 1 },
beforeContext: 5,
afterContext: 5,
useSourceMap: true,
test("defaults", (t: TestContext) => {
t.assert.deepStrictEqual(parseArguments([`https://foo.com:1:1`]), {
command: "context",
configuration: {
debug: false,
sourceURL: "https://foo.com",
position: { line: 1, column: 1 },
useSourceMap: true,
beforeContext: 5,
afterContext: 5,
},
});
});

const parsedArguments = parseArguments(`-C 2 https://foo.com:1:1`.split(" "));
t.assert.strictEqual(parsedArguments.beforeContext, 2);
t.assert.strictEqual(parsedArguments.afterContext, 2);
test("URL and position", (t: TestContext) => {
const parsedArguments = parseArguments([`https://foo.com:42:12`]);
t.assert.strictEqual(parsedArguments.command, "context");
t.assert.strictEqual(
parsedArguments.configuration.sourceURL,
"https://foo.com",
);
t.assert.deepStrictEqual(parsedArguments.configuration.position, {
line: 42,
column: 12,
});
});

t.assert.strictEqual(
parseArguments(`-d https://foo.com:1:1`.split(" ")).debug,
true,
);
});
test("use source maps", (t: TestContext) => {
const parsedArguments = parseArguments([
`--no-source-map`,
`https://foo.com:1:1`,
]);
t.assert.strictEqual(parsedArguments.command, "context");
t.assert.strictEqual(parsedArguments.configuration.useSourceMap, false);
});

function withStdout(fn: () => void) {
// Make sure to restore the mocks as soon as possible so nodejs test runner can actually write to
// stdout.
const writeMock = mock.method(process.stdout, "write", () => true);
const exitMock = mock.method(process, "exit", () => {
throw new Error("exit");
test("context", (t: TestContext) => {
const parsedArguments = parseArguments([`-C`, `2`, `https://foo.com:1:1`]);
t.assert.strictEqual(parsedArguments.command, "context");
t.assert.strictEqual(parsedArguments.configuration.beforeContext, 2);
t.assert.strictEqual(parsedArguments.configuration.afterContext, 2);
});
let result;
try {
fn();
} catch {
// Ignore
} finally {
result = {
stdout: writeMock.mock.calls.map((call) => call.arguments[0]).join(""),
exit:
exitMock.mock.calls.length > 0
? { status: exitMock.mock.calls[0].arguments[0] }
: undefined,
};
exitMock.mock.restore();
writeMock.mock.restore();
}
return result;
}

test("--debug", (t: TestContext) => {
const parsedArguments = parseArguments([`--debug`, `https://foo.com:1:1`]);
t.assert.strictEqual(parsedArguments.command, "context");
t.assert.strictEqual(parsedArguments.configuration.debug, true);
});
});
3 changes: 0 additions & 3 deletions src/__tests__/parseArguments.test.ts.snapshot

This file was deleted.

11 changes: 6 additions & 5 deletions src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const isTTY = process.stderr.isTTY;
let status: string | undefined;

export default {
debug: makeLogFunction(),
error: makeLogFunction(),
debug: makeLogFunction(process.stdout),
info: makeLogFunction(process.stdout),
error: makeLogFunction(process.stderr),
status: setStatus,
};

Expand All @@ -16,12 +17,12 @@ interface LogFunction {
disabled: boolean;
}

function makeLogFunction() {
function makeLogFunction(stream: NodeJS.WriteStream): LogFunction {
const log: LogFunction = (message) => {
if (!log.disabled) {
clearStatus();
process.stderr.write(String(message));
process.stderr.write("\n");
stream.write(String(message));
stream.write("\n");
restoreStatus();
}
};
Expand Down
88 changes: 78 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#!/usr/bin/env node

import { fileURLToPath } from "node:url";
import { readFileSync } from "fs";

import CLIError from "./CLIError.ts";
import parseArguments from "./parseArguments.ts";
import parseArguments, { OPTIONS } from "./parseArguments.ts";
import applyBeautify from "./applyBeautify.ts";
import applySourceMap from "./applySourceMap.ts";
import printContext from "./printContext.ts";
import log from "./log.ts";
import read from "./read.ts";
import type { ApplyResult } from "./types.ts";
import type { ApplyResult, Configuration } from "./types.ts";

main().catch((e) => {
if (e instanceof CLIError) {
Expand All @@ -21,14 +24,56 @@ main().catch((e) => {
});

async function main() {
const {
debug,
sourceURL,
position,
beforeContext,
afterContext,
useSourceMap,
} = parseArguments();
const parseArgumentsResult = parseArguments(process.argv.slice(2));
switch (parseArgumentsResult.command) {
case "version":
commanVersion();
break;
case "help":
commandHelp();
break;
case "context":
await commandContext(parseArgumentsResult.configuration);
break;
default:
parseArgumentsResult satisfies never;
}
}

export function commandHelp() {
const pkg = getPackageInfos();
let message = `\
Usage: ${pkg.name} [options] <URL:LINE:COLUMN>
${pkg.description}
Options:`;
for (const [name, option] of Object.entries(OPTIONS)) {
let names = "";
if ("short" in option) {
names += `-${option.short}, `;
}
names += `--${name}`;
if (option.type === "string") {
names += " <num>"; // for now, all 'string' types are numbers
}
message += `\n ${names.padEnd(27)} ${option.description}`;
}
log.info(message);
}

function commanVersion() {
log.info(getPackageInfos().version);
}

async function commandContext({
debug,
sourceURL,
position,
beforeContext,
afterContext,
useSourceMap,
}: Configuration) {
log.debug.disabled = !debug;
log.status("Fetching source code...");

Expand All @@ -55,3 +100,26 @@ async function main() {

printContext(applyResult, { beforeContext, afterContext });
}

function getPackageInfos() {
let input;
for (const path of [
// When from main.js
"./package.json",
// When from src/main.js
"../package.json",
]) {
try {
input = readFileSync(fileURLToPath(import.meta.resolve(path)), "utf-8");
break;
} catch {
// continue
}
}

if (!input) {
throw new CLIError("Cannot find package.json");
}

return JSON.parse(input);
}
Loading

0 comments on commit 31e3f0e

Please sign in to comment.