ts-suppress 0.2.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/cli.js +63 -0
- package/dist/commands/check.js +28 -0
- package/dist/commands/init.js +5 -0
- package/dist/commands/suppress.js +12 -0
- package/dist/commands/update.js +23 -0
- package/dist/diagnostics.js +35 -0
- package/dist/hash.js +5 -0
- package/dist/project.js +24 -0
- package/dist/scope.js +54 -0
- package/dist/suppressions.js +101 -0
- package/dist/types.js +2 -0
- package/package.json +68 -0
- package/skills/ts-suppress/SKILL.md +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 br0p0p
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ts-suppress
|
|
2
|
+
|
|
3
|
+
Incremental TypeScript strictness adoption via bulk error suppression.
|
|
4
|
+
|
|
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
|
+
|
|
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
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add -d ts-suppress
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### `init`
|
|
30
|
+
|
|
31
|
+
Create an empty `.ts-suppressions.json`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bunx ts-suppress init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `suppress`
|
|
38
|
+
|
|
39
|
+
Snapshot all current TypeScript errors into `.ts-suppressions.json`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bunx ts-suppress suppress
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `check`
|
|
46
|
+
|
|
47
|
+
Verify that all errors are suppressed and no suppressions are stale. Exits non-zero on failure — useful in CI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bunx ts-suppress check
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `update`
|
|
54
|
+
|
|
55
|
+
Add new suppressions and remove stale ones in a single pass:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bunx ts-suppress update
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Also available as `bunx ts-suppress fix`.
|
|
62
|
+
|
|
63
|
+
## Typical Workflow
|
|
64
|
+
|
|
65
|
+
1. Enable a stricter TypeScript option (e.g. `"strict": true`)
|
|
66
|
+
2. Run `bunx ts-suppress suppress` to baseline all existing errors
|
|
67
|
+
3. Commit `.ts-suppressions.json`
|
|
68
|
+
4. Add `bunx ts-suppress check` to CI
|
|
69
|
+
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
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parse } from "@bomb.sh/args";
|
|
2
|
+
import { createProject } from "./project.js";
|
|
3
|
+
import { runCheck } from "./commands/check.js";
|
|
4
|
+
import { runInit } from "./commands/init.js";
|
|
5
|
+
import { runSuppress } from "./commands/suppress.js";
|
|
6
|
+
import { runUpdate } from "./commands/update.js";
|
|
7
|
+
const VERSION = "0.2.0";
|
|
8
|
+
const commands = [
|
|
9
|
+
["init", "Create an empty .ts-suppressions.json file"],
|
|
10
|
+
["suppress", "Snapshot all current TypeScript errors into .ts-suppressions.json"],
|
|
11
|
+
["update", "Add new suppressions and remove stale ones (alias: fix)"],
|
|
12
|
+
["check", "Check for unsuppressed errors and stale suppressions"],
|
|
13
|
+
];
|
|
14
|
+
function printHelp() {
|
|
15
|
+
const longest = Math.max(...commands.map(([name]) => name.length));
|
|
16
|
+
const lines = commands.map(([name, desc]) => ` ${name.padEnd(longest + 4)}${desc}`);
|
|
17
|
+
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
|
+
const args = parse(process.argv.slice(2), {
|
|
20
|
+
boolean: ["help", "version"],
|
|
21
|
+
alias: { h: "help", v: "version" },
|
|
22
|
+
});
|
|
23
|
+
const command = args._[0];
|
|
24
|
+
if (args.version) {
|
|
25
|
+
console.log(VERSION);
|
|
26
|
+
}
|
|
27
|
+
else if (args.help || (!command && process.argv.length <= 2)) {
|
|
28
|
+
printHelp();
|
|
29
|
+
}
|
|
30
|
+
else if (command) {
|
|
31
|
+
switch (command) {
|
|
32
|
+
case "init": {
|
|
33
|
+
await runInit();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "suppress": {
|
|
37
|
+
const { project, projectRoot } = createProject(process.cwd());
|
|
38
|
+
await runSuppress(project, projectRoot);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "update":
|
|
42
|
+
case "fix": {
|
|
43
|
+
const { project, projectRoot } = createProject(process.cwd());
|
|
44
|
+
await runUpdate(project, projectRoot);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "check": {
|
|
48
|
+
const { project, projectRoot } = createProject(process.cwd());
|
|
49
|
+
const { exitCode } = await runCheck(project, projectRoot);
|
|
50
|
+
if (exitCode !== 0)
|
|
51
|
+
process.exit(exitCode);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
default: {
|
|
55
|
+
console.error(`Unknown command: ${command}`);
|
|
56
|
+
printHelp();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
printHelp();
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { collectDiagnostics } from "../diagnostics.js";
|
|
2
|
+
import { readSuppressions, diffSuppressions } from "../suppressions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Core logic, extracted for testability.
|
|
5
|
+
* suppressionsRoot is where the suppression file is read from (may differ from projectRoot in tests).
|
|
6
|
+
*/
|
|
7
|
+
export async function runCheck(project, projectRoot, suppressionsRoot = projectRoot) {
|
|
8
|
+
const existing = await readSuppressions(suppressionsRoot);
|
|
9
|
+
const current = collectDiagnostics(project, projectRoot);
|
|
10
|
+
const { unsuppressed, stale } = diffSuppressions(existing, current);
|
|
11
|
+
if (unsuppressed.length > 0) {
|
|
12
|
+
console.error(`\n${unsuppressed.length} unsuppressed error(s):\n`);
|
|
13
|
+
for (const s of unsuppressed) {
|
|
14
|
+
console.error(` TS${s.code} in ${s.file}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (stale.length > 0) {
|
|
18
|
+
console.error(`\n${stale.length} stale suppression(s):\n`);
|
|
19
|
+
for (const s of stale) {
|
|
20
|
+
console.error(` TS${s.code} in ${s.file}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const exitCode = unsuppressed.length > 0 || stale.length > 0 ? 1 : 0;
|
|
24
|
+
if (exitCode === 0) {
|
|
25
|
+
console.log("No unsuppressed errors or stale suppressions.");
|
|
26
|
+
}
|
|
27
|
+
return { exitCode, unsuppressed, stale };
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { collectDiagnostics } from "../diagnostics.js";
|
|
2
|
+
import { writeSuppressions, SUPPRESSIONS_FILENAME } from "../suppressions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Core logic, extracted for testability.
|
|
5
|
+
* Accepts a ts-morph Project and roots separately so tests can pass in-memory projects.
|
|
6
|
+
* outputRoot is where the suppression file is written (may differ from projectRoot in tests).
|
|
7
|
+
*/
|
|
8
|
+
export async function runSuppress(project, projectRoot, outputRoot = projectRoot) {
|
|
9
|
+
const diagnostics = collectDiagnostics(project, projectRoot);
|
|
10
|
+
await writeSuppressions(outputRoot, diagnostics);
|
|
11
|
+
console.log(`Wrote ${diagnostics.length} suppression(s) to ${SUPPRESSIONS_FILENAME}`);
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { collectDiagnostics } from "../diagnostics.js";
|
|
2
|
+
import { readSuppressions, writeSuppressions, diffSuppressions } from "../suppressions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Core logic, extracted for testability.
|
|
5
|
+
* Reads existing suppressions, diffs against current diagnostics,
|
|
6
|
+
* writes the updated file, and reports what changed.
|
|
7
|
+
*/
|
|
8
|
+
export async function runUpdate(project, projectRoot, outputRoot = projectRoot) {
|
|
9
|
+
const existing = await readSuppressions(outputRoot);
|
|
10
|
+
const current = collectDiagnostics(project, projectRoot);
|
|
11
|
+
const { unsuppressed: added, stale: removed } = diffSuppressions(existing, current);
|
|
12
|
+
await writeSuppressions(outputRoot, current);
|
|
13
|
+
if (added.length > 0) {
|
|
14
|
+
console.log(`Added ${added.length} new suppression(s)`);
|
|
15
|
+
}
|
|
16
|
+
if (removed.length > 0) {
|
|
17
|
+
console.log(`Removed ${removed.length} stale suppression(s)`);
|
|
18
|
+
}
|
|
19
|
+
if (added.length === 0 && removed.length === 0) {
|
|
20
|
+
console.log("Already up to date.");
|
|
21
|
+
}
|
|
22
|
+
return { added, removed, total: current.length };
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { hashMessage } from "./hash.js";
|
|
3
|
+
import { buildScopePath } from "./scope.js";
|
|
4
|
+
/**
|
|
5
|
+
* Collect all pre-emit diagnostics from a ts-morph Project as Suppression fingerprints.
|
|
6
|
+
* Project creation is the caller's responsibility — this enables in-memory testing.
|
|
7
|
+
*/
|
|
8
|
+
export function collectDiagnostics(project, projectRoot) {
|
|
9
|
+
const diagnostics = project.getPreEmitDiagnostics();
|
|
10
|
+
const suppressions = [];
|
|
11
|
+
for (const diag of diagnostics) {
|
|
12
|
+
const sourceFile = diag.getSourceFile();
|
|
13
|
+
if (!sourceFile)
|
|
14
|
+
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();
|
|
20
|
+
let scope = "";
|
|
21
|
+
if (start != null) {
|
|
22
|
+
const node = sourceFile.getDescendantAtPos(start);
|
|
23
|
+
if (node) {
|
|
24
|
+
scope = buildScopePath(node);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
suppressions.push({
|
|
28
|
+
file: filePath,
|
|
29
|
+
code,
|
|
30
|
+
hash: hashMessage(message),
|
|
31
|
+
scope,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return suppressions;
|
|
35
|
+
}
|
package/dist/hash.js
ADDED
package/dist/project.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Project } from "ts-morph";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Find the nearest tsconfig.json by walking up from the given directory.
|
|
6
|
+
* Uses TypeScript's own findConfigFile for correct resolution behavior.
|
|
7
|
+
*/
|
|
8
|
+
export function findTsConfig(cwd) {
|
|
9
|
+
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
|
|
10
|
+
if (!configPath) {
|
|
11
|
+
throw new Error(`No tsconfig.json found starting from ${cwd}`);
|
|
12
|
+
}
|
|
13
|
+
return configPath;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a ts-morph Project from the nearest tsconfig.json.
|
|
17
|
+
* Returns the Project and the resolved project root (directory containing tsconfig.json).
|
|
18
|
+
*/
|
|
19
|
+
export function createProject(cwd) {
|
|
20
|
+
const tsConfigFilePath = findTsConfig(cwd);
|
|
21
|
+
const projectRoot = dirname(tsConfigFilePath);
|
|
22
|
+
const project = new Project({ tsConfigFilePath });
|
|
23
|
+
return { project, projectRoot };
|
|
24
|
+
}
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/scope.ts
|
|
2
|
+
import { Node } from "ts-morph";
|
|
3
|
+
/**
|
|
4
|
+
* Build a dot-separated scope path by walking up the AST from a node.
|
|
5
|
+
* Returns empty string for module-level code.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - "MyClass.myMethod" for a method inside a class
|
|
9
|
+
* - "processData" for a top-level function
|
|
10
|
+
* - "handler" for an arrow function assigned to a const
|
|
11
|
+
* - "MyClass.get:name" for a getter
|
|
12
|
+
* - "" for module scope
|
|
13
|
+
*/
|
|
14
|
+
export function buildScopePath(node) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
let current = node;
|
|
17
|
+
while (current) {
|
|
18
|
+
const name = getScopeName(current);
|
|
19
|
+
if (name != null) {
|
|
20
|
+
parts.unshift(name);
|
|
21
|
+
}
|
|
22
|
+
current = current.getParent();
|
|
23
|
+
}
|
|
24
|
+
return parts.join(".");
|
|
25
|
+
}
|
|
26
|
+
function getScopeName(node) {
|
|
27
|
+
if (Node.isFunctionDeclaration(node)) {
|
|
28
|
+
return node.getName() ?? null;
|
|
29
|
+
}
|
|
30
|
+
if (Node.isMethodDeclaration(node)) {
|
|
31
|
+
return node.getName();
|
|
32
|
+
}
|
|
33
|
+
if (Node.isClassDeclaration(node)) {
|
|
34
|
+
return node.getName() ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (Node.isGetAccessorDeclaration(node)) {
|
|
37
|
+
return `get:${node.getName()}`;
|
|
38
|
+
}
|
|
39
|
+
if (Node.isSetAccessorDeclaration(node)) {
|
|
40
|
+
return `set:${node.getName()}`;
|
|
41
|
+
}
|
|
42
|
+
if (Node.isConstructorDeclaration(node)) {
|
|
43
|
+
return "constructor";
|
|
44
|
+
}
|
|
45
|
+
// 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();
|
|
50
|
+
}
|
|
51
|
+
return null; // anonymous, no scope name
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFile, writeFile, access } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export const SUPPRESSIONS_FILENAME = ".ts-suppressions.json";
|
|
4
|
+
/** Compare function for deterministic sorting of suppressions */
|
|
5
|
+
function compareSuppression(a, b) {
|
|
6
|
+
return (a.file.localeCompare(b.file) ||
|
|
7
|
+
a.code - b.code ||
|
|
8
|
+
a.hash.localeCompare(b.hash) ||
|
|
9
|
+
a.scope.localeCompare(b.scope));
|
|
10
|
+
}
|
|
11
|
+
/** Key without scope — used for grouping duplicates */
|
|
12
|
+
function baseKey(s) {
|
|
13
|
+
return `${s.file}\0${s.code}\0${s.hash}`;
|
|
14
|
+
}
|
|
15
|
+
/** Key with scope — used for matching duplicates */
|
|
16
|
+
function fullKey(s) {
|
|
17
|
+
return `${s.file}\0${s.code}\0${s.hash}\0${s.scope}`;
|
|
18
|
+
}
|
|
19
|
+
/** Count occurrences of each base key in a list */
|
|
20
|
+
function countByBaseKey(list) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
for (const s of list) {
|
|
23
|
+
const key = baseKey(s);
|
|
24
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
return counts;
|
|
27
|
+
}
|
|
28
|
+
/** Read suppressions from .ts-suppressions.json in the given directory */
|
|
29
|
+
export async function readSuppressions(projectRoot) {
|
|
30
|
+
const filePath = resolve(projectRoot, SUPPRESSIONS_FILENAME);
|
|
31
|
+
try {
|
|
32
|
+
await access(filePath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const raw = await readFile(filePath, "utf-8");
|
|
38
|
+
const data = JSON.parse(raw);
|
|
39
|
+
return data.suppressions;
|
|
40
|
+
}
|
|
41
|
+
/** Write suppressions to .ts-suppressions.json, sorted deterministically */
|
|
42
|
+
export async function writeSuppressions(projectRoot, suppressions) {
|
|
43
|
+
const filePath = resolve(projectRoot, SUPPRESSIONS_FILENAME);
|
|
44
|
+
const sorted = [...suppressions].sort(compareSuppression);
|
|
45
|
+
const data = { suppressions: sorted };
|
|
46
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Diff existing suppressions against current diagnostics.
|
|
50
|
+
*
|
|
51
|
+
* Matching strategy:
|
|
52
|
+
* - For unique { file, code, hash } tuples: match by base key only (scope is informational)
|
|
53
|
+
* - For duplicate { file, code, hash } tuples: match by full key including scope
|
|
54
|
+
*
|
|
55
|
+
* "Became duplicate" edge case: if existing has 1 entry for a base key but current has 2+,
|
|
56
|
+
* the existing suppression covers one occurrence; extras are reported as unsuppressed.
|
|
57
|
+
*/
|
|
58
|
+
export function diffSuppressions(existing, current) {
|
|
59
|
+
const existingCounts = countByBaseKey(existing);
|
|
60
|
+
const currentCounts = countByBaseKey(current);
|
|
61
|
+
// A base key is "duplicate" if EITHER list has more than one entry for it
|
|
62
|
+
const isDuplicate = (key) => (existingCounts.get(key) ?? 0) > 1 || (currentCounts.get(key) ?? 0) > 1;
|
|
63
|
+
// Build a pool of existing match counts keyed appropriately
|
|
64
|
+
const existingKeys = new Map();
|
|
65
|
+
for (const s of existing) {
|
|
66
|
+
const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
|
|
67
|
+
existingKeys.set(key, (existingKeys.get(key) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
// Match current diagnostics against existing suppressions
|
|
70
|
+
const unsuppressed = [];
|
|
71
|
+
const matchedKeys = new Map();
|
|
72
|
+
for (const s of current) {
|
|
73
|
+
const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
|
|
74
|
+
const remaining = (existingKeys.get(key) ?? 0) - (matchedKeys.get(key) ?? 0);
|
|
75
|
+
if (remaining > 0) {
|
|
76
|
+
matchedKeys.set(key, (matchedKeys.get(key) ?? 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
unsuppressed.push(s);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Find stale: existing entries not consumed by any current diagnostic
|
|
83
|
+
const currentKeySet = new Map();
|
|
84
|
+
for (const s of current) {
|
|
85
|
+
const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
|
|
86
|
+
currentKeySet.set(key, (currentKeySet.get(key) ?? 0) + 1);
|
|
87
|
+
}
|
|
88
|
+
const staleConsumed = new Map();
|
|
89
|
+
const stale = [];
|
|
90
|
+
for (const s of existing) {
|
|
91
|
+
const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
|
|
92
|
+
const available = (currentKeySet.get(key) ?? 0) - (staleConsumed.get(key) ?? 0);
|
|
93
|
+
if (available > 0) {
|
|
94
|
+
staleConsumed.set(key, (staleConsumed.get(key) ?? 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
stale.push(s);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { unsuppressed, stale };
|
|
101
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-suppress",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Incremental TypeScript strictness adoption via bulk error suppression",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"migration",
|
|
7
|
+
"strict",
|
|
8
|
+
"suppression",
|
|
9
|
+
"tanstack-intent",
|
|
10
|
+
"ts-ignore",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/br0p0p/ts-suppress#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/br0p0p/ts-suppress/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "br0p0p",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/br0p0p/ts-suppress.git"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"ts-suppress": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"package.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"skills",
|
|
31
|
+
"!skills/_artifacts"
|
|
32
|
+
],
|
|
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
|
+
"dependencies": {
|
|
47
|
+
"@bomb.sh/args": "^0.3.1",
|
|
48
|
+
"ts-morph": "^27.0.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"@types/node": "^24",
|
|
53
|
+
"husky": "^9.1.7",
|
|
54
|
+
"knip": "^5.87.0",
|
|
55
|
+
"lint-staged": "^16.4.0",
|
|
56
|
+
"oxfmt": "^0.41.0",
|
|
57
|
+
"oxlint": "^1.56.0",
|
|
58
|
+
"oxlint-tsgolint": "^0.17.0",
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"lint-staged": {
|
|
65
|
+
"*.{js,jsx,ts,tsx,mjs,cjs}": "bun run lint",
|
|
66
|
+
"*": "oxfmt --no-error-on-unmatched-pattern"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ts-suppress
|
|
3
|
+
description: Use when working with TypeScript error suppressions, enabling stricter tsconfig options incrementally, or managing .ts-suppressions.json files
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ts-suppress
|
|
7
|
+
|
|
8
|
+
Incremental TypeScript strictness adoption via bulk error suppression. Instead of scattering `@ts-ignore` everywhere, ts-suppress captures all errors into a single `.ts-suppressions.json` file so you can enable strict options immediately and fix errors at your own pace.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
| Command | Description |
|
|
13
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
14
|
+
| `ts-suppress init` | Create an empty `.ts-suppressions.json` file |
|
|
15
|
+
| `ts-suppress suppress` | Snapshot all current TypeScript errors into `.ts-suppressions.json` |
|
|
16
|
+
| `ts-suppress check` | Verify all errors are suppressed and none are stale (use in CI) |
|
|
17
|
+
| `ts-suppress update` | Add new suppressions and remove stale ones (alias: `fix`) |
|
|
18
|
+
|
|
19
|
+
**Flags:** `--help` / `-h`, `--version` / `-v`
|
|
20
|
+
|
|
21
|
+
## Typical Workflow
|
|
22
|
+
|
|
23
|
+
1. Enable stricter TypeScript settings in `tsconfig.json` (e.g. `"strict": true`)
|
|
24
|
+
2. Run `ts-suppress suppress` to capture all resulting errors as a baseline
|
|
25
|
+
3. Commit `.ts-suppressions.json` to version control
|
|
26
|
+
4. Add `ts-suppress check` to CI — fails on new unsuppressed errors or stale suppressions
|
|
27
|
+
5. Fix errors incrementally; run `ts-suppress update` to sync the suppression file
|
|
28
|
+
|
|
29
|
+
## Suppression File Format
|
|
30
|
+
|
|
31
|
+
`.ts-suppressions.json` contains fingerprinted error entries:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"suppressions": [
|
|
36
|
+
{
|
|
37
|
+
"file": "src/utils.ts",
|
|
38
|
+
"code": 2322,
|
|
39
|
+
"hash": "a1b2c3...",
|
|
40
|
+
"scope": "MyClass.myMethod"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **file** — relative path to source file
|
|
47
|
+
- **code** — TypeScript error code
|
|
48
|
+
- **hash** — SHA256 of the diagnostic message text
|
|
49
|
+
- **scope** — dot-separated scope chain (e.g. `MyClass.myMethod`, empty for module-level)
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- TypeScript >= 5.9.3
|
|
54
|
+
- A `tsconfig.json` in the project (ts-suppress walks up directories to find it)
|