add-ts-expect-error 1.0.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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024 Steve Rodriguez
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # add-ts-expect-error
2
+
3
+ Adds a `@ts-expect-error` comment above every type error in a TypeScript project,
4
+ with the error message inline.
5
+
6
+ Useful when you turn on `strict` (or bump TypeScript) and get a few hundred errors
7
+ at once: this gets the project compiling again and leaves each error documented in
8
+ place, so you can work through them one file at a time.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm i -D add-ts-expect-error
14
+ ```
15
+
16
+ Or run it once without installing:
17
+
18
+ ```sh
19
+ npx add-ts-expect-error
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Run it from the directory holding your `tsconfig.json`:
25
+
26
+ ```sh
27
+ npx add-ts-expect-error
28
+ ```
29
+
30
+ It reads that `tsconfig.json`, collects the diagnostics for every source file in the
31
+ project, and rewrites the files in place. There are no flags.
32
+
33
+ **It edits your files.** Commit or stash first.
34
+
35
+ ## What it does to your code
36
+
37
+ Before:
38
+
39
+ ```ts
40
+ const total: number = getTotal()
41
+ ```
42
+
43
+ After:
44
+
45
+ ```ts
46
+ // @ts-expect-error: FIX: Type 'string' is not assignable to type 'number'.
47
+ const total: number = getTotal()
48
+ ```
49
+
50
+ In JSX it uses the brace form, so the comment does not render as text:
51
+
52
+ ```tsx
53
+ <div>
54
+ {/* @ts-expect-error: FIX: Type 'string' is not assignable to type 'number'. */}
55
+ <Widget count="not a number" />
56
+ </div>
57
+ ```
58
+
59
+ Details:
60
+
61
+ - Comments are indented to match the line they annotate.
62
+ - Only errors are commented. Warnings and suggestions are left alone.
63
+ - When a line has more than one error, the comment reads
64
+ `Multiple errors, uncomment to see.` Remove the comment to see them all.
65
+ - Lines that already carry the comment are skipped, so a second run does not stack
66
+ them up.
67
+
68
+ ## What it does not do
69
+
70
+ - No flags, no config, no file or directory filtering. It processes the whole project.
71
+ - It does not fix anything. Every comment it writes is a `FIX:` you still owe.
72
+ - It does not reformat. Run your formatter afterward if the inserted lines bother it.
73
+
74
+ ## Why this exists
75
+
76
+ Airbnb's [ts-migrate](https://github.com/airbnb/ts-migrate) has a `reignore` command
77
+ that covers similar ground, as part of a larger JavaScript to TypeScript migration
78
+ framework. This is the narrow version: one command, one job, no migration pipeline
79
+ around it.
80
+
81
+ ## Development
82
+
83
+ ```sh
84
+ npm install
85
+ npm test
86
+ npm run build
87
+ ```
88
+
89
+ Tests are Vitest, including end to end runs against a temporary project on disk.
90
+
91
+ ## License
92
+
93
+ ISC, Steve Rodriguez.
@@ -0,0 +1,23 @@
1
+ import { Diagnostic, SourceFile } from "ts-morph";
2
+ interface ConstructorParams {
3
+ lineNum: number;
4
+ lineDiagnostics: Diagnostic[];
5
+ sourceFile: SourceFile;
6
+ }
7
+ export declare class Comment {
8
+ private sourceFile;
9
+ private text?;
10
+ private lineNum;
11
+ constructor({ lineNum, lineDiagnostics, sourceFile }: ConstructorParams);
12
+ private generateTextFrom;
13
+ private getErrorDiagnostics;
14
+ private getDiagnosticMessagesFrom;
15
+ private getErrorMessage;
16
+ private checkIfInJSX;
17
+ private getNodeInfo;
18
+ private getLineNumFor;
19
+ hasTextAndAlreadyExists(lines: string[]): boolean;
20
+ getLineNum(): number;
21
+ getText(): string;
22
+ }
23
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Comment = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ const utils_1 = require("./utils");
6
+ class Comment {
7
+ constructor({ lineNum, lineDiagnostics, sourceFile }) {
8
+ this.sourceFile = sourceFile;
9
+ this.lineNum = lineNum;
10
+ const errorDiagnostics = this.getErrorDiagnostics(lineDiagnostics);
11
+ this.text = this.generateTextFrom(errorDiagnostics);
12
+ }
13
+ generateTextFrom(lineDiagnostics) {
14
+ if (lineDiagnostics.length === 0)
15
+ return;
16
+ const errorMessage = this.getErrorMessage(lineDiagnostics);
17
+ const startPosition = lineDiagnostics[0].getStart();
18
+ if (!startPosition)
19
+ return;
20
+ const inJSX = this.checkIfInJSX(startPosition);
21
+ return inJSX
22
+ ? `{/* @ts-expect-error: FIX: ${errorMessage} */}`
23
+ : `// @ts-expect-error: FIX: ${errorMessage}`;
24
+ }
25
+ getErrorDiagnostics(lineDiagnostics) {
26
+ return lineDiagnostics.filter((diagnostic) => diagnostic?.getCategory() === ts_morph_1.DiagnosticCategory.Error);
27
+ }
28
+ getDiagnosticMessagesFrom(lineDiagnostics) {
29
+ return lineDiagnostics.map((diagnostic) => {
30
+ const messageText = diagnostic.getMessageText();
31
+ return (0, utils_1.stringifyDiagnosticMessage)(messageText);
32
+ });
33
+ }
34
+ getErrorMessage(lineDiagnostics) {
35
+ const diagnosticMessages = this.getDiagnosticMessagesFrom(lineDiagnostics);
36
+ return diagnosticMessages.length > 1
37
+ ? "Multiple errors, uncomment to see."
38
+ : diagnosticMessages[0];
39
+ }
40
+ checkIfInJSX(startPosition) {
41
+ const startLineNum = this.getLineNumFor(startPosition);
42
+ let currentNode = this.sourceFile.getDescendantAtPos(startPosition);
43
+ let inAttribute = false;
44
+ while (currentNode) {
45
+ const { kind, currentLineNum } = this.getNodeInfo(currentNode);
46
+ if (currentLineNum === startLineNum) {
47
+ inAttribute || (inAttribute = (0, utils_1.isJsxAttributeOrSpread)(kind));
48
+ }
49
+ if (currentLineNum !== startLineNum) {
50
+ if (kind === ts_morph_1.SyntaxKind.JsxExpression)
51
+ return false;
52
+ if (inAttribute && (0, utils_1.isJsxOpeningOrSelfClosing)(kind))
53
+ return false;
54
+ if ((0, utils_1.isJsxElementOrFragment)(kind))
55
+ return true;
56
+ }
57
+ currentNode = currentNode.getParent();
58
+ }
59
+ return false;
60
+ }
61
+ getNodeInfo(node) {
62
+ const kind = node.getKind();
63
+ const currentPos = node.getStart();
64
+ const currentLineNum = this.getLineNumFor(currentPos);
65
+ return { kind, currentLineNum };
66
+ }
67
+ getLineNumFor(position) {
68
+ return this.sourceFile.getLineAndColumnAtPos(position).line;
69
+ }
70
+ hasTextAndAlreadyExists(lines) {
71
+ if (!this.text)
72
+ return false;
73
+ const line = lines[this.lineNum];
74
+ const beginningOfText = this.text.trim().slice(0, 3);
75
+ return line?.trim().startsWith(beginningOfText) || false;
76
+ }
77
+ getLineNum() {
78
+ return this.lineNum;
79
+ }
80
+ getText() {
81
+ if (!this.text)
82
+ throw new Error("Comment text not found.");
83
+ return this.text;
84
+ }
85
+ }
86
+ exports.Comment = Comment;
@@ -0,0 +1,16 @@
1
+ import { SourceFile } from "ts-morph";
2
+ export declare class FileContent {
3
+ private readonly sourceFile;
4
+ private readonly diagnosticsByLine;
5
+ private lines;
6
+ private newLineAddedOffset;
7
+ constructor(sourceFile: SourceFile);
8
+ private groupDiagnosticsByLine;
9
+ private getLines;
10
+ private getSortedDiagLineNums;
11
+ /** The leading whitespace of a line, so an inserted comment lines up with the code it annotates. */
12
+ private indentOf;
13
+ private insertComment;
14
+ private createComment;
15
+ generate(): string | undefined;
16
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileContent = void 0;
4
+ const Comment_1 = require("./Comment");
5
+ const utils_1 = require("./utils");
6
+ class FileContent {
7
+ constructor(sourceFile) {
8
+ this.sourceFile = sourceFile;
9
+ this.diagnosticsByLine = this.groupDiagnosticsByLine();
10
+ this.lines = this.getLines();
11
+ this.newLineAddedOffset = 0;
12
+ }
13
+ groupDiagnosticsByLine() {
14
+ const diagnostics = this.sourceFile.getPreEmitDiagnostics();
15
+ return diagnostics.reduce((map, diagnostic) => {
16
+ const pos = diagnostic.getStart();
17
+ if (!pos)
18
+ return map;
19
+ const { line } = this.sourceFile.getLineAndColumnAtPos(pos);
20
+ if (!map.has(line))
21
+ map.set(line, []);
22
+ map.get(line).push(diagnostic);
23
+ return map;
24
+ }, new Map());
25
+ }
26
+ getLines() {
27
+ const path = this.sourceFile.getFilePath();
28
+ const content = (0, utils_1.readFile)(path);
29
+ return content.split("\n");
30
+ }
31
+ getSortedDiagLineNums() {
32
+ return Array.from(this.diagnosticsByLine.keys()).sort((a, b) => a - b);
33
+ }
34
+ /** The leading whitespace of a line, so an inserted comment lines up with the code it annotates. */
35
+ indentOf(lineNum) {
36
+ return this.lines[lineNum]?.match(/^[ \t]*/)?.[0] ?? "";
37
+ }
38
+ insertComment(comment) {
39
+ const lineNum = comment.getLineNum();
40
+ const text = this.indentOf(lineNum) + comment.getText();
41
+ this.lines.splice(lineNum, 0, text);
42
+ this.newLineAddedOffset += 1;
43
+ }
44
+ createComment(diagnosticLineNum) {
45
+ const lineNum = diagnosticLineNum + this.newLineAddedOffset - 1;
46
+ const lineDiagnostics = this.diagnosticsByLine.get(diagnosticLineNum);
47
+ const sourceFile = this.sourceFile;
48
+ return new Comment_1.Comment({ lineNum, lineDiagnostics, sourceFile });
49
+ }
50
+ generate() {
51
+ const sortedDiagnosticLineNums = this.getSortedDiagLineNums();
52
+ for (const diagnosticLineNum of sortedDiagnosticLineNums) {
53
+ const comment = this.createComment(diagnosticLineNum);
54
+ if (comment.hasTextAndAlreadyExists(this.lines))
55
+ continue;
56
+ if (!comment.getText())
57
+ continue;
58
+ this.insertComment(comment);
59
+ }
60
+ return this.lines.join("\n");
61
+ }
62
+ }
63
+ exports.FileContent = FileContent;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const main_1 = require("./main");
5
+ (0, main_1.main)();
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(): void;
package/dist/main.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.main = main;
7
+ const path_1 = __importDefault(require("path"));
8
+ const ts_morph_1 = require("ts-morph");
9
+ const utils_1 = require("./utils");
10
+ function main() {
11
+ const currentWorkingDir = process.cwd();
12
+ const tsConfigFilePath = path_1.default.resolve(currentWorkingDir, "tsconfig.json");
13
+ const project = new ts_morph_1.Project({ tsConfigFilePath });
14
+ const sourceFiles = project.getSourceFiles();
15
+ sourceFiles.forEach(utils_1.processSourceFile);
16
+ console.log("Added @ts-expect-error comments to all TypeScript errors.");
17
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readFile(filePath: string): string;
2
+ export declare function writeFile(filePath: string, content: string): void;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readFile = readFile;
7
+ exports.writeFile = writeFile;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ function readFile(filePath) {
10
+ try {
11
+ return fs_1.default.readFileSync(filePath, "utf-8");
12
+ }
13
+ catch (error) {
14
+ console.error(`Error reading file ${filePath}:`, error);
15
+ throw error;
16
+ }
17
+ }
18
+ function writeFile(filePath, content) {
19
+ try {
20
+ fs_1.default.writeFileSync(filePath, content, "utf-8");
21
+ }
22
+ catch (error) {
23
+ console.error(`Error writing file ${filePath}:`, error);
24
+ throw error;
25
+ }
26
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./jsx-util";
2
+ export * from "./file-util";
3
+ export * from "./stringifyDiagnosticMessage";
4
+ export * from "./processSourceFile";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./jsx-util"), exports);
18
+ __exportStar(require("./file-util"), exports);
19
+ __exportStar(require("./stringifyDiagnosticMessage"), exports);
20
+ __exportStar(require("./processSourceFile"), exports);
@@ -0,0 +1,4 @@
1
+ import { SyntaxKind } from "ts-morph";
2
+ export declare function isJsxElementOrFragment(kind: SyntaxKind): boolean;
3
+ export declare function isJsxAttributeOrSpread(kind: SyntaxKind): boolean;
4
+ export declare function isJsxOpeningOrSelfClosing(kind: SyntaxKind): boolean;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isJsxElementOrFragment = isJsxElementOrFragment;
4
+ exports.isJsxAttributeOrSpread = isJsxAttributeOrSpread;
5
+ exports.isJsxOpeningOrSelfClosing = isJsxOpeningOrSelfClosing;
6
+ const ts_morph_1 = require("ts-morph");
7
+ function isJsxElementOrFragment(kind) {
8
+ return (kind === ts_morph_1.SyntaxKind.JsxElement ||
9
+ kind === ts_morph_1.SyntaxKind.JsxSelfClosingElement ||
10
+ kind === ts_morph_1.SyntaxKind.JsxFragment ||
11
+ kind === ts_morph_1.SyntaxKind.JsxOpeningElement ||
12
+ kind === ts_morph_1.SyntaxKind.JsxClosingElement ||
13
+ kind === ts_morph_1.SyntaxKind.JsxOpeningFragment ||
14
+ kind === ts_morph_1.SyntaxKind.JsxClosingFragment);
15
+ }
16
+ function isJsxAttributeOrSpread(kind) {
17
+ return (kind === ts_morph_1.SyntaxKind.JsxAttribute || kind === ts_morph_1.SyntaxKind.JsxSpreadAttribute);
18
+ }
19
+ function isJsxOpeningOrSelfClosing(kind) {
20
+ return (kind === ts_morph_1.SyntaxKind.JsxOpeningElement ||
21
+ kind === ts_morph_1.SyntaxKind.JsxSelfClosingElement);
22
+ }
@@ -0,0 +1,2 @@
1
+ import { SourceFile } from "ts-morph";
2
+ export declare function processSourceFile(sourceFile: SourceFile): void;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processSourceFile = processSourceFile;
4
+ const FileContent_1 = require("../FileContent");
5
+ const file_util_1 = require("./file-util");
6
+ function processSourceFile(sourceFile) {
7
+ const filePath = sourceFile.getFilePath();
8
+ const newContent = new FileContent_1.FileContent(sourceFile).generate();
9
+ if (!newContent)
10
+ return;
11
+ (0, file_util_1.writeFile)(filePath, newContent);
12
+ }
@@ -0,0 +1,2 @@
1
+ import { DiagnosticMessageChain } from "ts-morph";
2
+ export declare function stringifyDiagnosticMessage(diagnosticMessage: string | DiagnosticMessageChain): string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringifyDiagnosticMessage = stringifyDiagnosticMessage;
4
+ function stringifyDiagnosticMessage(diagnosticMessage) {
5
+ if (isDiagnosticMessageChain(diagnosticMessage)) {
6
+ return stringifyDiagnosticMessageChain(diagnosticMessage);
7
+ }
8
+ return diagnosticMessage.toString();
9
+ }
10
+ function isDiagnosticMessageChain(message) {
11
+ return typeof message === "object" && "getMessageText" in message;
12
+ }
13
+ function stringifyDiagnosticMessageChain(diagnosticMessage) {
14
+ let messages = [extractMessageText(diagnosticMessage)];
15
+ let next = diagnosticMessage.getNext();
16
+ while (next && next.length > 0) {
17
+ messages.push(extractMessageText(next[0]));
18
+ next = next[0].getNext();
19
+ }
20
+ return messages.join(" ");
21
+ }
22
+ function extractMessageText(diagnosticMessage) {
23
+ return diagnosticMessage.getMessageText().toString();
24
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "add-ts-expect-error",
3
+ "version": "1.0.0",
4
+ "description": "Adds a @ts-expect-error comment above every type error in a TypeScript project, with the error message inline. JSX aware.",
5
+ "license": "ISC",
6
+ "author": "Steve Rodriguez",
7
+ "homepage": "https://github.com/steve-rodri/add-ts-expect-error#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/steve-rodri/add-ts-expect-error.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/steve-rodri/add-ts-expect-error/issues"
14
+ },
15
+ "keywords": [
16
+ "typescript",
17
+ "ts-expect-error",
18
+ "ts-ignore",
19
+ "codemod",
20
+ "cli",
21
+ "strict-mode",
22
+ "migration",
23
+ "jsx",
24
+ "tsx",
25
+ "ts-morph"
26
+ ],
27
+ "main": "dist/index.js",
28
+ "types": "dist/index.d.ts",
29
+ "bin": {
30
+ "add-ts-expect-error": "dist/index.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "clean": "rm -rf dist",
42
+ "prepublishOnly": "npm run clean && npm run build && npm test",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "test:ui": "vitest --ui",
46
+ "start": "tsx src/index.ts"
47
+ },
48
+ "dependencies": {
49
+ "ts-morph": "^23.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.2.0",
53
+ "@vitest/ui": "^2.0.5",
54
+ "tsx": "^4.19.2",
55
+ "typescript": "^5.5.4",
56
+ "vitest": "^2.0.5"
57
+ },
58
+ "prettier": {
59
+ "semi": false
60
+ }
61
+ }