From 0546cacb3a7a8079fddcdf453d1df3e0729d63fd Mon Sep 17 00:00:00 2001 From: Mike Bell Date: Fri, 6 Nov 2026 20:50:20 +0000 Subject: [PATCH] fix(cli): base exit code on visible problems per resolution getExitCode counted a problem unless it carried resolutionKind and that kind was ignored. Eight problem kinds carry no resolutionKind, so a problem that affects only ignored resolutions still produced exit 1. Count a problem only when it affects a resolution that is not ignored. --- .changeset/exit-code-visible-problems.md | 5 ++ packages/cli/src/getExitCode.ts | 10 ++- packages/cli/test/getExitCode.test.ts | 102 +++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 .changeset/exit-code-visible-problems.md create mode 100644 packages/cli/test/getExitCode.test.ts diff --git a/.changeset/exit-code-visible-problems.md b/.changeset/exit-code-visible-problems.md new file mode 100644 index 0000000..655c448 --- /dev/null +++ b/.changeset/exit-code-visible-problems.md @@ -0,0 +1,5 @@ +--- +"@arethetypeswrong/cli": patch +--- + +The exit code now honours `--profile` for every problem kind. A problem visible only in ignored resolutions no longer causes exit 1. diff --git a/packages/cli/src/getExitCode.ts b/packages/cli/src/getExitCode.ts index ce31575..550c983 100644 --- a/packages/cli/src/getExitCode.ts +++ b/packages/cli/src/getExitCode.ts @@ -1,4 +1,6 @@ import type { CheckResult } from "@arethetypeswrong/core"; +import { problemAffectsResolutionKind } from "@arethetypeswrong/core/problems"; +import { allResolutionKinds } from "@arethetypeswrong/core/utils"; import { problemFlags } from "./problemUtils.js"; import type { RenderOptions } from "./render/index.js"; @@ -8,11 +10,13 @@ export function getExitCode(analysis: CheckResult, opts?: RenderOptions): number } const ignoreRules = opts?.ignoreRules ?? []; const ignoreResolutions = opts?.ignoreResolutions ?? []; + const requiredResolutions = allResolutionKinds.filter((kind) => !ignoreResolutions.includes(kind)); 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; + const affectsRequiredResolution = requiredResolutions.some((kind) => + problemAffectsResolutionKind(problem, kind, analysis), + ); + return notRuleIgnored && affectsRequiredResolution; }) ? 1 : 0; diff --git a/packages/cli/test/getExitCode.test.ts b/packages/cli/test/getExitCode.test.ts new file mode 100644 index 0000000..24062e3 --- /dev/null +++ b/packages/cli/test/getExitCode.test.ts @@ -0,0 +1,102 @@ +import { getExitCode } from "@arethetypeswrong/cli/internal/getExitCode"; +import { checkPackage, Package } from "@arethetypeswrong/core"; +import { problemAffectsResolutionKind } from "@arethetypeswrong/core/problems"; +import { allResolutionKinds } from "@arethetypeswrong/core/utils"; +import assert from "node:assert"; +import { describe, test } from "node:test"; + +function createPackage(files: Record) { + return new Package( + Object.fromEntries(Object.entries(files).map(([name, content]) => [`/node_modules/test/${name}`, content])), + "test", + "1.0.0", + ); +} + +function problemKinds(analysis: Awaited>) { + return analysis.types ? analysis.problems.map((problem) => problem.kind).sort() : []; +} + +// Node 10 reaches dist/index.d.ts through `types` and cannot resolve its `#lib/*` import. +const internalResolutionError = createPackage({ + "package.json": JSON.stringify({ + name: "test", + version: "1.0.0", + type: "module", + main: "./dist/index.js", + types: "./dist/index.d.ts", + exports: { ".": { types: "./dist/index.d.ts", default: "./dist/index.js" } }, + imports: { "#lib/*": "./dist/lib/*" }, + }), + "dist/index.d.ts": `export { x } from "#lib/util.js";`, + "dist/index.js": `export { x } from "#lib/util.js";`, + "dist/lib/util.d.ts": `export declare const x: number;`, + "dist/lib/util.js": `export const x = 1;`, +}); + +// The require condition pairs CommonJS types with an ES module. +const falseCjs = createPackage({ + "package.json": JSON.stringify({ + name: "test", + version: "1.0.0", + exports: { + ".": { + import: { types: "./index.d.mts", default: "./index.mjs" }, + require: { types: "./index.d.ts", default: "./index.mjs" }, + }, + }, + }), + "index.d.mts": `export declare const x: number;`, + "index.d.ts": `export declare const x: number;`, + "index.mjs": `export const x = 1;`, +}); + +// The declaration has only a default export and the JavaScript also assigns module.exports, in every resolution. +const missingExportEquals = createPackage({ + "package.json": JSON.stringify({ name: "test", version: "1.0.0", main: "./index.js" }), + "index.d.ts": `declare function f(): void;\nexport default f;`, + "index.js": `function f() {}\nmodule.exports = f;\nmodule.exports.default = f;`, +}); + +describe("getExitCode", () => { + test("ignores an InternalResolutionError visible only under ignored resolutions", async () => { + const analysis = await checkPackage(internalResolutionError); + assert.deepStrictEqual(problemKinds(analysis), ["CJSResolvesToESM", "InternalResolutionError"]); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: ["node10", "node16-cjs"] }), 0); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: ["node16-cjs"] }), 1); + assert.strictEqual(getExitCode(analysis), 1); + }); + + test("ignores a FalseCJS visible only under ignored resolutions", async () => { + const analysis = await checkPackage(falseCjs); + assert.deepStrictEqual(problemKinds(analysis), ["CJSResolvesToESM", "FalseCJS"]); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: ["node10", "node16-cjs"] }), 0); + assert.strictEqual(getExitCode(analysis), 1); + assert.strictEqual( + getExitCode(analysis, { ignoreResolutions: ["node10"], ignoreRules: ["cjs-resolves-to-esm"] }), + 1, + ); + }); + + test("counts a problem visible in both ignored and required resolutions", async () => { + const analysis = await checkPackage(missingExportEquals); + assert.deepStrictEqual(problemKinds(analysis), ["MissingExportEquals"]); + assert(analysis.types); + const [problem] = analysis.problems; + assert.deepStrictEqual( + allResolutionKinds.filter((kind) => problemAffectsResolutionKind(problem!, kind, analysis)), + allResolutionKinds, + ); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: ["node10", "node16-cjs"] }), 1); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: ["node10", "node16-cjs", "node16-esm"] }), 1); + assert.strictEqual(getExitCode(analysis, { ignoreResolutions: allResolutionKinds }), 0); + }); + + test("still applies ignored rules to required resolutions", async () => { + const analysis = await checkPackage(internalResolutionError); + assert.strictEqual( + getExitCode(analysis, { ignoreResolutions: ["node16-cjs"], ignoreRules: ["internal-resolution-error"] }), + 0, + ); + }); +});