Skip to content

Commit 658982e

Browse files
authored
Merge pull request #201 from jason-ha/ignore-resolutions-option
feat: `profile` option
2 parents fcb4268 + 8f9afe0 commit 658982e

15 files changed

+3200
-15
lines changed

.changeset/fuzzy-cats-shed.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@arethetypeswrong/core": minor
3+
---
4+
5+
Add --profile cli option. Example: --profile node16

packages/cli/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ attw --pack . --exclude-entrypoints styles.css # Auto-discovered entrypoints exc
154154
attw --pack . --entrypoints-legacy # All published code files
155155
```
156156

157+
#### Profiles
158+
159+
Profiles select a set of resolution modes to require/ignore. All are evaluated but failures outside of those required are ignored.
160+
161+
The available profiles are:
162+
163+
- `strict` - requires all resolutions
164+
- `node16` - ignores node10 resolution failures
165+
- `esm-only` - ignores CJS resolution failures
166+
167+
In the CLI: `--profile`
168+
169+
```shell
170+
attw <file-name> --profile <profile>
171+
```
172+
173+
In the config file, `profile` can be a string value.
174+
157175
#### Ignore Rules
158176

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

packages/cli/src/getExitCode.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ export function getExitCode(analysis: CheckResult, opts?: RenderOptions): number
66
if (!analysis.types) {
77
return 0;
88
}
9-
if (!opts?.ignoreRules) {
10-
return analysis.problems.length > 0 ? 1 : 0;
11-
}
12-
return analysis.problems.some((problem) => !opts.ignoreRules!.includes(problemFlags[problem.kind])) ? 1 : 0;
9+
const ignoreRules = opts?.ignoreRules ?? [];
10+
const ignoreResolutions = opts?.ignoreResolutions ?? [];
11+
return analysis.problems.some((problem) => {
12+
const notRuleIgnored = !ignoreRules.includes(problemFlags[problem.kind]);
13+
const notResolutionIgnored = "resolutionKind" in problem ? !ignoreResolutions.includes(problem.resolutionKind) : true;
14+
return notRuleIgnored && notResolutionIgnored;
15+
}) ? 1 : 0;
1316
}

packages/cli/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { readConfig } from "./readConfig.js";
1515
import * as render from "./render/index.js";
1616
import { major, minor } from "semver";
1717
import { getExitCode } from "./getExitCode.js";
18+
import { applyProfile, profiles } from "./profiles.js";
1819

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

3031
interface Opts extends render.RenderOptions {
32+
profile?: keyof typeof profiles;
33+
3134
pack?: boolean;
3235
fromNpm?: boolean;
3336
definitelyTyped?: boolean | string;
@@ -80,6 +83,9 @@ particularly ESM-related module resolution issues.`,
8083
.addOption(
8184
new Option("--ignore-rules <rules...>", "Specify rules to ignore").choices(Object.values(problemFlags)).default([]),
8285
)
86+
.addOption(
87+
new Option("--profile <profile>", "Specify analysis profile").choices(Object.keys(profiles)).default("strict"),
88+
)
8389
.option("--summary, --no-summary", "Whether to print summary information about the different errors")
8490
.option("--emoji, --no-emoji", "Whether to use any emojis")
8591
.option("--color, --no-color", "Whether to use any colors (the FORCE_COLOR env variable is also available)")
@@ -88,6 +94,10 @@ particularly ESM-related module resolution issues.`,
8894
const opts = program.opts<Opts>();
8995
await readConfig(program, opts.configPath);
9096

97+
if (opts.profile) {
98+
applyProfile(opts.profile, opts);
99+
}
100+
91101
if (opts.quiet) {
92102
console.log = () => {};
93103
}

packages/cli/src/profiles.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { RenderOptions } from "./render/index.js";
2+
3+
type Profile = Pick<Required<RenderOptions>, "ignoreResolutions">;
4+
5+
export const profiles = {
6+
strict: {
7+
ignoreResolutions: [],
8+
},
9+
node16: {
10+
ignoreResolutions: ["node10"],
11+
},
12+
"esm-only": {
13+
ignoreResolutions: ["node10", "node16-cjs"],
14+
},
15+
} satisfies Record<string, Profile>;
16+
17+
/**
18+
* Merges the profile with the provided options
19+
*
20+
* @param profileKey - name of the profile to apply
21+
* @param opts - options to apply the profile to
22+
*/
23+
export function applyProfile(profileKey: keyof typeof profiles, opts: RenderOptions): void {
24+
const profile = profiles[profileKey];
25+
opts.ignoreResolutions = (opts.ignoreResolutions ?? []).concat(profile.ignoreResolutions);
26+
}

packages/cli/src/readConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from "commander";
22
import { readFile } from "fs/promises";
33
import { problemFlags } from "./problemUtils.js";
4+
import { profiles } from "./profiles.js";
45

56
export async function readConfig(program: Command, alternate = ".attw.json") {
67
try {
@@ -25,6 +26,16 @@ export async function readConfig(program: Command, alternate = ".attw.json") {
2526
);
2627
}
2728

29+
if (key === "profile") {
30+
if (typeof value !== "string") program.error(`error: config option 'profile' should be a string.`);
31+
if (!(value in profiles))
32+
program.error(
33+
`error: config option 'profile' argument '${value}' is invalid. Allowed choices are ${Object.keys(
34+
profiles,
35+
).join(", ")}.`,
36+
);
37+
}
38+
2839
if (Array.isArray(value)) {
2940
const opt = program.getOptionValue(key);
3041

packages/cli/src/render/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { problemFlags } from "../problemUtils.js";
1+
import type { problemFlags, resolutionKinds } from "../problemUtils.js";
22

33
export type Format = "auto" | "table" | "table-flipped" | "ascii" | "json";
44
export interface RenderOptions {
55
ignoreRules?: (typeof problemFlags)[keyof typeof problemFlags][];
6+
ignoreResolutions?: (keyof typeof resolutionKinds)[];
67
format?: Format;
78
color?: boolean;
89
summary?: boolean;

packages/cli/src/render/typed.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as core from "@arethetypeswrong/core";
2-
import { filterProblems, problemAffectsEntrypoint, problemKindInfo } from "@arethetypeswrong/core/problems";
2+
import {
3+
filterProblems,
4+
problemAffectsEntrypoint,
5+
problemAffectsResolutionKind,
6+
problemKindInfo,
7+
} from "@arethetypeswrong/core/problems";
38
import { allResolutionKinds, getResolutionOption, groupProblemsByKind } from "@arethetypeswrong/core/utils";
49
import chalk from "chalk";
510
import Table, { type GenericTable, type HorizontalTableRow } from "cli-table3";
@@ -11,13 +16,16 @@ import type { RenderOptions } from "./index.js";
1116

1217
export async function typed(
1318
analysis: core.Analysis,
14-
{ emoji = true, summary = true, format = "auto", ignoreRules = [] }: RenderOptions,
19+
{ emoji = true, summary = true, format = "auto", ignoreRules = [], ignoreResolutions = [] }: RenderOptions,
1520
): Promise<string> {
1621
let output = "";
1722
const problems = analysis.problems.filter(
1823
(problem) => !ignoreRules || !ignoreRules.includes(problemFlags[problem.kind]),
1924
);
20-
const grouped = groupProblemsByKind(problems);
25+
// sort resolutions with required (impacts result) first and ignored after
26+
const requiredResolutions = allResolutionKinds.filter((kind) => !ignoreResolutions.includes(kind));
27+
const ignoredResolutions = allResolutionKinds.filter((kind) => ignoreResolutions.includes(kind));
28+
const resolutions = requiredResolutions.concat(ignoredResolutions);
2129
const entrypoints = Object.keys(analysis.entrypoints);
2230
marked.setOptions({
2331
renderer: new TerminalRenderer(),
@@ -43,15 +51,26 @@ export async function typed(
4351
if (ignoreRules && ignoreRules.length) {
4452
out(chalk.gray(` (ignoring rules: ${ignoreRules.map((rule) => `'${rule}'`).join(", ")})\n`));
4553
}
54+
if (ignoreResolutions && ignoreResolutions.length) {
55+
out(
56+
chalk.gray(` (ignoring resolutions: ${ignoreResolutions.map((resolution) => `'${resolution}'`).join(", ")})\n`),
57+
);
58+
}
4659

4760
if (summary) {
4861
const defaultSummary = marked(!emoji ? " No problems found" : " No problems found 🌟");
49-
const summaryTexts = Object.keys(grouped).map((kind) => {
62+
const grouped = groupProblemsByKind(problems);
63+
const summaryTexts = Object.entries(grouped).map(([kind, kindProblems]) => {
5064
const info = problemKindInfo[kind as core.ProblemKind];
65+
const affectsRequiredResolution = kindProblems.some((p) =>
66+
requiredResolutions.some((r) => problemAffectsResolutionKind(p, r, analysis)),
67+
);
5168
const description = marked(
5269
`${info.description}${info.details ? ` Use \`-f json\` to see ${info.details}.` : ""} ${info.docsUrl}`,
5370
);
54-
return `${emoji ? `${info.emoji} ` : ""}${description}`;
71+
return `${affectsRequiredResolution ? "" : "(ignored per resolution) "}${
72+
emoji ? `${info.emoji} ` : ""
73+
}${description}`;
5574
});
5675

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

6988
const getCellContents = memo((subpath: string, resolutionKind: core.ResolutionKind) => {
89+
const ignoredPrefix = ignoreResolutions.includes(resolutionKind) ? "(ignored) " : "";
7090
const problemsForCell = groupProblemsByKind(
7191
filterProblems(problems, analysis, { entrypoint: subpath, resolutionKind }),
7292
);
@@ -75,7 +95,10 @@ export async function typed(
7595
const kinds = Object.keys(problemsForCell) as core.ProblemKind[];
7696
if (kinds.length) {
7797
return kinds
78-
.map((kind) => (emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription)
98+
.map(
99+
(kind) =>
100+
ignoredPrefix + (emoji ? `${problemKindInfo[kind].emoji} ` : "") + problemKindInfo[kind].shortDescription,
101+
)
79102
.join("\n");
80103
}
81104

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

93116
const flippedTable =
94117
format === "auto" || format === "table-flipped"
95118
? new Table({
96-
head: ["", ...allResolutionKinds.map((kind) => chalk.reset(resolutionKinds[kind]))],
119+
head: [
120+
"",
121+
...resolutions.map((kind) =>
122+
chalk.reset(resolutionKinds[kind] + (ignoreResolutions.includes(kind) ? " (ignored)" : "")),
123+
),
124+
],
97125
})
98126
: undefined;
99127
if (flippedTable) {
100128
entrypoints.forEach((subpath, i) => {
101129
flippedTable.push([
102130
entrypointHeaders[i],
103-
...allResolutionKinds.map((resolutionKind) => getCellContents(subpath, resolutionKind)),
131+
...resolutions.map((resolutionKind) => getCellContents(subpath, resolutionKind)),
104132
]);
105133
});
106134
}
@@ -112,7 +140,7 @@ export async function typed(
112140
}) as GenericTable<HorizontalTableRow>)
113141
: undefined;
114142
if (table) {
115-
allResolutionKinds.forEach((kind) => {
143+
resolutions.forEach((kind) => {
116144
table.push([resolutionKinds[kind], ...entrypoints.map((entrypoint) => getCellContents(entrypoint, kind))]);
117145
});
118146
}

packages/cli/test/snapshots.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ const tests = [
5050

5151
["[email protected]", "--entrypoints-legacy --ignore-rules=cjs-only-exports-default"],
5252
53+
54+
// Profile test cases
55+
// Some ignored failures and some not - exit code should be 1 per non-node10 failures
56+
["[email protected]", "--profile node16"],
57+
// Explicit strict profile - exit code 1 per node10 resolution
58+
["@[email protected]", "--profile strict -f table"],
59+
// Profile ignoring node10 resolution - exit code 0
60+
["@[email protected]", "--profile node16 -f table-flipped"],
61+
// Profile ignoring node10 and CJS resolution mixed with specific entrypoint - exit code 0
62+
["@[email protected]", "--profile esm-only -f json --entrypoints ."],
5363
];
5464

5565
const defaultOpts = "-f table-flipped";

0 commit comments

Comments
 (0)