TypeScript refactorings are supplied by the language server. VSCode uses the standalone tsserver binary, but you can also use the API directly.
import ts from 'typescript'
const REFACTOR_NAME = 'Convert import'
const ACTION_NAME = 'Convert namespace import to named imports'
const compilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020
// ...
}
const formatOptions: ts.FormatCodeSettings = {
insertSpaceAfterCommaDelimiter: true,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false
// ...
}
const preferences: ts.UserPreferences = {
// This is helpful to find out why the refactor isn't working
// provideRefactorNotApplicableReason: true
}
// An example with the 'filesystem' as an object
const files = {
'index.ts': `
// Both should be transformed
import * as a from './a'
import * as b from './b'
a.c()
a.d()
b.e()
b.f()
`,
'another.ts': `
// Should be transformed
import * as a from './a'
// Should NOT be transformed
import b from './b'
a.a
`,
'unaffected.ts': `
console.log(42)
`
}
// https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API#document-registry
// It was the only way I could find to get a SourceFile from the language
// service without having to parse the file again
const registry = ts.createDocumentRegistry()
// I think the getScriptVersion thing may be useful for incremental compilation,
// but I'm trying to keep this as simple as possible
const scriptVersion = '0'
const service = ts.createLanguageService(
{
getCurrentDirectory: () => '/',
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => Object.keys(files),
getScriptVersion: _file => scriptVersion,
// https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API#scriptsnapshot
getScriptSnapshot: file =>
file in files
? ts.ScriptSnapshot.fromString(files[file as keyof typeof files])
: undefined,
getDefaultLibFileName: ts.getDefaultLibFilePath
},
registry
)
const transformFile = (fileName: string, text: string): string => {
// Get the AST of the file
const sourceFile = registry.acquireDocument(
fileName,
compilerOptions,
ts.ScriptSnapshot.fromString(text),
scriptVersion
)
return (
sourceFile.statements
// Get the namespace import declarations
.filter(
node =>
ts.isImportDeclaration(node) &&
node.importClause?.namedBindings &&
ts.isNamespaceImport(node.importClause.namedBindings)
)
// Get the refactors
.flatMap(node => {
// The range of the import declaration
const range: ts.TextRange = {
pos: node.getStart(sourceFile),
end: node.getEnd()
}
// If preferences.provideRefactorNotApplicableReason is true,
// each refactor will have a notApplicableReason property if it
// isn't applicable (could be useful for debugging)
const refactors = service.getApplicableRefactors(
fileName,
range,
preferences
)
// Make sure the refactor is applicable (otherwise getEditsForRefactor
// will throw an error)
return refactors
.find(({name}) => name === REFACTOR_NAME)
?.actions.some(({name}) => name === ACTION_NAME) ?? false
? // The actual part where you get the edits for the refactor
service
.getEditsForRefactor(
fileName,
formatOptions,
range,
REFACTOR_NAME,
ACTION_NAME,
preferences
)
?.edits.flatMap(({textChanges}) => textChanges) ?? []
: []
})
.sort((a, b) => a.span.start - b.span.start)
// Apply the edits
.reduce<[text: string, offset: number]>(
([text, offset], {span: {start, length}, newText}) => {
// start: index (of original text) of text to replace
// length: length of text to replace
// newText: new text
// Because newText.length does not necessarily === length, the second
// element of the accumulator keeps track of the of offset
const newStart = start + offset
return [
text.slice(0, newStart) + newText + text.slice(newStart + length),
offset + newText.length - length
]
},
[text, 0]
)[0]
)
}
const newFiles = Object.fromEntries(
Object.entries(files).map(([fileName, text]) => [
fileName,
transformFile(fileName, text)
])
)
console.log(newFiles)
/*
{
'index.ts': '\n' +
' // Both should be transformed\n' +
" import {c, d} from './a'\n" +
" import {e, f} from './b'\n" +
'\n' +
' c()\n' +
' d()\n' +
' e()\n' +
' f()\n' +
' ',
'another.ts': '\n' +
' // Should be transformed\n' +
" import {a as a_1} from './a'\n" +
' // Should NOT be transformed\n' +
" import b from './b'\n" +
'\n' +
' a_1\n' +
' ',
'unaffected.ts': '\n console.log(42)\n '
}
*/
There isn't much documentation on the TypeScript compiler API, unfortunately. The repository wiki seems to be the only official resource.
In my experience the best way to figure out how to do something with the TS API is to just type ts.
and search for an appropriately named function in the autocomplete suggestions, or to look at the source code of TypeScript and/or VSCode.