Last active
February 28, 2026 06:04
-
-
Save NanamiNakano/dc6035e22291df0ac058f71e5f50c5c7 to your computer and use it in GitHub Desktop.
temporarily suppress typescript error
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| import { spawnSync } from "node:child_process"; | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import process from "node:process"; | |
| import ts from "typescript"; | |
| const args = new Set(process.argv.slice(2)); | |
| const dryRun = args.has("--dry-run"); | |
| const cwd = process.cwd(); | |
| const tscCommand = process.env.TSC_COMMAND || | |
| "pnpm -s tsc -b --noEmit --pretty false"; | |
| const tscResult = spawnSync(tscCommand, { | |
| cwd, | |
| encoding: "utf8", | |
| shell: true, | |
| }); | |
| const output = `${tscResult.stdout || ""}${tscResult.stderr || ""}`.trim(); | |
| if (!output) { | |
| console.log("No TypeScript diagnostics found."); | |
| process.exit(0); | |
| } | |
| const diagnostics = parseDiagnostics(output, cwd); | |
| if (diagnostics.length === 0) { | |
| console.log("No parseable TypeScript diagnostics found in tsc output."); | |
| process.exit(0); | |
| } | |
| const groupedByFile = new Map(); | |
| for (const diagnostic of diagnostics) { | |
| const key = diagnostic.filePath; | |
| if (!groupedByFile.has(key)) { | |
| groupedByFile.set(key, []); | |
| } | |
| groupedByFile.get(key).push(diagnostic); | |
| } | |
| let filesChanged = 0; | |
| let commentsInserted = 0; | |
| for (const [filePath, fileDiagnostics] of groupedByFile) { | |
| if (!fs.existsSync(filePath)) { | |
| continue; | |
| } | |
| const originalText = fs.readFileSync(filePath, "utf8"); | |
| const newline = detectNewline(originalText); | |
| const lines = originalText.split(/\r?\n/); | |
| const scriptKind = getScriptKind(filePath); | |
| const sourceFile = ts.createSourceFile( | |
| filePath, | |
| originalText, | |
| ts.ScriptTarget.Latest, | |
| true, | |
| scriptKind, | |
| ); | |
| const insertionsByLine = new Map(); | |
| for (const diagnostic of fileDiagnostics) { | |
| const zeroBasedLine = Math.max(0, diagnostic.line - 1); | |
| const zeroBasedColumn = Math.max(0, diagnostic.column - 1); | |
| const maxLine = Math.max(sourceFile.getLineStarts().length - 1, 0); | |
| const safeLine = Math.min(zeroBasedLine, maxLine); | |
| const lineStarts = sourceFile.getLineStarts(); | |
| const lineStart = lineStarts[safeLine] ?? 0; | |
| const lineEnd = safeLine + 1 < lineStarts.length | |
| ? lineStarts[safeLine + 1] | |
| : sourceFile.getEnd(); | |
| const safePos = Math.min( | |
| Math.max(lineStart + zeroBasedColumn, lineStart), | |
| lineEnd, | |
| ); | |
| const lineText = lines[safeLine] || ""; | |
| const style = getCommentStyle( | |
| sourceFile, | |
| safePos, | |
| diagnostic.line, | |
| lineText, | |
| ); | |
| if (!insertionsByLine.has(diagnostic.line)) { | |
| insertionsByLine.set(diagnostic.line, { | |
| line: diagnostic.line, | |
| style, | |
| codes: new Set(), | |
| }); | |
| } | |
| const existing = insertionsByLine.get(diagnostic.line); | |
| existing.codes.add(diagnostic.code); | |
| if (existing.style !== "jsx" && style === "jsx") { | |
| existing.style = "jsx"; | |
| } | |
| } | |
| const plannedInsertions = [...insertionsByLine.values()].sort((a, b) => | |
| b.line - a.line | |
| ); | |
| let fileChanged = false; | |
| for (const insertion of plannedInsertions) { | |
| const lineIndex = Math.max(0, insertion.line - 1); | |
| if (hasExistingTsExpectError(lines, lineIndex)) { | |
| continue; | |
| } | |
| const targetLine = lines[lineIndex] || ""; | |
| const indent = getIndent(targetLine); | |
| const sortedCodes = [...insertion.codes].sort((a, b) => a - b); | |
| const codeText = sortedCodes.map((code) => `TS${code}`).join(", "); | |
| const fixmeText = `FIXME: Error code ${codeText}`; | |
| const comment = insertion.style === "jsx" | |
| ? `${indent}{/* @ts-expect-error ${fixmeText} */}` | |
| : `${indent}// @ts-expect-error ${fixmeText}`; | |
| lines.splice(lineIndex, 0, comment); | |
| fileChanged = true; | |
| commentsInserted += 1; | |
| } | |
| if (fileChanged) { | |
| filesChanged += 1; | |
| if (!dryRun) { | |
| const nextText = lines.join(newline); | |
| fs.writeFileSync(filePath, nextText, "utf8"); | |
| } | |
| } | |
| } | |
| if (dryRun) { | |
| console.log( | |
| `Dry run complete. ${commentsInserted} comment(s) would be inserted in ${filesChanged} file(s).`, | |
| ); | |
| } else { | |
| console.log( | |
| `Done. Inserted ${commentsInserted} comment(s) in ${filesChanged} file(s).`, | |
| ); | |
| } | |
| function parseDiagnostics(tscOutput, baseDir) { | |
| const parsed = []; | |
| const lines = tscOutput.split(/\r?\n/); | |
| for (const line of lines) { | |
| const match = line.match(/^(.+)\((\d+),(\d+)\): error TS(\d+):/); | |
| if (!match) { | |
| continue; | |
| } | |
| const [, filePathRaw, lineRaw, columnRaw, codeRaw] = match; | |
| const filePath = path.resolve(baseDir, filePathRaw); | |
| parsed.push({ | |
| filePath, | |
| line: Number(lineRaw), | |
| column: Number(columnRaw), | |
| code: Number(codeRaw), | |
| }); | |
| } | |
| return parsed; | |
| } | |
| function getScriptKind(filePath) { | |
| if (filePath.endsWith(".tsx")) { | |
| return ts.ScriptKind.TSX; | |
| } | |
| if (filePath.endsWith(".ts")) { | |
| return ts.ScriptKind.TS; | |
| } | |
| if (filePath.endsWith(".jsx")) { | |
| return ts.ScriptKind.JSX; | |
| } | |
| if (filePath.endsWith(".js")) { | |
| return ts.ScriptKind.JS; | |
| } | |
| return ts.ScriptKind.Unknown; | |
| } | |
| function getCommentStyle(sourceFile, position, insertionLine, lineText) { | |
| const node = findInnermostNode(sourceFile, position); | |
| const openingLike = findAncestor( | |
| node, | |
| (value) => | |
| ts.isJsxOpeningElement(value) || ts.isJsxSelfClosingElement(value), | |
| ); | |
| if (!openingLike) { | |
| return "line"; | |
| } | |
| const openingLine = getLineFromPosition( | |
| sourceFile, | |
| openingLike.getStart(sourceFile), | |
| ); | |
| const trimmedLine = lineText.trimStart(); | |
| if (insertionLine !== openingLine || !trimmedLine.startsWith("<")) { | |
| return "line"; | |
| } | |
| return shouldUseJsxComment(openingLike) ? "jsx" : "line"; | |
| } | |
| function shouldUseJsxComment(openingLike) { | |
| const jsxNode = ts.isJsxOpeningElement(openingLike) | |
| ? openingLike.parent | |
| : openingLike; | |
| const parent = jsxNode.parent; | |
| if (!parent) { | |
| return false; | |
| } | |
| if (!ts.isJsxElement(parent) && !ts.isJsxFragment(parent)) { | |
| return false; | |
| } | |
| return parent.children.includes(jsxNode); | |
| } | |
| function findInnermostNode(sourceFile, position) { | |
| let best = sourceFile; | |
| const visit = (node) => { | |
| if (position < node.getFullStart() || position >= node.getEnd()) { | |
| return; | |
| } | |
| best = node; | |
| ts.forEachChild(node, visit); | |
| }; | |
| visit(sourceFile); | |
| return best; | |
| } | |
| function findAncestor(node, predicate) { | |
| let current = node; | |
| while (current) { | |
| if (predicate(current)) { | |
| return current; | |
| } | |
| current = current.parent; | |
| } | |
| return undefined; | |
| } | |
| function getLineFromPosition(sourceFile, position) { | |
| return ts.getLineAndCharacterOfPosition(sourceFile, position).line + 1; | |
| } | |
| function hasExistingTsExpectError(lines, lineIndex) { | |
| const current = lines[lineIndex] || ""; | |
| if (/@ts-expect-error/.test(current)) { | |
| return true; | |
| } | |
| let checked = 0; | |
| for (let index = lineIndex - 1; index >= 0 && checked < 3; index -= 1) { | |
| const line = lines[index].trim(); | |
| if (!line) { | |
| continue; | |
| } | |
| checked += 1; | |
| if (/@ts-expect-error/.test(line)) { | |
| return true; | |
| } | |
| // If we already hit a code line, further lines are not attached to this statement. | |
| if ( | |
| !line.startsWith("//") && !line.startsWith("/*") && | |
| !line.startsWith("*") && !line.startsWith("{/*") | |
| ) { | |
| break; | |
| } | |
| } | |
| return false; | |
| } | |
| function getIndent(line) { | |
| const match = line.match(/^\s*/); | |
| return match ? match[0] : ""; | |
| } | |
| function detectNewline(content) { | |
| return content.includes("\r\n") ? "\r\n" : "\n"; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment