ts-suppress 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,67 +4,88 @@ Incremental TypeScript strictness adoption via bulk error suppression.
4
4
 
5
5
  Instead of scattering `@ts-ignore` or `@ts-expect-error` comments throughout your codebase, `ts-suppress` captures all TypeScript errors into a single `.ts-suppressions.json` file. This lets you enable stricter compiler options immediately and fix errors at your own pace.
6
6
 
7
- ## How It Works
8
-
9
- Each suppression is a fingerprint of a TypeScript error, consisting of:
10
-
11
- - **file** — relative path to the source file
12
- - **code** — TypeScript error code (e.g. `2322`)
13
- - **hash** — hex hash of the diagnostic message text
14
- - **scope** — dot-separated scope chain (e.g. `MyClass.myMethod`)
15
-
16
- The `check` command diffs the current diagnostics against the suppression file and reports:
17
-
18
- - **Unsuppressed errors** — new errors not yet in the suppression file
19
- - **Stale suppressions** — entries that no longer match any current error (i.e. errors that have been fixed)
20
-
21
7
  ## Install
22
8
 
23
9
  ```bash
24
- bun add -d ts-suppress
10
+ npm install -D ts-suppress
25
11
  ```
26
12
 
27
- ## Usage
28
-
29
- ### `init`
30
-
31
- Create an empty `.ts-suppressions.json`:
32
-
33
13
  ```bash
34
- bunx ts-suppress init
14
+ pnpm add -D ts-suppress
35
15
  ```
36
16
 
37
- ### `suppress`
38
-
39
- Snapshot all current TypeScript errors into `.ts-suppressions.json`:
17
+ ```bash
18
+ yarn add -D ts-suppress
19
+ ```
40
20
 
41
21
  ```bash
42
- bunx ts-suppress suppress
22
+ bun add -d ts-suppress
43
23
  ```
44
24
 
45
- ### `check`
25
+ > **Note:** TypeScript >= 5.9.3 is a peer dependency.
46
26
 
47
- Verify that all errors are suppressed and no suppressions are stale. Exits non-zero on failure — useful in CI:
27
+ ## Usage
48
28
 
49
29
  ```bash
50
- bunx ts-suppress check
51
- ```
30
+ # Create an empty .ts-suppressions.json
31
+ npx ts-suppress init
52
32
 
53
- ### `update`
33
+ # Snapshot all current TypeScript errors
34
+ npx ts-suppress suppress
54
35
 
55
- Add new suppressions and remove stale ones in a single pass:
36
+ # Verify all errors are suppressed and no suppressions are stale (useful in CI)
37
+ npx ts-suppress check
56
38
 
57
- ```bash
58
- bunx ts-suppress update
39
+ # Add new suppressions and remove stale ones in a single pass
40
+ npx ts-suppress update
59
41
  ```
60
42
 
61
- Also available as `bunx ts-suppress fix`.
62
-
63
43
  ## Typical Workflow
64
44
 
65
45
  1. Enable a stricter TypeScript option (e.g. `"strict": true`)
66
- 2. Run `bunx ts-suppress suppress` to baseline all existing errors
46
+ 2. Run `npx ts-suppress suppress` to baseline all existing errors
67
47
  3. Commit `.ts-suppressions.json`
68
- 4. Add `bunx ts-suppress check` to CI
48
+ 4. Add `npx ts-suppress check` to CI
69
49
  5. Fix errors over time — `check` will flag stale suppressions as you go
70
- 6. Run `bunx ts-suppress update` to sync the suppression file after fixing errors
50
+ 6. Run `npx ts-suppress update` to sync the suppression file after fixing errors
51
+
52
+ ## How It Works
53
+
54
+ Each suppression is a fingerprint of a TypeScript error, consisting of:
55
+
56
+ - **file** — relative path to the source file
57
+ - **code** — TypeScript error code (e.g. `2322`)
58
+ - **hash** — hex hash of the diagnostic message text
59
+ - **scope** — dot-separated scope chain (e.g. `MyClass.myMethod`)
60
+
61
+ The `check` command diffs the current diagnostics against the suppression file and reports:
62
+
63
+ - **Unsuppressed errors** — new errors not yet in the suppression file
64
+ - **Stale suppressions** — entries that no longer match any current error (i.e. errors that have been fixed)
65
+
66
+ ## Comparison with ts-bulk-suppress
67
+
68
+ ts-suppress is inspired by [ts-bulk-suppress](https://github.com/tiktok/ts-bulk-suppress) by TikTok and shares the same core idea: capture TypeScript errors into an external file instead of scattering `@ts-ignore` comments. The two tools take different approaches to the problem.
69
+
70
+ | | ts-suppress | ts-bulk-suppress |
71
+ | ------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------- |
72
+ | **Suppression file** | Single `.ts-suppressions.json` | `.ts-bulk-suppressions.json` |
73
+ | **Error identification** | file + error code + message hash + scope | file + error code + scope |
74
+ | **tsc integration** | Standalone — reads diagnostics via TypeScript compiler API | Wraps/intercepts tsc output |
75
+ | **CLI interface** | Separate commands: `init`, `suppress`, `check`, `update` | Flag-based: `--gen-bulk-suppress`, `--changed` |
76
+ | **Runtime dependencies** | 1 (mri) + TypeScript as peer dep | 37 packages |
77
+ | **Maintenance** | Actively maintained | [Last published 2024](https://www.npmjs.com/package/ts-bulk-suppress) |
78
+
79
+ ### Key differences
80
+
81
+ - **Hash-based fingerprinting** — ts-suppress includes a SHA-256 hash of the diagnostic message text in each suppression entry. This means two errors on the same line with the same error code but different messages are tracked independently, reducing false matches.
82
+ - **No tsc patching** — ts-suppress uses the TypeScript compiler API directly to collect diagnostics rather than wrapping or intercepting tsc. This avoids coupling to tsc's output format.
83
+ - **Explicit CLI commands** — Each operation (`init`, `suppress`, `check`, `update`) is a separate command rather than a flag, making the workflow easier to script and understand.
84
+
85
+ ## Acknowledgements
86
+
87
+ Inspired by [ts-bulk-suppress](https://github.com/tiktok/ts-bulk-suppress) by TikTok.
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/ast.js ADDED
@@ -0,0 +1,11 @@
1
+ import ts from "typescript";
2
+ /** Find the most specific (deepest) AST node at the given position in a source file. */
3
+ export function findNodeAtPosition(sourceFile, position) {
4
+ function visit(node) {
5
+ if (position >= node.getStart(sourceFile) && position < node.getEnd()) {
6
+ return ts.forEachChild(node, visit) ?? node;
7
+ }
8
+ return undefined;
9
+ }
10
+ return visit(sourceFile);
11
+ }
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
- import { parse } from "@bomb.sh/args";
1
+ #!/usr/bin/env node
2
+ import mri from "mri";
2
3
  import { createProject } from "./project.js";
3
4
  import { runCheck } from "./commands/check.js";
4
5
  import { runInit } from "./commands/init.js";
@@ -16,7 +17,7 @@ function printHelp() {
16
17
  const lines = commands.map(([name, desc]) => ` ${name.padEnd(longest + 4)}${desc}`);
17
18
  console.log(`ts-suppress v${VERSION}\nIncremental TypeScript strictness adoption via bulk error suppression\n\nCommands:\n${lines.join("\n")}\n\nRun ts-suppress <command> --help for details.`);
18
19
  }
19
- const args = parse(process.argv.slice(2), {
20
+ const args = mri(process.argv.slice(2), {
20
21
  boolean: ["help", "version"],
21
22
  alias: { h: "help", v: "version" },
22
23
  });
@@ -2,4 +2,5 @@ import { writeSuppressions, SUPPRESSIONS_FILENAME } from "../suppressions.js";
2
2
  export async function runInit() {
3
3
  await writeSuppressions(process.cwd(), []);
4
4
  console.log(`Created ${SUPPRESSIONS_FILENAME}`);
5
+ console.log(`\nTip: Add ${SUPPRESSIONS_FILENAME} to your formatter's ignore list (e.g. .prettierignore, .oxfmtignore) to preserve its compact format.`);
5
6
  }
@@ -2,7 +2,6 @@ import { collectDiagnostics } from "../diagnostics.js";
2
2
  import { writeSuppressions, SUPPRESSIONS_FILENAME } from "../suppressions.js";
3
3
  /**
4
4
  * Core logic, extracted for testability.
5
- * Accepts a ts-morph Project and roots separately so tests can pass in-memory projects.
6
5
  * outputRoot is where the suppression file is written (may differ from projectRoot in tests).
7
6
  */
8
7
  export async function runSuppress(project, projectRoot, outputRoot = projectRoot) {
@@ -1,25 +1,31 @@
1
+ import ts from "typescript";
1
2
  import { relative } from "node:path";
2
3
  import { hashMessage } from "./hash.js";
3
4
  import { buildScopePath } from "./scope.js";
5
+ import { findNodeAtPosition } from "./ast.js";
6
+ // Only the top-level message is used for fingerprinting; chained sub-messages
7
+ // are diagnostic detail that varies with context and would produce unstable hashes.
8
+ function flattenDiagnosticMessage(messageText) {
9
+ return typeof messageText === "string" ? messageText : messageText.messageText;
10
+ }
4
11
  /**
5
- * Collect all pre-emit diagnostics from a ts-morph Project as Suppression fingerprints.
12
+ * Collect all pre-emit diagnostics from a TypeScript Program as Suppression fingerprints.
6
13
  * Project creation is the caller's responsibility — this enables in-memory testing.
7
14
  */
8
15
  export function collectDiagnostics(project, projectRoot) {
9
- const diagnostics = project.getPreEmitDiagnostics();
16
+ const diagnostics = ts.getPreEmitDiagnostics(project.program);
10
17
  const suppressions = [];
11
18
  for (const diag of diagnostics) {
12
- const sourceFile = diag.getSourceFile();
19
+ const sourceFile = diag.file;
13
20
  if (!sourceFile)
14
21
  continue;
15
- const filePath = relative(projectRoot, sourceFile.getFilePath());
16
- const code = diag.getCode();
17
- const messageText = diag.getMessageText();
18
- const message = typeof messageText === "string" ? messageText : messageText.getMessageText();
19
- const start = diag.getStart();
22
+ const filePath = relative(projectRoot, sourceFile.fileName);
23
+ const code = diag.code;
24
+ const message = flattenDiagnosticMessage(diag.messageText);
25
+ const start = diag.start;
20
26
  let scope = "";
21
27
  if (start != null) {
22
- const node = sourceFile.getDescendantAtPos(start);
28
+ const node = findNodeAtPosition(sourceFile, start);
23
29
  if (node) {
24
30
  scope = buildScopePath(node);
25
31
  }
package/dist/project.js CHANGED
@@ -1,4 +1,3 @@
1
- import { Project } from "ts-morph";
2
1
  import ts from "typescript";
3
2
  import { dirname } from "node:path";
4
3
  /**
@@ -6,19 +5,27 @@ import { dirname } from "node:path";
6
5
  * Uses TypeScript's own findConfigFile for correct resolution behavior.
7
6
  */
8
7
  export function findTsConfig(cwd) {
9
- const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
8
+ const configPath = ts.findConfigFile(cwd, (f) => ts.sys.fileExists(f), "tsconfig.json");
10
9
  if (!configPath) {
11
10
  throw new Error(`No tsconfig.json found starting from ${cwd}`);
12
11
  }
13
12
  return configPath;
14
13
  }
15
14
  /**
16
- * Create a ts-morph Project from the nearest tsconfig.json.
17
- * Returns the Project and the resolved project root (directory containing tsconfig.json).
15
+ * Create a TypeScript Program from the nearest tsconfig.json.
16
+ * Returns the Program and the resolved project root (directory containing tsconfig.json).
18
17
  */
19
18
  export function createProject(cwd) {
20
19
  const tsConfigFilePath = findTsConfig(cwd);
21
20
  const projectRoot = dirname(tsConfigFilePath);
22
- const project = new Project({ tsConfigFilePath });
23
- return { project, projectRoot };
21
+ const configFile = ts.readConfigFile(tsConfigFilePath, (f) => ts.sys.readFile(f));
22
+ if (configFile.error) {
23
+ throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
24
+ }
25
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectRoot);
26
+ if (parsed.errors.length > 0) {
27
+ throw new Error(ts.flattenDiagnosticMessageText(parsed.errors[0].messageText, "\n"));
28
+ }
29
+ const program = ts.createProgram(parsed.fileNames, parsed.options);
30
+ return { project: { program }, projectRoot };
24
31
  }
package/dist/scope.js CHANGED
@@ -1,5 +1,4 @@
1
- // src/scope.ts
2
- import { Node } from "ts-morph";
1
+ import ts from "typescript";
3
2
  /**
4
3
  * Build a dot-separated scope path by walking up the AST from a node.
5
4
  * Returns empty string for module-level code.
@@ -19,34 +18,36 @@ export function buildScopePath(node) {
19
18
  if (name != null) {
20
19
  parts.unshift(name);
21
20
  }
22
- current = current.getParent();
21
+ current = current.parent;
23
22
  }
24
23
  return parts.join(".");
25
24
  }
26
25
  function getScopeName(node) {
27
- if (Node.isFunctionDeclaration(node)) {
28
- return node.getName() ?? null;
26
+ if (ts.isFunctionDeclaration(node)) {
27
+ return node.name?.text ?? null;
29
28
  }
30
- if (Node.isMethodDeclaration(node)) {
31
- return node.getName();
29
+ if (ts.isMethodDeclaration(node)) {
30
+ return ts.isIdentifier(node.name) ? node.name.text : node.name.getText();
32
31
  }
33
- if (Node.isClassDeclaration(node)) {
34
- return node.getName() ?? null;
32
+ if (ts.isClassDeclaration(node)) {
33
+ return node.name?.text ?? null;
35
34
  }
36
- if (Node.isGetAccessorDeclaration(node)) {
37
- return `get:${node.getName()}`;
35
+ if (ts.isGetAccessorDeclaration(node)) {
36
+ const name = ts.isIdentifier(node.name) ? node.name.text : node.name.getText();
37
+ return `get:${name}`;
38
38
  }
39
- if (Node.isSetAccessorDeclaration(node)) {
40
- return `set:${node.getName()}`;
39
+ if (ts.isSetAccessorDeclaration(node)) {
40
+ const name = ts.isIdentifier(node.name) ? node.name.text : node.name.getText();
41
+ return `set:${name}`;
41
42
  }
42
- if (Node.isConstructorDeclaration(node)) {
43
+ if (ts.isConstructorDeclaration(node)) {
43
44
  return "constructor";
44
45
  }
45
46
  // Arrow function or function expression assigned to a variable
46
- if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) {
47
- const parent = node.getParent();
48
- if (parent && Node.isVariableDeclaration(parent)) {
49
- return parent.getName();
47
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
48
+ const parent = node.parent;
49
+ if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
50
+ return parent.name.text;
50
51
  }
51
52
  return null; // anonymous, no scope name
52
53
  }
@@ -42,8 +42,9 @@ export async function readSuppressions(projectRoot) {
42
42
  export async function writeSuppressions(projectRoot, suppressions) {
43
43
  const filePath = resolve(projectRoot, SUPPRESSIONS_FILENAME);
44
44
  const sorted = [...suppressions].sort(compareSuppression);
45
- const data = { suppressions: sorted };
46
- await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
45
+ const lines = sorted.map((s) => " " + JSON.stringify(s));
46
+ const content = `{"suppressions": [\n${lines.join(",\n")}\n]}\n`;
47
+ await writeFile(filePath, content);
47
48
  }
48
49
  /**
49
50
  * Diff existing suppressions against current diagnostics.
@@ -0,0 +1,36 @@
1
+ import ts from "typescript";
2
+ export function createInMemoryProject(files) {
3
+ const fileMap = new Map();
4
+ const fileNames = [];
5
+ for (const [name, content] of Object.entries(files)) {
6
+ const fullPath = `/${name}`;
7
+ fileMap.set(fullPath, content);
8
+ fileNames.push(fullPath);
9
+ }
10
+ const options = {
11
+ strict: true,
12
+ target: ts.ScriptTarget.ESNext,
13
+ lib: ["lib.esnext.d.ts"],
14
+ moduleDetection: ts.ModuleDetectionKind.Force,
15
+ types: [],
16
+ };
17
+ const host = ts.createCompilerHost(options);
18
+ const originalGetSourceFile = host.getSourceFile.bind(host);
19
+ const originalFileExists = host.fileExists.bind(host);
20
+ const originalReadFile = host.readFile.bind(host);
21
+ host.getSourceFile = (fileName, languageVersion) => {
22
+ const content = fileMap.get(fileName);
23
+ if (content != null) {
24
+ return ts.createSourceFile(fileName, content, languageVersion, true);
25
+ }
26
+ return originalGetSourceFile(fileName, languageVersion);
27
+ };
28
+ host.fileExists = (fileName) => {
29
+ return fileMap.has(fileName) || originalFileExists(fileName);
30
+ };
31
+ host.readFile = (fileName) => {
32
+ return fileMap.get(fileName) ?? originalReadFile(fileName);
33
+ };
34
+ const program = ts.createProgram(fileNames, options, host);
35
+ return { program };
36
+ }
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- // src/types.ts
2
1
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-suppress",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Incremental TypeScript strictness adoption via bulk error suppression",
5
5
  "keywords": [
6
6
  "migration",
@@ -31,24 +31,10 @@
31
31
  "!skills/_artifacts"
32
32
  ],
33
33
  "type": "module",
34
- "scripts": {
35
- "build": "tsc -p tsconfig.build.json",
36
- "test": "bun test",
37
- "prepublishOnly": "bun run build",
38
- "fmt": "oxfmt",
39
- "fmt:check": "oxfmt --check",
40
- "knip": "knip",
41
- "lint": "oxlint",
42
- "lint:fix": "oxlint --fix",
43
- "prepare": "husky",
44
- "typecheck": "tsc --noEmit"
45
- },
46
34
  "dependencies": {
47
- "@bomb.sh/args": "^0.3.1",
48
- "ts-morph": "^27.0.2"
35
+ "mri": "^1.2.0"
49
36
  },
50
37
  "devDependencies": {
51
- "@types/bun": "latest",
52
38
  "@types/node": "^24",
53
39
  "husky": "^9.1.7",
54
40
  "knip": "^5.87.0",
@@ -56,13 +42,21 @@
56
42
  "oxfmt": "^0.41.0",
57
43
  "oxlint": "^1.56.0",
58
44
  "oxlint-tsgolint": "^0.17.0",
59
- "typescript": "^5.9.3"
45
+ "tsx": "^4.19.4",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^3.2.1"
60
48
  },
61
49
  "peerDependencies": {
62
50
  "typescript": "^5.9.3"
63
51
  },
64
- "lint-staged": {
65
- "*.{js,jsx,ts,tsx,mjs,cjs}": "bun run lint",
66
- "*": "oxfmt --no-error-on-unmatched-pattern"
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json",
54
+ "test": "vitest run",
55
+ "fmt": "oxfmt",
56
+ "fmt:check": "oxfmt --check",
57
+ "knip": "knip",
58
+ "lint": "oxlint",
59
+ "lint:fix": "oxlint --fix",
60
+ "typecheck": "tsc --noEmit"
67
61
  }
68
- }
62
+ }