Skip to content

Instantly share code, notes, and snippets.

@NanamiNakano
Last active February 28, 2026 06:04
Show Gist options
  • Select an option

  • Save NanamiNakano/dc6035e22291df0ac058f71e5f50c5c7 to your computer and use it in GitHub Desktop.

Select an option

Save NanamiNakano/dc6035e22291df0ac058f71e5f50c5c7 to your computer and use it in GitHub Desktop.
temporarily suppress typescript error
#!/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