以编程方式应用 Typescript 重构
Applying a Typescript refactoring programmatically
VS Code 进行了 'Convert namespace import to named imports'
重构。据我了解,重构是 defined in the Typescript codebase itself,因此它不特定于 VS Code。
我需要 运行 在 Jest 转换器中以编程方式对源文件进行重构。不幸的是,我一直无法找到任何有关 运行ning TypeScript 编程重构的文档。任何帮助表示赞赏。
TypeScript 重构由语言服务器提供。 VSCode 使用独立的 tsserver 二进制文件,但您也可以直接使用 API。
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 '
}
*/
遗憾的是,关于 TypeScript 编译器的文档不多 API。存储库 wiki 似乎是唯一的官方资源。
根据我的经验,弄清楚如何使用 TS API 做某事的最佳方法是键入 ts.
并在自动完成建议中搜索适当命名的函数,或者查看在 TypeScript 的源代码 and/or VSCode.
VS Code 进行了 'Convert namespace import to named imports'
重构。据我了解,重构是 defined in the Typescript codebase itself,因此它不特定于 VS Code。
我需要 运行 在 Jest 转换器中以编程方式对源文件进行重构。不幸的是,我一直无法找到任何有关 运行ning TypeScript 编程重构的文档。任何帮助表示赞赏。
TypeScript 重构由语言服务器提供。 VSCode 使用独立的 tsserver 二进制文件,但您也可以直接使用 API。
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 '
}
*/
遗憾的是,关于 TypeScript 编译器的文档不多 API。存储库 wiki 似乎是唯一的官方资源。
根据我的经验,弄清楚如何使用 TS API 做某事的最佳方法是键入 ts.
并在自动完成建议中搜索适当命名的函数,或者查看在 TypeScript 的源代码 and/or VSCode.