@kubb/parser-ts 5.0.0-beta.7 → 5.0.0-beta.8

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/src/utils.ts ADDED
@@ -0,0 +1,459 @@
1
+ import { normalize, relative } from 'node:path'
2
+ import type { ArrowFunctionNode, CodeNode, ConstNode, FunctionNode, JSDocNode, JsxNode, SourceNode, TextNode, TypeNode } from '@kubb/ast'
3
+ import ts from 'typescript'
4
+ import {
5
+ CARRIAGE_RETURN_PATTERN,
6
+ CRLF_PATTERN,
7
+ CURRENT_DIRECTORY_PREFIX,
8
+ FILE_EXTENSION_PATTERN,
9
+ INDENT_SIZE,
10
+ JSDOC_TERMINATOR_PATTERN,
11
+ LEADING_DIGIT_PATTERN,
12
+ PARENT_DIRECTORY_PREFIX,
13
+ WINDOWS_PATH_SEPARATOR,
14
+ } from './constants.ts'
15
+
16
+ const { factory } = ts
17
+
18
+ /**
19
+ * Normalises a file-system path to POSIX separators and strips any leading `../` segment.
20
+ */
21
+ export function slash(path: string): string {
22
+ return normalize(path).replaceAll(WINDOWS_PATH_SEPARATOR, '/').replace(PARENT_DIRECTORY_PREFIX, '')
23
+ }
24
+
25
+ /**
26
+ * Resolves `filePath` relative to `rootDir` and returns a POSIX-style path
27
+ * prefixed with `./` when the target sits inside the root, or `../` when it escapes it.
28
+ */
29
+ export function getRelativePath(rootDir: string, filePath: string): string {
30
+ const rel = relative(rootDir, filePath)
31
+ const slashed = slash(rel)
32
+ return slashed.startsWith(PARENT_DIRECTORY_PREFIX) ? slashed : `${CURRENT_DIRECTORY_PREFIX}${slashed}`
33
+ }
34
+
35
+ /**
36
+ * Strips the trailing file extension (for example `.ts`) from a path.
37
+ * Preserves intermediate dots like `foo.bar.ts` → `foo.bar`.
38
+ */
39
+ export function trimExtName(text: string): string {
40
+ return text.replace(FILE_EXTENSION_PATTERN, '')
41
+ }
42
+
43
+ /**
44
+ * Rewrites an import/export path so its extension matches the caller-supplied
45
+ * `options.extname`. When the source path has no extension the original is kept,
46
+ * so virtual/module-only paths flow through unchanged.
47
+ */
48
+ export function resolveOutputPath(path: string, options: { extname?: string } | undefined, rootAware: boolean): string {
49
+ const hasExtname = FILE_EXTENSION_PATTERN.test(path)
50
+ if (options?.extname && hasExtname) {
51
+ return `${trimExtName(path)}${options.extname}`
52
+ }
53
+ return rootAware ? trimExtName(path) : path
54
+ }
55
+
56
+ /**
57
+ * Serializes the body / value content from a `nodes` array.
58
+ *
59
+ * Each element is either a raw string or a structured {@link CodeNode}
60
+ * (recursively converted via {@link printCodeNode}).
61
+ * Elements are joined with `\n`.
62
+ */
63
+ export function printNodes(nodes: Array<CodeNode> | undefined): string {
64
+ if (!nodes || nodes.length === 0) return ''
65
+ return nodes.map(printCodeNode).join('\n')
66
+ }
67
+
68
+ /**
69
+ * Indents every non-empty line of `text` by `spaces` spaces.
70
+ */
71
+ export function indentLines(text: string, spaces: number = INDENT_SIZE): string {
72
+ if (!text) return ''
73
+ const pad = ' '.repeat(spaces)
74
+ return text
75
+ .split('\n')
76
+ .map((line) => (line.trim() ? `${pad}${line}` : ''))
77
+ .join('\n')
78
+ }
79
+
80
+ /**
81
+ * Renders the generic clause (`<T, U>`) shared by function and arrow-function nodes.
82
+ * Accepts either a raw string (rendered verbatim) or an array of type-parameter names.
83
+ */
84
+ export function formatGenerics(generics: FunctionNode['generics'] | ArrowFunctionNode['generics']): string {
85
+ if (!generics) return ''
86
+ return `<${Array.isArray(generics) ? generics.join(', ') : generics}>`
87
+ }
88
+
89
+ /**
90
+ * Renders the return-type suffix (`: T` or `: Promise<T>` when `isAsync` is true).
91
+ * Returns an empty string when no return type is provided.
92
+ */
93
+ export function formatReturnType(returnType: string | undefined, isAsync: boolean | undefined): string {
94
+ if (!returnType) return ''
95
+ return isAsync ? `: Promise<${returnType}>` : `: ${returnType}`
96
+ }
97
+
98
+ /**
99
+ * Validates TypeScript AST nodes before printing.
100
+ * Throws an error if any node has SyntaxKind.Unknown which would cause the
101
+ * TypeScript printer to crash.
102
+ */
103
+ export function validateNodes(...nodes: ts.Node[]): void {
104
+ for (const node of nodes) {
105
+ if (!node) {
106
+ throw new Error('Attempted to print undefined or null TypeScript node')
107
+ }
108
+ if (node.kind === ts.SyntaxKind.Unknown) {
109
+ throw new Error(
110
+ 'Invalid TypeScript AST node detected with SyntaxKind.Unknown. ' +
111
+ 'This typically indicates a schema pattern that could not be properly converted to TypeScript. ' +
112
+ `Node: ${JSON.stringify(node, null, 2)}`,
113
+ )
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Converts TypeScript/TSX AST nodes to a string using the TypeScript printer.
120
+ */
121
+ export function print(...elements: Array<ts.Node>): string {
122
+ const sourceFile = ts.createSourceFile('print.tsx', '', ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX)
123
+
124
+ const printer = ts.createPrinter({
125
+ omitTrailingSemicolon: true,
126
+ newLine: ts.NewLineKind.LineFeed,
127
+ removeComments: false,
128
+ noEmitHelpers: true,
129
+ })
130
+
131
+ const output = printer.printList(ts.ListFormat.MultiLine, factory.createNodeArray(elements.filter(Boolean)), sourceFile)
132
+
133
+ return output.replace(CRLF_PATTERN, '\n')
134
+ }
135
+
136
+ /**
137
+ * Like `print` but validates nodes first to surface issues early.
138
+ */
139
+ export function safePrint(...elements: Array<ts.Node>): string {
140
+ validateNodes(...elements)
141
+ return print(...elements)
142
+ }
143
+
144
+ /**
145
+ * Converts a {@link JSDocNode} to a JSDoc comment block string.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * printJSDoc({ comments: ['@description A pet', '@deprecated'] })
150
+ * // /**
151
+ * // * @description A pet
152
+ * // * @deprecated
153
+ * // *\/
154
+ * ```
155
+ */
156
+ export function printJSDoc(jsDoc: JSDocNode): string {
157
+ const comments = (jsDoc.comments ?? []).filter((c) => c != null)
158
+ if (comments.length === 0) return ''
159
+
160
+ const lines = comments
161
+ .flatMap((c) => c.split(/\r?\n/))
162
+ .map((l) => l.replace(JSDOC_TERMINATOR_PATTERN, '* /').replace(CARRIAGE_RETURN_PATTERN, ''))
163
+ .filter((l) => l.trim().length > 0)
164
+
165
+ if (lines.length === 0) return ''
166
+
167
+ return ['/**', ...lines.map((l) => ` * ${l}`), ' */'].join('\n')
168
+ }
169
+
170
+ /**
171
+ * Converts a {@link ConstNode} to a TypeScript `const` declaration string.
172
+ *
173
+ * Mirrors the `Const` component from `@kubb/renderer-jsx`.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * printConst(createConst({ name: 'pet', export: true, nodes: ['{}'] }))
178
+ * // 'export const pet = {}'
179
+ * ```
180
+ *
181
+ * @example With type and `as const`
182
+ * ```ts
183
+ * printConst(createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true, nodes: ['[]'] }))
184
+ * // 'export const pets: Pet[] = [] as const'
185
+ * ```
186
+ */
187
+ export function printConst(node: ConstNode): string {
188
+ const { name, export: canExport, type, JSDoc, asConst, nodes } = node
189
+
190
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
191
+ const body = printNodes(nodes)
192
+
193
+ const parts: string[] = []
194
+ if (canExport) parts.push('export ')
195
+ parts.push('const ')
196
+ parts.push(name)
197
+ if (type) {
198
+ parts.push(`: ${type}`)
199
+ }
200
+ parts.push(' = ')
201
+ parts.push(body)
202
+ if (asConst) parts.push(' as const')
203
+
204
+ const declaration = parts.join('')
205
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
206
+ }
207
+
208
+ /**
209
+ * Converts a {@link TypeNode} to a TypeScript `type` alias declaration string.
210
+ *
211
+ * Mirrors the `Type` component from `@kubb/renderer-jsx`.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * printType(createType({ name: 'Pet', export: true, nodes: ['{ id: number }'] }))
216
+ * // 'export type Pet = { id: number }'
217
+ * ```
218
+ */
219
+ export function printType(node: TypeNode): string {
220
+ const { name, export: canExport, JSDoc, nodes } = node
221
+
222
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
223
+ const body = printNodes(nodes)
224
+
225
+ const parts: string[] = []
226
+ if (canExport) parts.push('export ')
227
+ parts.push('type ')
228
+ parts.push(name)
229
+ parts.push(' = ')
230
+ parts.push(body)
231
+
232
+ const declaration = parts.join('')
233
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
234
+ }
235
+
236
+ /**
237
+ * Converts a {@link FunctionNode} to a TypeScript `function` declaration string.
238
+ *
239
+ * Mirrors the `Function` component from `@kubb/renderer-jsx`.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * printFunction(createFunction({ name: 'getPet', export: true, params: 'id: string', returnType: 'Pet', nodes: ['return fetch(id)'] }))
244
+ * // 'export function getPet(id: string): Pet {\n return fetch(id)\n}'
245
+ * ```
246
+ *
247
+ * @example Async with generics
248
+ * ```ts
249
+ * printFunction(createFunction({ name: 'fetchPet', export: true, async: true, generics: ['T'], params: 'id: string', returnType: 'T' }))
250
+ * // 'export async function fetchPet<T>(id: string): Promise<T> {\n}'
251
+ * ```
252
+ */
253
+ export function printFunction(node: FunctionNode): string {
254
+ const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes } = node
255
+
256
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
257
+ const body = printNodes(nodes)
258
+ const indented = body ? indentLines(body) : ''
259
+
260
+ const parts: string[] = []
261
+ if (canExport) parts.push('export ')
262
+ if (isDefault) parts.push('default ')
263
+ if (isAsync) parts.push('async ')
264
+ parts.push('function ')
265
+ parts.push(name)
266
+ parts.push(formatGenerics(generics))
267
+ parts.push(`(${params ?? ''})`)
268
+ parts.push(formatReturnType(returnType, isAsync))
269
+ parts.push(' {')
270
+ if (indented) {
271
+ parts.push(`\n${indented}\n`)
272
+ }
273
+ parts.push('}')
274
+
275
+ const declaration = parts.join('')
276
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
277
+ }
278
+
279
+ /**
280
+ * Converts an {@link ArrowFunctionNode} to a TypeScript arrow function declaration string.
281
+ *
282
+ * Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
283
+ *
284
+ * @example Multi-line arrow function
285
+ * ```ts
286
+ * printArrowFunction(createArrowFunction({ name: 'getPet', export: true, params: 'id: string', nodes: ['return fetch(id)'] }))
287
+ * // 'export const getPet = (id: string) => {\n return fetch(id)\n}'
288
+ * ```
289
+ *
290
+ * @example Single-line arrow function
291
+ * ```ts
292
+ * printArrowFunction(createArrowFunction({ name: 'double', params: 'n: number', singleLine: true, nodes: ['n * 2'] }))
293
+ * // 'const double = (n: number) => n * 2'
294
+ * ```
295
+ */
296
+ export function printArrowFunction(node: ArrowFunctionNode): string {
297
+ const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes, singleLine } = node
298
+
299
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
300
+ const body = printNodes(nodes)
301
+ const arrowBody = singleLine ? ` => ${body}` : body ? ` => {\n${indentLines(body)}\n}` : ' => {}'
302
+
303
+ const parts: string[] = []
304
+ if (canExport) parts.push('export ')
305
+ if (isDefault) parts.push('default ')
306
+ parts.push('const ')
307
+ parts.push(name)
308
+ parts.push(' = ')
309
+ if (isAsync) parts.push('async ')
310
+ parts.push(formatGenerics(generics))
311
+ parts.push(`(${params ?? ''})`)
312
+ parts.push(formatReturnType(returnType, isAsync))
313
+ parts.push(arrowBody)
314
+
315
+ const declaration = parts.join('')
316
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
317
+ }
318
+
319
+ /**
320
+ * Converts a {@link CodeNode} to its TypeScript string representation.
321
+ *
322
+ * Dispatches to the appropriate printer based on the node's `kind`.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * printCodeNode(createConst({ name: 'x', nodes: ['1'] }))
327
+ * // 'const x = 1'
328
+ * ```
329
+ */
330
+ export function printCodeNode(node: CodeNode): string {
331
+ switch (node.kind) {
332
+ case 'Break':
333
+ return ''
334
+ case 'Text':
335
+ return (node as TextNode).value
336
+ case 'Jsx':
337
+ return (node as JsxNode).value
338
+ case 'Const':
339
+ return printConst(node)
340
+ case 'Type':
341
+ return printType(node)
342
+ case 'Function':
343
+ return printFunction(node)
344
+ case 'ArrowFunction':
345
+ return printArrowFunction(node)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Converts a {@link SourceNode} to its TypeScript string representation.
351
+ *
352
+ * Iterates `nodes` in DOM order, rendering each {@link CodeNode} via
353
+ * {@link printCodeNode}.
354
+ *
355
+ * @example From nodes
356
+ * ```ts
357
+ * printSource({ kind: 'Source', nodes: [createConst({ name: 'x', nodes: [createText('1')] }), createText('x.toString()')] })
358
+ * // 'const x = 1\nx.toString()'
359
+ * ```
360
+ */
361
+ export function printSource(node: SourceNode): string {
362
+ if (node.nodes && node.nodes.length > 0) {
363
+ return node.nodes.map(printCodeNode).join('\n')
364
+ }
365
+ return ''
366
+ }
367
+
368
+ export function createImport({
369
+ name,
370
+ path,
371
+ root,
372
+ isTypeOnly = false,
373
+ isNameSpace = false,
374
+ }: {
375
+ name: string | Array<string | { propertyName: string; name?: string }>
376
+ path: string
377
+ root?: string
378
+ /** @default false */
379
+ isTypeOnly?: boolean
380
+ /** @default false */
381
+ isNameSpace?: boolean
382
+ }): ts.ImportDeclaration {
383
+ const resolvePath = root ? getRelativePath(root, path) : path
384
+
385
+ if (!Array.isArray(name)) {
386
+ if (isNameSpace) {
387
+ return factory.createImportDeclaration(
388
+ undefined,
389
+ factory.createImportClause(isTypeOnly, undefined, factory.createNamespaceImport(factory.createIdentifier(name))),
390
+ factory.createStringLiteral(resolvePath),
391
+ undefined,
392
+ )
393
+ }
394
+
395
+ return factory.createImportDeclaration(
396
+ undefined,
397
+ factory.createImportClause(isTypeOnly, factory.createIdentifier(name), undefined),
398
+ factory.createStringLiteral(resolvePath),
399
+ undefined,
400
+ )
401
+ }
402
+
403
+ const specifiers = name.map((item) => {
404
+ if (typeof item === 'object') {
405
+ const { propertyName, name: alias } = item
406
+ return factory.createImportSpecifier(false, alias ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(alias ?? propertyName))
407
+ }
408
+ return factory.createImportSpecifier(false, undefined, factory.createIdentifier(item))
409
+ })
410
+
411
+ return factory.createImportDeclaration(
412
+ undefined,
413
+ factory.createImportClause(isTypeOnly, undefined, factory.createNamedImports(specifiers)),
414
+ factory.createStringLiteral(resolvePath),
415
+ undefined,
416
+ )
417
+ }
418
+
419
+ export function createExport({
420
+ path,
421
+ asAlias,
422
+ isTypeOnly = false,
423
+ name,
424
+ }: {
425
+ path: string
426
+ /** @default false */
427
+ asAlias?: boolean
428
+ /** @default false */
429
+ isTypeOnly?: boolean
430
+ name?: string | Array<ts.Identifier | string>
431
+ }): ts.ExportDeclaration {
432
+ if (name && !Array.isArray(name) && !asAlias) {
433
+ console.warn(`When using name as string, asAlias should be true: ${name}`)
434
+ }
435
+
436
+ if (!Array.isArray(name)) {
437
+ const parsedName = name && LEADING_DIGIT_PATTERN.test(name) ? `_${name.slice(1)}` : name
438
+
439
+ return factory.createExportDeclaration(
440
+ undefined,
441
+ isTypeOnly,
442
+ asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : undefined,
443
+ factory.createStringLiteral(path),
444
+ undefined,
445
+ )
446
+ }
447
+
448
+ return factory.createExportDeclaration(
449
+ undefined,
450
+ isTypeOnly,
451
+ factory.createNamedExports(
452
+ name.map((propertyName) =>
453
+ factory.createExportSpecifier(false, undefined, typeof propertyName === 'string' ? factory.createIdentifier(propertyName) : propertyName),
454
+ ),
455
+ ),
456
+ factory.createStringLiteral(path),
457
+ undefined,
458
+ )
459
+ }