Skip to content

feat: profile option #201

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
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
5 changes: 5 additions & 0 deletions .changeset/fuzzy-cats-shed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arethetypeswrong/core": minor
---

Add --profile cli option. Example: --profile node16
18 changes: 18 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ attw --pack . --exclude-entrypoints styles.css # Auto-discovered entrypoints exc
attw --pack . --entrypoints-legacy # All published code files
```

#### Profiles

Profiles select a set of resolution modes to require/ignore. All are evaluated but failures outside of those required are ignored.

The available profiles are:

- `strict` - requires all resolutions
- `node16` - ignores node10 resolution failures
- `esm-only` - ignores CJS resolution failures

In the CLI: `--profile`

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

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

#### Ignore Rules

Specifies rules/problems to ignore (i.e. not raise an error for).
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/getExitCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ export function getExitCode(analysis: CheckResult, opts?: RenderOptions): number
if (!analysis.types) {
return 0;
}
if (!opts?.ignoreRules) {
return analysis.problems.length > 0 ? 1 : 0;
}
return analysis.problems.some((problem) => !opts.ignoreRules!.includes(problemFlags[problem.kind])) ? 1 : 0;
const ignoreRules = opts?.ignoreRules ?? [];
const ignoreResolutions = opts?.ignoreResolutions ?? [];
return analysis.problems.some((problem) => {
const notRuleIgnored = !ignoreRules.includes(problemFlags[problem.kind]);
const notResolutionIgnored = "resolutionKind" in problem ? !ignoreResolutions.includes(problem.resolutionKind) : true;
return notRuleIgnored && notResolutionIgnored;
}) ? 1 : 0;
}
10 changes: 10 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { readConfig } from "./readConfig.js";
import * as render from "./render/index.js";
import { major, minor } from "semver";
import { getExitCode } from "./getExitCode.js";
import { applyProfile, profiles } from "./profiles.js";

const packageJson = createRequire(import.meta.url)("../package.json");
const version = packageJson.version;
Expand All @@ -28,6 +29,8 @@ const formats = Object.keys({
} satisfies Record<render.Format, any>) as render.Format[];

interface Opts extends render.RenderOptions {
profile?: keyof typeof profiles;

pack?: boolean;
fromNpm?: boolean;
definitelyTyped?: boolean | string;
Expand Down Expand Up @@ -80,6 +83,9 @@ particularly ESM-related module resolution issues.`,
.addOption(
new Option("--ignore-rules <rules...>", "Specify rules to ignore").choices(Object.values(problemFlags)).default([]),
)
.addOption(
new Option("--profile <profile>", "Specify analysis profile").choices(Object.keys(profiles)).default("strict"),
)
.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)")
Expand All @@ -88,6 +94,10 @@ particularly ESM-related module resolution issues.`,
const opts = program.opts<Opts>();
await readConfig(program, opts.configPath);

if (opts.profile) {
applyProfile(opts.profile, opts);
}

if (opts.quiet) {
console.log = () => {};
}
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { RenderOptions } from "./render/index.js";

type Profile = Pick<Required<RenderOptions>, "ignoreResolutions">;

export const profiles = {
strict: {
ignoreResolutions: [],
},
node16: {
ignoreResolutions: ["node10"],
},
"esm-only": {
ignoreResolutions: ["node10", "node16-cjs"],
},
} satisfies Record<string, Profile>;

/**
* Merges the profile with the provided options
*
* @param profileKey - name of the profile to apply
* @param opts - options to apply the profile to
*/
export function applyProfile(profileKey: keyof typeof profiles, opts: RenderOptions): void {
const profile = profiles[profileKey];
opts.ignoreResolutions = (opts.ignoreResolutions ?? []).concat(profile.ignoreResolutions);
}
11 changes: 11 additions & 0 deletions packages/cli/src/readConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from "commander";
import { readFile } from "fs/promises";
import { problemFlags } from "./problemUtils.js";
import { profiles } from "./profiles.js";

export async function readConfig(program: Command, alternate = ".attw.json") {
try {
Expand All @@ -25,6 +26,16 @@ export async function readConfig(program: Command, alternate = ".attw.json") {
);
}

if (key === "profile") {
if (typeof value !== "string") program.error(`error: config option 'profile' should be a string.`);
if (!(value in profiles))
program.error(
`error: config option 'profile' argument '${value}' is invalid. Allowed choices are ${Object.keys(
profiles,
).join(", ")}.`,
);
}

if (Array.isArray(value)) {
const opt = program.getOptionValue(key);

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/render/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { problemFlags } from "../problemUtils.js";
import type { problemFlags, resolutionKinds } from "../problemUtils.js";

export type Format = "auto" | "table" | "table-flipped" | "ascii" | "json";
export interface RenderOptions {
ignoreRules?: (typeof problemFlags)[keyof typeof problemFlags][];
ignoreResolutions?: (keyof typeof resolutionKinds)[];
format?: Format;
color?: boolean;
summary?: boolean;
Expand Down
48 changes: 38 additions & 10 deletions packages/cli/src/render/typed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as core from "@arethetypeswrong/core";
import { filterProblems, problemAffectsEntrypoint, problemKindInfo } from "@arethetypeswrong/core/problems";
import {
filterProblems,
problemAffectsEntrypoint,
problemAffectsResolutionKind,
problemKindInfo,
} from "@arethetypeswrong/core/problems";
import { allResolutionKinds, getResolutionOption, groupProblemsByKind } from "@arethetypeswrong/core/utils";
import chalk from "chalk";
import Table, { type GenericTable, type HorizontalTableRow } from "cli-table3";
Expand All @@ -11,13 +16,16 @@ import type { RenderOptions } from "./index.js";

export async function typed(
analysis: core.Analysis,
{ emoji = true, summary = true, format = "auto", ignoreRules = [] }: RenderOptions,
{ emoji = true, summary = true, format = "auto", ignoreRules = [], ignoreResolutions = [] }: RenderOptions,
): Promise<string> {
let output = "";
const problems = analysis.problems.filter(
(problem) => !ignoreRules || !ignoreRules.includes(problemFlags[problem.kind]),
);
const grouped = groupProblemsByKind(problems);
// sort resolutions with required (impacts result) first and ignored after
const requiredResolutions = allResolutionKinds.filter((kind) => !ignoreResolutions.includes(kind));
const ignoredResolutions = allResolutionKinds.filter((kind) => ignoreResolutions.includes(kind));
const resolutions = requiredResolutions.concat(ignoredResolutions);
const entrypoints = Object.keys(analysis.entrypoints);
marked.setOptions({
renderer: new TerminalRenderer(),
Expand All @@ -43,15 +51,26 @@ export async function typed(
if (ignoreRules && ignoreRules.length) {
out(chalk.gray(` (ignoring rules: ${ignoreRules.map((rule) => `'${rule}'`).join(", ")})\n`));
}
if (ignoreResolutions && ignoreResolutions.length) {
out(
chalk.gray(` (ignoring resolutions: ${ignoreResolutions.map((resolution) => `'${resolution}'`).join(", ")})\n`),
);
}

if (summary) {
const defaultSummary = marked(!emoji ? " No problems found" : " No problems found 🌟");
const summaryTexts = Object.keys(grouped).map((kind) => {
const grouped = groupProblemsByKind(problems);
const summaryTexts = Object.entries(grouped).map(([kind, kindProblems]) => {
const info = problemKindInfo[kind as core.ProblemKind];
const affectsRequiredResolution = kindProblems.some((p) =>
requiredResolutions.some((r) => problemAffectsResolutionKind(p, r, analysis)),
);
const description = marked(
`${info.description}${info.details ? ` Use \`-f json\` to see ${info.details}.` : ""} ${info.docsUrl}`,
);
return `${emoji ? `${info.emoji} ` : ""}${description}`;
return `${affectsRequiredResolution ? "" : "(ignored per resolution) "}${
emoji ? `${info.emoji} ` : ""
}${description}`;
});

out(summaryTexts.join("") || defaultSummary);
Expand All @@ -67,6 +86,7 @@ export async function typed(
});

const getCellContents = memo((subpath: string, resolutionKind: core.ResolutionKind) => {
const ignoredPrefix = ignoreResolutions.includes(resolutionKind) ? "(ignored) " : "";
const problemsForCell = groupProblemsByKind(
filterProblems(problems, analysis, { entrypoint: subpath, resolutionKind }),
);
Expand All @@ -75,7 +95,10 @@ export async function typed(
const kinds = Object.keys(problemsForCell) as core.ProblemKind[];
if (kinds.length) {
return kinds
.map((kind) => (emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription)
.map(
(kind) =>
ignoredPrefix + (emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription,
)
.join("\n");
}

Expand All @@ -87,20 +110,25 @@ export async function typed(
analysis.programInfo[getResolutionOption(resolutionKind)].moduleKinds?.[resolution?.fileName ?? ""]
?.detectedKind || ""
];
return resolution?.isJson ? jsonResult : moduleResult;
return ignoredPrefix + (resolution?.isJson ? jsonResult : moduleResult);
});

const flippedTable =
format === "auto" || format === "table-flipped"
? new Table({
head: ["", ...allResolutionKinds.map((kind) => chalk.reset(resolutionKinds[kind]))],
head: [
"",
...resolutions.map((kind) =>
chalk.reset(resolutionKinds[kind] + (ignoreResolutions.includes(kind) ? " (ignored)" : "")),
),
],
})
: undefined;
if (flippedTable) {
entrypoints.forEach((subpath, i) => {
flippedTable.push([
entrypointHeaders[i],
...allResolutionKinds.map((resolutionKind) => getCellContents(subpath, resolutionKind)),
...resolutions.map((resolutionKind) => getCellContents(subpath, resolutionKind)),
]);
});
}
Expand All @@ -112,7 +140,7 @@ export async function typed(
}) as GenericTable<HorizontalTableRow>)
: undefined;
if (table) {
allResolutionKinds.forEach((kind) => {
resolutions.forEach((kind) => {
table.push([resolutionKinds[kind], ...entrypoints.map((entrypoint) => getCellContents(entrypoint, kind))]);
});
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/test/snapshots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ const tests = [

["eslint-module-utils@2.8.1.tgz", "--entrypoints-legacy --ignore-rules=cjs-only-exports-default"],
["@cerbos__core@0.18.1.tgz"],

// Profile test cases
// Some ignored failures and some not - exit code should be 1 per non-node10 failures
["axios@1.4.0.tgz", "--profile node16"],
// Explicit strict profile - exit code 1 per node10 resolution
["@fluid-experimental__presence@2.3.0.tgz", "--profile strict -f table"],
// Profile ignoring node10 resolution - exit code 0
["@fluid-experimental__presence@2.3.0.tgz", "--profile node16 -f table-flipped"],
// Profile ignoring node10 and CJS resolution mixed with specific entrypoint - exit code 0
["@fluid-experimental__presence@2.3.0.tgz", "--profile esm-only -f json --entrypoints ."],
];

const defaultOpts = "-f table-flipped";
Expand Down
Loading
Loading