diff --git a/.changeset/nervous-carrots-yell.md b/.changeset/nervous-carrots-yell.md new file mode 100644 index 0000000..75cc323 --- /dev/null +++ b/.changeset/nervous-carrots-yell.md @@ -0,0 +1,5 @@ +--- +"@arethetypeswrong/cli": patch +--- + +Expose internal renderer and exit code API diff --git a/packages/cli/package.json b/packages/cli/package.json index 933d188..42c9412 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,11 +21,17 @@ }, "files": [ "LICENSE", - "dist/**/*.js" + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts" ], "bin": { "attw": "./dist/index.js" }, + "exports": { + "./internal/getExitCode": "./dist/getExitCode.js", + "./internal/render": "./dist/render/index.js" + }, "publishConfig": { "access": "public" }, @@ -42,8 +48,8 @@ "@types/marked-terminal": "^3.1.3", "@types/node": "^20.2.5", "@types/semver": "^7.5.3", - "ts-expose-internals": "5.3.2", - "typescript": "5.3.2" + "ts-expose-internals-conditionally": "1.0.0-empty.0", + "typescript": "5.3.3" }, "dependencies": { "@arethetypeswrong/core": "0.13.6", diff --git a/packages/cli/src/getExitCode.ts b/packages/cli/src/getExitCode.ts new file mode 100644 index 0000000..61078c4 --- /dev/null +++ b/packages/cli/src/getExitCode.ts @@ -0,0 +1,13 @@ +import type { CheckResult } from "@arethetypeswrong/core"; +import { problemFlags } from "./problemUtils.js"; +import type { RenderOptions } from "./render/index.js"; + +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; +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 779e64b..b722758 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,25 +14,25 @@ import { problemFlags } from "./problemUtils.js"; import { readConfig } from "./readConfig.js"; import * as render from "./render/index.js"; import { major, minor } from "semver"; +import { getExitCode } from "./getExitCode.js"; const packageJson = createRequire(import.meta.url)("../package.json"); const version = packageJson.version; -const formats = ["auto", "table", "table-flipped", "ascii", "json"] as const; +const formats = Object.keys({ + auto: true, + json: true, + ascii: true, + table: true, + "table-flipped": true, +} satisfies Record) as render.Format[]; -type Format = (typeof formats)[number]; - -export interface Opts { +interface Opts extends render.RenderOptions { pack?: boolean; fromNpm?: boolean; definitelyTyped?: boolean | string; - summary?: boolean; - emoji?: boolean; - color?: boolean; quiet?: boolean; configPath?: string; - ignoreRules?: string[]; - format: Format; entrypoints?: string[]; includeEntrypoints?: string[]; @@ -81,9 +81,6 @@ particularly ESM-related module resolution issues.`, .action(async (fileOrDirectory = ".") => { const opts = program.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 = () => {}; @@ -210,21 +207,24 @@ particularly ESM-related module resolution issues.`, console.log(JSON.stringify(result)); - if (analysis.types && analysis.problems.some((problem) => !opts.ignoreRules?.includes(problem.kind))) - process.exit(1); + if (deleteTgz) { + await unlink(deleteTgz); + } + + const exitCode = getExitCode(analysis, opts); + if (exitCode) { + process.exit(exitCode); + } return; } console.log(); if (analysis.types) { - await render.typed(analysis, opts); - - if (analysis.types && analysis.problems.some((problem) => !opts.ignoreRules?.includes(problem.kind))) { - process.exitCode = 1; - } + console.log(await render.typed(analysis, opts)); + process.exitCode = getExitCode(analysis, opts); } else { - render.untyped(analysis as core.UntypedResult); + console.log(render.untyped(analysis as core.UntypedResult)); } if (deleteTgz) { diff --git a/packages/cli/src/problemUtils.ts b/packages/cli/src/problemUtils.ts index 1c76153..f9eefe8 100644 --- a/packages/cli/src/problemUtils.ts +++ b/packages/cli/src/problemUtils.ts @@ -1,7 +1,7 @@ import * as core from "@arethetypeswrong/core"; import type { ProblemKind } from "@arethetypeswrong/core"; -export const problemFlags: Record = { +export const problemFlags = { NoResolution: "no-resolution", UntypedResolution: "untyped-resolution", FalseCJS: "false-cjs", @@ -13,7 +13,7 @@ export const problemFlags: Record = { MissingExportEquals: "missing-export-equals", UnexpectedModuleSyntax: "unexpected-module-syntax", InternalResolutionError: "internal-resolution-error", -}; +} as const satisfies Record; export const resolutionKinds: Record = { node10: "node10", diff --git a/packages/cli/src/render/index.ts b/packages/cli/src/render/index.ts index 28db131..955c11b 100644 --- a/packages/cli/src/render/index.ts +++ b/packages/cli/src/render/index.ts @@ -1,2 +1,13 @@ +import type { problemFlags } from "../problemUtils.js"; + +export type Format = "auto" | "table" | "table-flipped" | "ascii" | "json"; +export interface RenderOptions { + ignoreRules?: (typeof problemFlags)[keyof typeof problemFlags][]; + format?: Format; + color?: boolean; + summary?: boolean; + emoji?: boolean; +} + export * from "./typed.js"; export * from "./untyped.js"; diff --git a/packages/cli/src/render/typed.ts b/packages/cli/src/render/typed.ts index 607a3c3..9c2f171 100644 --- a/packages/cli/src/render/typed.ts +++ b/packages/cli/src/render/typed.ts @@ -1,48 +1,44 @@ import * as core from "@arethetypeswrong/core"; +import { filterProblems, problemAffectsEntrypoint, 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"; import { marked } from "marked"; - -import { filterProblems, problemAffectsEntrypoint, problemKindInfo } from "@arethetypeswrong/core/problems"; -import type { Opts } from "../index.js"; +import TerminalRenderer from "marked-terminal"; import { moduleKinds, problemFlags, resolutionKinds } from "../problemUtils.js"; import { asciiTable } from "./asciiTable.js"; -import TerminalRenderer from "marked-terminal"; +import type { RenderOptions } from "./index.js"; -export async function typed(analysis: core.Analysis, opts: Opts) { - const problems = analysis.problems.filter((problem) => !opts.ignoreRules || !opts.ignoreRules.includes(problem.kind)); +export async function typed(analysis: core.Analysis, opts: RenderOptions): Promise { + let output = ""; + const problems = analysis.problems.filter( + (problem) => !opts.ignoreRules || !opts.ignoreRules.includes(problemFlags[problem.kind]), + ); const grouped = groupProblemsByKind(problems); const entrypoints = Object.keys(analysis.entrypoints); marked.setOptions({ renderer: new TerminalRenderer(), }); - console.log(`${analysis.packageName} v${analysis.packageVersion}`); + out(`${analysis.packageName} v${analysis.packageVersion}`); if (analysis.types.kind === "@types") { - console.log(`${analysis.types.packageName} v${analysis.types.packageVersion}`); + out(`${analysis.types.packageName} v${analysis.types.packageVersion}`); } - console.log(); + out(); if (Object.keys(analysis.buildTools).length) { - console.log("Build tools:"); - console.log( + out("Build tools:"); + out( Object.entries(analysis.buildTools) .map(([tool, version]) => { return `- ${tool}@${version}`; }) .join("\n"), ); - console.log(); + out(); } if (opts.ignoreRules && opts.ignoreRules.length) { - console.log( - chalk.gray( - ` (ignoring rules: ${opts.ignoreRules - .map((rule) => `'${problemFlags[rule as core.ProblemKind]}'`) - .join(", ")})\n`, - ), - ); + out(chalk.gray(` (ignoring rules: ${opts.ignoreRules.map((rule) => `'${rule}'`).join(", ")})\n`)); } if (opts.summary) { @@ -54,7 +50,7 @@ export async function typed(analysis: core.Analysis, opts: Opts) { return `${emoji}${description}`; }); - console.log(summaryTexts.join("") || defaultSummary); + out(summaryTexts.join("") || defaultSummary); } const entrypointNames = entrypoints.map( @@ -119,25 +115,31 @@ export async function typed(analysis: core.Analysis, opts: Opts) { switch (opts.format) { case "table": - console.log(table!.toString()); + out(table!.toString()); break; case "table-flipped": - console.log(flippedTable!.toString()); + out(flippedTable!.toString()); break; case "ascii": - console.log(asciiTable(table!)); + out(asciiTable(table!)); break; case "auto": const terminalWidth = process.stdout.columns || 133; // This looks like GitHub Actions' width if (table!.width <= terminalWidth) { - console.log(table!.toString()); + out(table!.toString()); } else if (flippedTable!.width <= terminalWidth) { - console.log(flippedTable!.toString()); + out(flippedTable!.toString()); } else { - console.log(asciiTable(table!)); + out(asciiTable(table!)); } break; } + + return output.trimEnd(); + + function out(s: string = "") { + output += s + "\n"; + } } function memo(fn: (...args: Args) => Result): (...args: Args) => Result { diff --git a/packages/cli/src/render/untyped.ts b/packages/cli/src/render/untyped.ts index 819c1b6..89254d2 100644 --- a/packages/cli/src/render/untyped.ts +++ b/packages/cli/src/render/untyped.ts @@ -1,5 +1,5 @@ import * as core from "@arethetypeswrong/core"; export function untyped(analysis: core.UntypedResult) { - console.log("This package does not contain types.\nDetails: ", analysis); + return "This package does not contain types.\nDetails: " + JSON.stringify(analysis, null, 2); } diff --git a/packages/cli/test/snapshots/ejs@3.1.9.tgz.md b/packages/cli/test/snapshots/ejs@3.1.9.tgz.md index 2ef0ca0..7bdeed1 100644 --- a/packages/cli/test/snapshots/ejs@3.1.9.tgz.md +++ b/packages/cli/test/snapshots/ejs@3.1.9.tgz.md @@ -5,7 +5,11 @@ $ attw ejs@3.1.9.tgz -f table-flipped This package does not contain types. -Details: { packageName: 'ejs', packageVersion: '3.1.9', types: false } +Details: { + "packageName": "ejs", + "packageVersion": "3.1.9", + "types": false +} ``` diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 1a5b1f4..de50a14 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,9 +3,8 @@ "compilerOptions": { "module": "nodenext", "rootDir": "src", - "types": ["ts-expose-internals", "node"], + "types": ["ts-expose-internals-conditionally", "node"], "outDir": "./dist", - "declarationDir": "./lib", "sourceMap": true }, "include": ["src"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fc95ff..69bde91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,12 +57,12 @@ importers: '@types/semver': specifier: ^7.5.3 version: 7.5.3 - ts-expose-internals: - specifier: 5.3.2 - version: 5.3.2 + ts-expose-internals-conditionally: + specifier: 5.3.3 + version: 5.3.3 typescript: - specifier: 5.3.2 - version: 5.3.2 + specifier: 5.3.3 + version: 5.3.3 packages/core: dependencies: @@ -3640,11 +3640,6 @@ packages: /ts-expose-internals-conditionally@5.3.3: resolution: {integrity: sha512-k3wz3FnxrtuD8ue3Y9GmqEsph3PB1YCZs6zWy6d6T60A9/0zgy3w0wQBQOdKPoyWN0T+JQCbY5cE8Cg80VlzNw==} - dev: false - - /ts-expose-internals@5.3.2: - resolution: {integrity: sha512-lKFeJay+cwMvqfvf+Uv/+pkTwKc4OOtjEPjxKMUUcVaqKGlTEuN3yssT29VG0maDb0QbpAGb6iDzKj7/cf4WWQ==} - dev: true /ts-expose-internals@5.3.3: resolution: {integrity: sha512-0lW96u0pa08dq9QLYUBRbgGkvsD+LvPAVGe94kr2tHaRZAxgckwJ+qqc1BSRoR1rpkTZf7AWAa6DKrLGFmctwg==} @@ -3757,12 +3752,6 @@ packages: resolution: {integrity: sha512-+wpbd+gNRNaHeYdJESWL0dh0SbjitfegVUCpLavYUJ0NEc149r3+NYZz2Wv2ttB+80ZJXjLpLXKBNB/E87wyyA==} dev: true - /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - /typescript@5.3.3: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'}