Created
April 9, 2026 10:50
-
-
Save AlexGeb/c1b0427b2f0b924f755071ae9385f8b8 to your computer and use it in GitHub Desktop.
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
| import path from "path"; | |
| import { Project, ts } from "ts-morph"; | |
| const packageName = "assistant"; | |
| const packageDir = `packages/${packageName}`; | |
| const project = new Project({ | |
| tsConfigFilePath: path.resolve(`${packageDir}/tsconfig.json`), | |
| }); | |
| const assistantFiles = new Set( | |
| project | |
| .getSourceFiles(`${packageDir}/src/**/*{.ts,.tsx}`) | |
| .map((f) => f.getFilePath()), | |
| ); | |
| console.log( | |
| `Found ${assistantFiles.size} source files in ${packageDir}/src...`, | |
| ); | |
| const MAX_ITERATIONS = 5; | |
| let iteration = 0; | |
| let totalFixed = 0; | |
| let totalUnfixed = 0; | |
| while (iteration < MAX_ITERATIONS) { | |
| iteration++; | |
| console.log(`\n--- Pass ${iteration} ---`); | |
| // Phase 1: Fix TS1484 errors (type-only imports missing `type` keyword) | |
| let fixedThisPass = 0; | |
| for (const sourceFile of project.getSourceFiles()) { | |
| if (!assistantFiles.has(sourceFile.getFilePath())) continue; | |
| const ts1484Diagnostics = sourceFile | |
| .getPreEmitDiagnostics() | |
| .filter((d) => d.getCode() === 1484); | |
| if (ts1484Diagnostics.length === 0) continue; | |
| const relPath = path.relative(process.cwd(), sourceFile.getFilePath()); | |
| for (const diagnostic of ts1484Diagnostics) { | |
| const start = diagnostic.getStart(); | |
| if (start === undefined) continue; | |
| const node = sourceFile.getDescendantAtPos(start); | |
| if (!node) continue; | |
| const importSpecifier = | |
| node.getFirstAncestorByKind(ts.SyntaxKind.ImportSpecifier) ?? | |
| (node.getKind() === ts.SyntaxKind.ImportSpecifier ? node : undefined); | |
| if (importSpecifier && "setIsTypeOnly" in importSpecifier) { | |
| (importSpecifier as any).setIsTypeOnly(true); | |
| fixedThisPass++; | |
| } | |
| } | |
| if (ts1484Diagnostics.length > 0) { | |
| sourceFile.saveSync(); | |
| console.log( | |
| ` [TS1484] Fixed ${ts1484Diagnostics.length} import(s) in ${relPath}`, | |
| ); | |
| } | |
| } | |
| totalFixed += fixedThisPass; | |
| if (fixedThisPass === 0 && iteration > 1) { | |
| console.log(" No more TS1484 errors to fix."); | |
| break; | |
| } | |
| // Phase 2: Undo TS1361 errors (imports wrongly marked as type-only that are used as values) | |
| let unfixedThisPass = 0; | |
| for (const sourceFile of project.getSourceFiles()) { | |
| if (!assistantFiles.has(sourceFile.getFilePath())) continue; | |
| const ts1361Diagnostics = sourceFile | |
| .getPreEmitDiagnostics() | |
| .filter((d) => d.getCode() === 1361); | |
| if (ts1361Diagnostics.length === 0) continue; | |
| const relPath = path.relative(process.cwd(), sourceFile.getFilePath()); | |
| const seen = new Set<number>(); | |
| for (const diagnostic of ts1361Diagnostics) { | |
| const messageText = diagnostic.getMessageText(); | |
| const message = | |
| typeof messageText === "string" | |
| ? messageText | |
| : messageText.getMessageText(); | |
| // Extract the identifier name from the error message | |
| // Pattern: "'Foo' cannot be used as a value because it was imported using 'import type'." | |
| const match = message.match(/^'(\w+)' cannot be used as a value/); | |
| if (!match) continue; | |
| const identifierName = match[1]!; | |
| // Find the import specifier for this identifier in the file's import declarations | |
| for (const importDecl of sourceFile.getImportDeclarations()) { | |
| for (const specifier of importDecl.getNamedImports()) { | |
| const name = | |
| specifier.getAliasNode()?.getText() ?? specifier.getName(); | |
| if (name === identifierName && specifier.isTypeOnly()) { | |
| const pos = specifier.getStart(); | |
| if (seen.has(pos)) continue; | |
| seen.add(pos); | |
| specifier.setIsTypeOnly(false); | |
| unfixedThisPass++; | |
| console.log( | |
| ` [TS1361] Reverted type-only on '${identifierName}' in ${relPath}`, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| if (unfixedThisPass > 0) { | |
| sourceFile.saveSync(); | |
| } | |
| } | |
| totalUnfixed += unfixedThisPass; | |
| if (fixedThisPass === 0 && unfixedThisPass === 0) { | |
| console.log(" No changes needed. Converged."); | |
| break; | |
| } | |
| } | |
| console.log(`\n=== Summary ===`); | |
| console.log(` Passes: ${iteration}`); | |
| console.log(` Imports marked as type-only: ${totalFixed}`); | |
| console.log(` Imports reverted (used as value): ${totalUnfixed}`); | |
| console.log(` Net fixes: ${totalFixed - totalUnfixed}`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment