Skip to content

Add CLI Package #26

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

Merged
merged 34 commits into from
Jun 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5bb9cfa
Very basic CLI
ej-shafran May 29, 2023
b13fb3f
Fix issue with package
ej-shafran May 29, 2023
d88e7de
Reformatting, renaming, cleaner code
ej-shafran May 29, 2023
13bb3ef
Update errors (partial)
ej-shafran May 29, 2023
d2d78e1
Handle error with "version not found"
ej-shafran May 29, 2023
65af8c1
Add "--no-emoji" flag
ej-shafran May 29, 2023
e603e1d
Add several other options
ej-shafran May 29, 2023
7531000
Add --strict flag
ej-shafran May 29, 2023
8b218d0
Add --quiet flag
ej-shafran May 29, 2023
7f611e2
Handle FS errors
ej-shafran May 29, 2023
14e4759
Remove unnecessary devDependency
ej-shafran May 29, 2023
8167541
Read options from '.attw.json' (or other specified file)
ej-shafran May 29, 2023
df027aa
Update README
ej-shafran May 29, 2023
05cfc0a
Minor typo fixes
ej-shafran May 29, 2023
29b406a
Add `--ignore` flag and config option
ej-shafran May 30, 2023
b0dd98c
Add docs for `--ignore`
ej-shafran May 30, 2023
eccf212
Fix issue in README
ej-shafran May 30, 2023
ca5bf6a
Add `--flipped` flag
ej-shafran May 30, 2023
32a8fc3
Reorder `--help` options
ej-shafran May 30, 2023
952acd6
Update packages/cli/README.md
ej-shafran May 30, 2023
eae0191
Use TSC instead of esbuild
ej-shafran May 31, 2023
19309e2
Merge branch 'main' of gh:ej-shafran/arethetypeswrong.github.io
ej-shafran May 31, 2023
e59439d
Update help output on error
ej-shafran May 31, 2023
0ad405f
Combine formats into `--format`
ej-shafran Jun 3, 2023
2776f66
Add build script
ej-shafran Jun 3, 2023
f756272
Make --strict the default, remove configurability
ej-shafran Jun 3, 2023
8ad620c
Default to reading from file (possibly temporary?)
ej-shafran Jun 3, 2023
3e42e1c
Read version from package.json
ej-shafran Jun 4, 2023
c462224
Change `--ignore` to `--ignore-rules`
ej-shafran Jun 4, 2023
f42feed
Update README and fix config file issue
ej-shafran Jun 4, 2023
a329a00
More README updates
ej-shafran Jun 4, 2023
76638c2
Switch filter to `some`
ej-shafran Jun 4, 2023
b1389ea
Slight refactorings and renaming
ej-shafran Jun 4, 2023
e4f2011
Move parsePackageSpec into core
ej-shafran Jun 4, 2023
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
811 changes: 622 additions & 189 deletions package-lock.json

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# arethetypeswrong/cli

A CLI wrapper for [arethetypeswrong](https://arethetypeswrong.github.io/).

## Installation

**NOTE:** The package has not yet been published to NPM.

```shell
npm i -g @arethetypeswrong/cli
```

<!-- Or, using `npx`: -->
<!---->
<!-- ```shell -->
<!-- npx attw -->
<!-- ``` -->

## Usage

The `attw` command acts very similarly to [the arethetypeswrong website](https://arethetypeswrong.github.io/), with some additional features that are useful for command line usage.

The usage is:

```shell
attw [options] <file-name>
```

Where `<file-name>` is a required positional argument - either the path to a local `.tar.gz` file, or the name of an NPM package.

## Configuration

`attw` supports a JSON config file (by default named `.attw.json`) which allows you to pre-set the command line arguments. The options are a one-to-one mapping of the command line flags, changed to camelCase, and are all documented in their relevant `Options` section below.

Note that the `--config-path` option cannot be set from the config file :upside_down_face:

### Options

#### Help

Show help information and exit.

In the CLI: `--help`, `-h`

```shell
attw --help
```

#### Version

Print the current version of `attw` and exit.

In the CLI: `--version`, `-v`

```shell
attw --version
```

#### Format

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

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
- `ascii`, for large tables where the output is clunky
- `raw`, outputs the raw JSON data (overriding all other rendering options)

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

```shell
attw --format <format> <file-name>
```

In the config file, `format` can be a string value.

#### From NPM

Treat `<file-name>` as the name (and, optionally, version) of a package from the NPM registry.

In the CLI: `--from-npm`, `-f`

```shell
attw --from-npm <package-name>
```

In the config file, `fromNpm` can be a boolean value.

#### Ignore Rules

Specifies rules/problems to ignore (i.e. not raise an error for).

The available values are:
- `wildcard`
- `no-resolution`
- `untyped-resolution`
- `false-cjs`
- `false-esm`
- `cjs-resolves-to-esm`
- `fallback-condition`
- `cjs-only-exports-default`
- `false-export-default`
- `unexpected-esm-syntax`
- `unexpected-cjs-syntax`

In the CLI: `--ignore-rules`

```shell
attw <file-name> --ignore-rules <rules...>
```

In the config file, `ignoreRules` can be an array of strings.

#### Summary/No Summary

Whether to display a summary of what the different errors/problems mean. Defaults to showing the summary (`--summary`).

In the CLI: `--summary`/`--no-summary`

```shell
attw --summary/--no-summary <file-name>
```

In the config file, `summary` can be a boolean value.

#### Emoji/No Emoji

Whether to print the information with emojis. Defaults to printing with emojis (`--emoji`).

In the CLI: `--emoji`/`--no-emoji`

```shell
attw --emoji/--no-emoji <file-name>
```

In the config file, `emoji` can be a boolean value.

#### Color/No Color

Whether to print with colors. Defaults to printing with colors (`--color`).

The `FORCE_COLOR` env variable is also available for use (set is to `0` or `1`).

In the CLI: `--color`/`--no-color`

```shell
attw --color/--no-color <file-name>
```

In the config file, `color` can be a boolean value.

#### Quiet

When set, nothing will be printed to STDOUT.

In the CLI: `--quiet`, `-q`

```shell
attw --quiet <file-name>
```

In the config file, `quiet` can be a boolean value.

#### Config Path

The path to the config file. Defaults to `./.attw.json`.

In the CLI: `--config-path <path>`

```shell
attw --config-path <path> <file-name>
```

Cannot be set from within the config file itself.

39 changes: 39 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@arethetypeswrong/cli",
"version": "0.0.1",
"description": "A CLI tool for arethetypeswrong.github.io",
"author": "Andrew Branch & ej-shafran",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/arethetypeswrong/arethetypeswrong.github.io.git",
"directory": "packages/cli"
},
"files": [
"dist"
],
"bin": {
"attw": "./dist/index.js"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"tsc": "tsc",
"local:install": "npm install -g .",
"local:uninstall": "npm uninstall -g @arethetypeswrong/cli",
"build": "npm run local:uninstall && npm run tsc && npm run local:install"
},
"type": "module",
"devDependencies": {
"@types/node": "^20.2.5",
"@types/node-fetch": "^2.6.4",
"typescript": "^5.0.0-dev.20230207"
},
"dependencies": {
"@arethetypeswrong/core": "^0.0.6",
"chalk": "^4.1.2",
"cli-table3": "^0.6.3",
"commander": "^10.0.1"
}
}
142 changes: 142 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env node

import * as core from "@arethetypeswrong/core";
import { Option, program } from "commander";
import chalk from "chalk";
import { readFile } from "fs/promises";
import { FetchError } from "node-fetch";
import { createRequire } from "module";

import * as render from "./render/index.js";
import { readConfig } from "./readConfig.js";
import { problemFlags } from "./problemUtils.js";

const packageJson = createRequire(import.meta.url)("../package.json");
const version = packageJson.version;
const coreVersion = packageJson.dependencies["@arethetypeswrong/core"].substring(1);
const tsVersion = packageJson.devDependencies.typescript.substring(1);

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

type Format = (typeof formats)[number];

export interface Opts {
fromNpm?: boolean;
summary?: boolean;
emoji?: boolean;
color?: boolean;
quiet?: boolean;
configPath?: string;
ignoreRules?: string[];
format: Format;
}

program
.addHelpText("before", `ATTW CLI (v${version})\n`)
.addHelpText("after", `\ncore: v${coreVersion}, typescript: v${tsVersion}`)
.version(`v${version}`)
.name("attw")
.description(
`${chalk.bold.blue(
"Are the Types Wrong?"
)} attempts to analyze npm package contents for issues with their TypeScript types,
particularly ESM-related module resolution issues.`
)
.argument("<file-name>", "the file to check; by default a path to a .tar.gz file, unless --from-npm is set")
.option("-f, --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"))
.option("-q, --quiet", "don't print anything to STDOUT (overrides all other options)")
.addOption(
new Option("--ignore-rules <rules...>", "specify rules to ignore").choices(Object.values(problemFlags)).default([])
)
.option("--summary, --no-summary", "whether to print summary information about the different errors")
.option("--emoji, --no-emoji", "whether to use any emojis")
.option("--color, --no-color", "whether to use any colors (the FORCE_COLOR env variable is also available)")
.option("--config-path <path>", "path to config file (default: ./.attw.json)")
.action(async (fileName: string) => {
const opts = program.opts<Opts>();
await readConfig(program, opts.configPath);
opts.ignoreRules = opts.ignoreRules?.map(
(value) => Object.keys(problemFlags).find((key) => problemFlags[key as core.ProblemKind] === value) as string
);

if (opts.quiet) {
console.log = () => { };
}

if (!opts.color) {
process.env.FORCE_COLOR = "0";
}

let analysis: core.Analysis;
if (opts.fromNpm) {
try {
const result = core.parsePackageSpec(fileName);
if (result.status === "error") {
program.error(result.error);
} else {
analysis = await core.checkPackage(result.data.packageName, result.data.version);
}
} catch (error) {
if (error instanceof FetchError) {
program.error(`error while fetching package:\n${error.message}`, { code: error.code });
}

handleError(error, "checking package");
}
} else {
try {
const file = await readFile(fileName);
const data = new Uint8Array(file);
analysis = await core.checkTgz(data);
} catch (error) {
handleError(error, "checking file");
}
}

if (opts.format === "json") {
const result = { analysis } as {
analysis: core.Analysis;
problems?: Partial<Record<core.ProblemKind, core.Problem[]>>;
};

if (analysis.containsTypes) {
result.problems = core.groupByKind(core.getProblems(analysis));
}

console.log(JSON.stringify(result));

if (
analysis.containsTypes &&
core.getProblems(analysis).some((problem) => !opts.ignoreRules.includes(problem.kind))
)
process.exit(1);

return;
}

console.log();
if (analysis.containsTypes) {
await render.typed(analysis, opts);

if (
analysis.containsTypes &&
core.getProblems(analysis).some((problem) => !opts.ignoreRules.includes(problem.kind))
)
process.exit(1);
} else {
render.untyped(analysis as core.UntypedAnalysis);
}
});

program.parse(process.argv);

function handleError(error: unknown, title: string): never {
if (error && typeof error === "object" && "message" in error) {
program.error(`error while ${title}:\n${error.message}`, {
code: "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN",
});
}

program.error(`unknown error while ${title}`, { code: "UNKNOWN" });
}
Loading