kea-typegen 3.7.1 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +33 -0
- package/dist/package.json +3 -1
- package/dist/src/__tests__/inline.d.ts +1 -0
- package/dist/src/__tests__/inline.js +606 -0
- package/dist/src/__tests__/inline.js.map +1 -0
- package/dist/src/cache.d.ts +1 -0
- package/dist/src/cache.js +19 -0
- package/dist/src/cache.js.map +1 -1
- package/dist/src/cli/typegen.js +9 -0
- package/dist/src/cli/typegen.js.map +1 -1
- package/dist/src/print/print.d.ts +6 -0
- package/dist/src/print/print.js +61 -42
- package/dist/src/print/print.js.map +1 -1
- package/dist/src/types.d.ts +9 -0
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +82 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/visit/visit.js +7 -0
- package/dist/src/visit/visit.js.map +1 -1
- package/dist/src/visit/visitConnect.js +40 -0
- package/dist/src/visit/visitConnect.js.map +1 -1
- package/dist/src/visit/visitSelectors.js +14 -1
- package/dist/src/visit/visitSelectors.js.map +1 -1
- package/dist/src/write/writeInlineLogicTypes.d.ts +8 -0
- package/dist/src/write/writeInlineLogicTypes.js +259 -0
- package/dist/src/write/writeInlineLogicTypes.js.map +1 -0
- package/dist/src/write/writeTypeImports.d.ts +8 -0
- package/dist/src/write/writeTypeImports.js +3 -0
- package/dist/src/write/writeTypeImports.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/samples/manualTypedLogic.ts +20 -0
- package/samples-inline/counterLogic.ts +37 -0
- package/samples-inline/manualLogic.ts +20 -0
- package/samples-inline/profileLogic.ts +31 -0
- package/samples-inline/searchLogic.ts +32 -0
- package/samples-inline/tsconfig.json +17 -0
- package/samples-inline/types.ts +5 -0
- package/src/__tests__/inline.ts +747 -0
- package/src/cache.ts +19 -0
- package/src/cli/typegen.ts +10 -0
- package/src/print/print.ts +106 -59
- package/src/types.ts +15 -0
- package/src/utils.ts +124 -1
- package/src/visit/visit.ts +15 -0
- package/src/visit/visitConnect.ts +50 -0
- package/src/visit/visitSelectors.ts +26 -1
- package/src/write/writeInlineLogicTypes.ts +354 -0
- package/src/write/writeTypeImports.ts +4 -4
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as osPath from 'path'
|
|
4
|
+
import { factory, SyntaxKind } from 'typescript'
|
|
5
|
+
import { AppOptions, ParsedLogic } from '../types'
|
|
6
|
+
import { buildTypeImportEntries, nodeToString, runThroughPrettier } from '../print/print'
|
|
7
|
+
import { printValues } from '../print/printValues'
|
|
8
|
+
import { getInlineBlockStatements, isGeneratedInlineBlock } from '../utils'
|
|
9
|
+
import { applyTextEdits, getImportInsertPosition, getTypeArgumentInsertEnd, TextEdit } from './writeTypeImports'
|
|
10
|
+
|
|
11
|
+
export const INLINE_TYPE_MARKER = ' Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Print a block of interfaces (Values, Actions, and Props when it's a literal), plus an
|
|
15
|
+
* `export type logicType = MakeLogicType<logicValues, logicActions, logicProps>` alias tying them together.
|
|
16
|
+
*/
|
|
17
|
+
export function printInlineLogicType(parsedLogic: ParsedLogic): string {
|
|
18
|
+
const statements: ts.Statement[] = []
|
|
19
|
+
const typeArguments: ts.TypeNode[] = []
|
|
20
|
+
|
|
21
|
+
const createExportedInterface = (name: string, members: readonly ts.TypeElement[]) =>
|
|
22
|
+
factory.createInterfaceDeclaration(
|
|
23
|
+
[factory.createModifier(SyntaxKind.ExportKeyword)],
|
|
24
|
+
factory.createIdentifier(name),
|
|
25
|
+
undefined,
|
|
26
|
+
undefined,
|
|
27
|
+
members,
|
|
28
|
+
)
|
|
29
|
+
const addSection = (suffix: string, members: readonly ts.TypeElement[]) => {
|
|
30
|
+
if (members.length > 0) {
|
|
31
|
+
const name = `${parsedLogic.logicName}${suffix}`
|
|
32
|
+
const declaration = createExportedInterface(name, members)
|
|
33
|
+
ts.addSyntheticLeadingComment(declaration, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
|
|
34
|
+
statements.push(declaration)
|
|
35
|
+
typeArguments.push(factory.createTypeReferenceNode(factory.createIdentifier(name), undefined))
|
|
36
|
+
} else {
|
|
37
|
+
typeArguments.push(factory.createTypeLiteralNode([]))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// mark connected values/actions with the logic they come from, e.g. `user: UserType | null // userLogic`
|
|
42
|
+
const withSourceLogicComment = <T extends ts.TypeElement>(member: T, sourceLogic: string | undefined): T =>
|
|
43
|
+
sourceLogic ? ts.addSyntheticTrailingComment(member, SyntaxKind.SingleLineCommentTrivia, ` ${sourceLogic}`, false) : member
|
|
44
|
+
|
|
45
|
+
const valueSources = new Map<string, string | undefined>()
|
|
46
|
+
for (const value of [...parsedLogic.reducers, ...parsedLogic.selectors]) {
|
|
47
|
+
valueSources.set(value.name, value.sourceLogic)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addSection(
|
|
51
|
+
'Values',
|
|
52
|
+
printValues(parsedLogic).members.map((member) =>
|
|
53
|
+
withSourceLogicComment(
|
|
54
|
+
member,
|
|
55
|
+
member.name && ts.isIdentifier(member.name) ? valueSources.get(member.name.text) : undefined,
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
addSection(
|
|
60
|
+
'Actions',
|
|
61
|
+
parsedLogic.actions.map(({ name, parameters, returnTypeNode, sourceLogic }) =>
|
|
62
|
+
withSourceLogicComment(
|
|
63
|
+
factory.createPropertySignature(
|
|
64
|
+
undefined,
|
|
65
|
+
factory.createIdentifier(name),
|
|
66
|
+
undefined,
|
|
67
|
+
factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
|
|
68
|
+
),
|
|
69
|
+
sourceLogic,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
if (parsedLogic.propsType) {
|
|
74
|
+
if (ts.isTypeLiteralNode(parsedLogic.propsType)) {
|
|
75
|
+
addSection('Props', parsedLogic.propsType.members)
|
|
76
|
+
} else {
|
|
77
|
+
// props are already a named type (e.g. `props: {} as MyProps`) - reference it directly
|
|
78
|
+
typeArguments.push(parsedLogic.propsType)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const typeAlias = factory.createTypeAliasDeclaration(
|
|
83
|
+
[factory.createModifier(SyntaxKind.ExportKeyword)],
|
|
84
|
+
factory.createIdentifier(parsedLogic.logicTypeName),
|
|
85
|
+
undefined,
|
|
86
|
+
factory.createTypeReferenceNode(factory.createIdentifier('MakeLogicType'), typeArguments),
|
|
87
|
+
)
|
|
88
|
+
if (statements.length === 0) {
|
|
89
|
+
// no interfaces at all - the alias itself carries the marker
|
|
90
|
+
ts.addSyntheticLeadingComment(typeAlias, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
|
|
91
|
+
}
|
|
92
|
+
statements.push(typeAlias)
|
|
93
|
+
return statements.map(nodeToString).join('\n\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write/update generated MakeLogicType blocks above every logic in the file,
|
|
98
|
+
* instead of writing a separate logicType.ts file.
|
|
99
|
+
*/
|
|
100
|
+
export async function writeInlineLogicTypes(
|
|
101
|
+
program: ts.Program,
|
|
102
|
+
appOptions: AppOptions,
|
|
103
|
+
filename: string,
|
|
104
|
+
parsedLogics: ParsedLogic[],
|
|
105
|
+
nodeModulesPath: string,
|
|
106
|
+
shouldIgnore: (absolutePath: string) => boolean,
|
|
107
|
+
): Promise<{ filesToWrite: number; writtenFiles: number }> {
|
|
108
|
+
const { log } = appOptions
|
|
109
|
+
const unchanged = { filesToWrite: 0, writtenFiles: 0 }
|
|
110
|
+
|
|
111
|
+
if (!filename.match(/\.tsx?$/)) {
|
|
112
|
+
return unchanged
|
|
113
|
+
}
|
|
114
|
+
const sourceFile = program.getSourceFile(filename)
|
|
115
|
+
if (!sourceFile) {
|
|
116
|
+
throw new Error(`Could not find source file: ${filename}`)
|
|
117
|
+
}
|
|
118
|
+
const rawCode = sourceFile.text
|
|
119
|
+
const edits: TextEdit[] = []
|
|
120
|
+
const managedLogics: ParsedLogic[] = []
|
|
121
|
+
|
|
122
|
+
for (const parsedLogic of parsedLogics) {
|
|
123
|
+
const existingDeclaration = sourceFile.statements.find(
|
|
124
|
+
(statement): statement is ts.TypeAliasDeclaration | ts.InterfaceDeclaration =>
|
|
125
|
+
(ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) &&
|
|
126
|
+
statement.name.text === parsedLogic.logicTypeName,
|
|
127
|
+
)
|
|
128
|
+
if (existingDeclaration && !isGeneratedInlineBlock(existingDeclaration, parsedLogic.logicName)) {
|
|
129
|
+
// a manually written type with the same name - leave the logic alone
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
managedLogics.push(parsedLogic)
|
|
133
|
+
|
|
134
|
+
const printedBlock = printInlineLogicType(parsedLogic)
|
|
135
|
+
const block = (
|
|
136
|
+
appOptions.prettier === false ? printedBlock : await runThroughPrettier(printedBlock, filename)
|
|
137
|
+
).trimEnd()
|
|
138
|
+
|
|
139
|
+
if (existingDeclaration) {
|
|
140
|
+
const blockStatements = getInlineBlockStatements(existingDeclaration, parsedLogic.logicName)
|
|
141
|
+
const start = getBlockStart(blockStatements[0], sourceFile)
|
|
142
|
+
const end = existingDeclaration.getEnd()
|
|
143
|
+
if (!sameGeneratedCode(rawCode.slice(start, end), block)) {
|
|
144
|
+
edits.push({ start, end, text: block })
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const statement = getTopLevelStatement(parsedLogic.node)
|
|
148
|
+
// insert above the statement's leading comments, so they stay attached to the logic
|
|
149
|
+
const leadingComments = ts.getLeadingCommentRanges(rawCode, statement.pos) ?? []
|
|
150
|
+
const insertPos = leadingComments.length > 0 ? leadingComments[0].pos : statement.getStart(sourceFile)
|
|
151
|
+
edits.push({ start: insertPos, end: insertPos, text: `${block}\n\n` })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// make sure the kea call is typed with the local logic type: kea<logicType>(...)
|
|
155
|
+
const callExpression = parsedLogic.node.parent
|
|
156
|
+
if (ts.isCallExpression(callExpression)) {
|
|
157
|
+
const start = callExpression.expression.getEnd()
|
|
158
|
+
const end = callExpression.typeArguments ? getTypeArgumentInsertEnd(callExpression, sourceFile) : start
|
|
159
|
+
const text = `<${parsedLogic.logicTypeName}>`
|
|
160
|
+
if (rawCode.slice(start, end) !== text) {
|
|
161
|
+
edits.push({ start, end, text })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// annotate untyped selector combiner parameters, e.g. `(counter) => ...` -> `(counter: number) => ...`.
|
|
166
|
+
// MakeLogicType types these as `any`, so without an explicit annotation the inferred
|
|
167
|
+
// value types would degrade to `any` on the next typegen run.
|
|
168
|
+
for (const { parameter, typeNode } of parsedLogic.selectorParamAnnotations) {
|
|
169
|
+
const insertPos = (parameter.questionToken ?? parameter.name).getEnd()
|
|
170
|
+
edits.push({ start: insertPos, end: insertPos, text: `: ${nodeToString(typeNode)}` })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (managedLogics.length > 0) {
|
|
175
|
+
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
|
|
176
|
+
const newImportLines: string[] = []
|
|
177
|
+
|
|
178
|
+
// make sure MakeLogicType is imported from kea
|
|
179
|
+
if (!importsMakeLogicType(importDeclarations)) {
|
|
180
|
+
const keaNamedImport = importDeclarations.find(
|
|
181
|
+
(declaration) =>
|
|
182
|
+
ts.isStringLiteralLike(declaration.moduleSpecifier) &&
|
|
183
|
+
declaration.moduleSpecifier.text === 'kea' &&
|
|
184
|
+
declaration.importClause?.namedBindings &&
|
|
185
|
+
ts.isNamedImports(declaration.importClause.namedBindings) &&
|
|
186
|
+
declaration.importClause.namedBindings.elements.length > 0,
|
|
187
|
+
)
|
|
188
|
+
if (keaNamedImport) {
|
|
189
|
+
const firstElement = (keaNamedImport.importClause.namedBindings as ts.NamedImports).elements[0]
|
|
190
|
+
const pos = firstElement.getStart(sourceFile)
|
|
191
|
+
edits.push({ start: pos, end: pos, text: 'MakeLogicType, ' })
|
|
192
|
+
} else {
|
|
193
|
+
newImportLines.push(`import type { MakeLogicType } from 'kea'`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// remove the now obsolete `import type { logicType } from './logicType'`, if any
|
|
198
|
+
const typeFileNameBase = parsedLogics[0].typeFileName.replace(/\.[tj]sx?$/, '')
|
|
199
|
+
for (const declaration of importDeclarations) {
|
|
200
|
+
if (!ts.isStringLiteralLike(declaration.moduleSpecifier)) {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
const specifier = declaration.moduleSpecifier.text
|
|
204
|
+
if (!specifier.startsWith('.')) {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if (osPath.resolve(osPath.dirname(filename), specifier) === typeFileNameBase) {
|
|
208
|
+
const start = declaration.getStart(sourceFile)
|
|
209
|
+
let end = declaration.getEnd()
|
|
210
|
+
if (rawCode[end] === '\n') {
|
|
211
|
+
end += 1
|
|
212
|
+
}
|
|
213
|
+
edits.push({ start, end, text: '' })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// import referenced types that are not yet available in this file
|
|
218
|
+
const knownNames = collectKnownTypeNames(sourceFile)
|
|
219
|
+
for (const parsedLogic of parsedLogics) {
|
|
220
|
+
knownNames.add(parsedLogic.logicTypeName)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const mergedTypeReferences: Record<string, Set<string>> = {}
|
|
224
|
+
for (const parsedLogic of managedLogics) {
|
|
225
|
+
for (const [file, names] of Object.entries(parsedLogic.typeReferencesToImportFromFiles)) {
|
|
226
|
+
mergedTypeReferences[file] = mergedTypeReferences[file] ?? new Set()
|
|
227
|
+
for (const name of names) {
|
|
228
|
+
mergedTypeReferences[file].add(name)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const entry of buildTypeImportEntries(mergedTypeReferences, filename, nodeModulesPath, shouldIgnore)) {
|
|
234
|
+
const absolutePath = entry.fullPath.startsWith('.')
|
|
235
|
+
? osPath.resolve(osPath.dirname(filename), entry.fullPath)
|
|
236
|
+
: entry.fullPath
|
|
237
|
+
if (absolutePath === filename || absolutePath === parsedLogics[0].typeFileName) {
|
|
238
|
+
// types declared in this very file, or in the logicType.ts file we're replacing
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
const missing = entry.list.filter((name) => !knownNames.has(name))
|
|
242
|
+
missing.forEach((name) => knownNames.add(name))
|
|
243
|
+
if (missing.length > 0) {
|
|
244
|
+
newImportLines.push(`import type { ${missing.join(', ')} } from '${entry.finalPath}'`)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (newImportLines.length > 0) {
|
|
249
|
+
const insertPos = getImportInsertPosition(sourceFile, rawCode)
|
|
250
|
+
const text =
|
|
251
|
+
importDeclarations.length > 0
|
|
252
|
+
? `\n${newImportLines.join('\n')}${rawCode.slice(insertPos, insertPos + 1) === '\n' ? '' : '\n'}`
|
|
253
|
+
: `${newImportLines.join('\n')}\n`
|
|
254
|
+
edits.push({ start: insertPos, end: insertPos, text })
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (edits.length === 0) {
|
|
259
|
+
if (appOptions.verbose) {
|
|
260
|
+
log(`🤷 Unchanged: ${osPath.relative(process.cwd(), filename)}`)
|
|
261
|
+
}
|
|
262
|
+
return unchanged
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!appOptions.write) {
|
|
266
|
+
log(`❌ Will not write inline logic types: ${osPath.relative(process.cwd(), filename)}`)
|
|
267
|
+
return { filesToWrite: 1, writtenFiles: 0 }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const editedCode = applyTextEdits(rawCode, edits)
|
|
271
|
+
const newText = appOptions.prettier === false ? editedCode : await runThroughPrettier(editedCode, filename)
|
|
272
|
+
fs.writeFileSync(filename, newText)
|
|
273
|
+
log(`🔥 Writing inline logic types: ${osPath.relative(process.cwd(), filename)}`)
|
|
274
|
+
return { filesToWrite: 1, writtenFiles: 1 }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Compare generated code ignoring formatting, so a formatter (prettier, oxfmt, ...) reflowing the
|
|
279
|
+
* block - semicolons, trailing commas, quotes, line breaks - does not make us rewrite it forever.
|
|
280
|
+
*/
|
|
281
|
+
function sameGeneratedCode(existingCode: string, generatedCode: string): boolean {
|
|
282
|
+
const normalize = (code: string): string =>
|
|
283
|
+
code
|
|
284
|
+
.replace(/"/g, "'")
|
|
285
|
+
.replace(/[;,]/g, '')
|
|
286
|
+
.replace(/\s+/g, '')
|
|
287
|
+
// leading pipe of a formatter-reflowed union type, e.g. `selection: | number | EditorRange`
|
|
288
|
+
.replace(/([:(<=[])\|/g, '$1')
|
|
289
|
+
return normalize(existingCode) === normalize(generatedCode)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Start of the generated block: the kea-typegen marker comment if present, otherwise the declaration itself */
|
|
293
|
+
function getBlockStart(declaration: ts.Statement, sourceFile: ts.SourceFile): number {
|
|
294
|
+
const ranges = ts.getLeadingCommentRanges(sourceFile.text, declaration.pos) ?? []
|
|
295
|
+
const marker = ranges.find(({ pos, end }) => sourceFile.text.slice(pos, end).includes('kea-typegen'))
|
|
296
|
+
return marker ? marker.pos : declaration.getStart(sourceFile)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getTopLevelStatement(node: ts.Node): ts.Node {
|
|
300
|
+
let statement = node
|
|
301
|
+
while (statement.parent && !ts.isSourceFile(statement.parent)) {
|
|
302
|
+
statement = statement.parent
|
|
303
|
+
}
|
|
304
|
+
return statement
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function importsMakeLogicType(importDeclarations: ts.ImportDeclaration[]): boolean {
|
|
308
|
+
return importDeclarations.some(
|
|
309
|
+
(declaration) =>
|
|
310
|
+
ts.isStringLiteralLike(declaration.moduleSpecifier) &&
|
|
311
|
+
declaration.moduleSpecifier.text === 'kea' &&
|
|
312
|
+
declaration.importClause?.namedBindings &&
|
|
313
|
+
ts.isNamedImports(declaration.importClause.namedBindings) &&
|
|
314
|
+
declaration.importClause.namedBindings.elements.some(
|
|
315
|
+
(element) => element.name.text === 'MakeLogicType' && !element.propertyName,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** All type-ish names already bound at the top level of the file */
|
|
321
|
+
function collectKnownTypeNames(sourceFile: ts.SourceFile): Set<string> {
|
|
322
|
+
const names = new Set<string>()
|
|
323
|
+
for (const statement of sourceFile.statements) {
|
|
324
|
+
if (ts.isImportDeclaration(statement) && statement.importClause) {
|
|
325
|
+
if (statement.importClause.name) {
|
|
326
|
+
names.add(statement.importClause.name.text)
|
|
327
|
+
}
|
|
328
|
+
const namedBindings = statement.importClause.namedBindings
|
|
329
|
+
if (namedBindings) {
|
|
330
|
+
if (ts.isNamedImports(namedBindings)) {
|
|
331
|
+
namedBindings.elements.forEach((element) => names.add(element.name.text))
|
|
332
|
+
} else {
|
|
333
|
+
names.add(namedBindings.name.text)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (
|
|
337
|
+
(ts.isTypeAliasDeclaration(statement) ||
|
|
338
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
339
|
+
ts.isClassDeclaration(statement) ||
|
|
340
|
+
ts.isEnumDeclaration(statement) ||
|
|
341
|
+
ts.isFunctionDeclaration(statement)) &&
|
|
342
|
+
statement.name
|
|
343
|
+
) {
|
|
344
|
+
names.add(statement.name.text)
|
|
345
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
346
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
347
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
348
|
+
names.add(declaration.name.text)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return names
|
|
354
|
+
}
|
|
@@ -3,13 +3,13 @@ import * as ts from 'typescript'
|
|
|
3
3
|
import * as osPath from 'path'
|
|
4
4
|
import { runThroughPrettier } from '../print/print'
|
|
5
5
|
import * as fs from 'fs'
|
|
6
|
-
interface TextEdit {
|
|
6
|
+
export interface TextEdit {
|
|
7
7
|
start: number
|
|
8
8
|
end: number
|
|
9
9
|
text: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function applyTextEdits(source: string, edits: TextEdit[]): string {
|
|
12
|
+
export function applyTextEdits(source: string, edits: TextEdit[]): string {
|
|
13
13
|
return edits
|
|
14
14
|
.sort((a, b) => b.start - a.start || b.end - a.end)
|
|
15
15
|
.reduce((output, { start, end, text }) => output.slice(0, start) + text + output.slice(end), source)
|
|
@@ -19,7 +19,7 @@ function getImportPath(importDeclaration: ts.ImportDeclaration): string | null {
|
|
|
19
19
|
return ts.isStringLiteralLike(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : null
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
|
|
22
|
+
export function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
|
|
23
23
|
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
|
|
24
24
|
if (importDeclarations.length > 0) {
|
|
25
25
|
return importDeclarations[importDeclarations.length - 1].getEnd()
|
|
@@ -29,7 +29,7 @@ function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): nu
|
|
|
29
29
|
return shebangMatch ? shebangMatch[0].length : 0
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
|
|
32
|
+
export function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
|
|
33
33
|
const openParenToken = callExpression
|
|
34
34
|
.getChildren(sourceFile)
|
|
35
35
|
.find((child) => child.kind === ts.SyntaxKind.OpenParenToken)
|