@kubb/parser-ts 5.0.0-beta.4 → 5.0.0-beta.41

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/parserTsx.ts CHANGED
@@ -1,20 +1,37 @@
1
- import type { Parser } from '@kubb/core'
2
1
  import { defineParser } from '@kubb/core'
2
+ import type * as ts from 'typescript'
3
3
  import { parserTs } from './parserTs.ts'
4
+ import { print } from './utils.ts'
4
5
 
5
6
  /**
6
- * Parser that converts `.tsx` and `.jsx` files to strings.
7
- * Delegates to `typescriptParser` since the TypeScript compiler natively
8
- * supports JSX/TSX syntax via `ScriptKind.TSX`.
7
+ * Kubb parser for `.tsx` and `.jsx` files. Delegates to `parserTs` because the
8
+ * TypeScript compiler handles JSX natively via `ScriptKind.TSX`.
9
9
  *
10
- * Add this parser to the `parsers` option in `defineConfig` when generating `.tsx`/`.jsx` files.
10
+ * Add to the `parsers` array on `defineConfig` when generating components for
11
+ * React (or any framework that emits JSX).
11
12
  *
12
- * @default extname '.tsx'
13
+ * @example
14
+ * ```ts
15
+ * import { defineConfig } from 'kubb'
16
+ * import { adapterOas } from '@kubb/adapter-oas'
17
+ * import { parserTsx } from '@kubb/parser-ts'
18
+ *
19
+ * export default defineConfig({
20
+ * input: { path: './petStore.yaml' },
21
+ * output: { path: './src/gen' },
22
+ * adapter: adapterOas(),
23
+ * parsers: [parserTsx],
24
+ * plugins: [],
25
+ * })
26
+ * ```
13
27
  */
14
- export const parserTsx: Parser = defineParser({
28
+ export const parserTsx = defineParser({
15
29
  name: 'tsx',
16
30
  extNames: ['.tsx', '.jsx'],
17
- async parse(file, options = { extname: '.tsx' }) {
31
+ print(...nodes: Array<ts.Node>) {
32
+ return print(...nodes)
33
+ },
34
+ parse(file, options = { extname: '.tsx' }) {
18
35
  return parserTs.parse(file, options)
19
36
  },
20
37
  })
package/src/utils.ts ADDED
@@ -0,0 +1,458 @@
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
+ * Normalizes 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
+
66
+ const parts: Array<string> = []
67
+
68
+ for (const node of nodes) {
69
+ parts.push(printCodeNode(node))
70
+ }
71
+
72
+ return parts.join('\n')
73
+ }
74
+
75
+ /**
76
+ * Indents every non-empty line of `text` by `spaces` spaces.
77
+ */
78
+ export function indentLines(text: string, spaces: number = INDENT_SIZE): string {
79
+ if (!text) return ''
80
+ const pad = ' '.repeat(spaces)
81
+ return text
82
+ .split('\n')
83
+ .map((line) => (line.trim() ? `${pad}${line}` : ''))
84
+ .join('\n')
85
+ }
86
+
87
+ /**
88
+ * Renders the generic clause (`<T, U>`) shared by function and arrow-function nodes.
89
+ * Accepts either a raw string (rendered verbatim) or an array of type-parameter names.
90
+ */
91
+ export function formatGenerics(generics: FunctionNode['generics'] | ArrowFunctionNode['generics']): string {
92
+ if (!generics) return ''
93
+ return `<${Array.isArray(generics) ? generics.join(', ') : generics}>`
94
+ }
95
+
96
+ /**
97
+ * Renders the return-type suffix (`: T` or `: Promise<T>` when `isAsync` is true).
98
+ * Returns an empty string when no return type is provided.
99
+ */
100
+ export function formatReturnType(returnType: string | null | undefined, isAsync: boolean | null | undefined): string {
101
+ if (!returnType) return ''
102
+ return isAsync ? `: Promise<${returnType}>` : `: ${returnType}`
103
+ }
104
+
105
+ /**
106
+ * Module-scoped TypeScript printer instance. `ts.createPrinter()` is stateless across calls
107
+ * (it does not mutate the source file) so a single instance can be safely reused for every
108
+ * `print()` call. Hoisting it out of `print()` avoids re-running the printer initialization
109
+ * for each file's import/export section.
110
+ */
111
+ const TS_PRINTER = ts.createPrinter({
112
+ omitTrailingSemicolon: true,
113
+ newLine: ts.NewLineKind.LineFeed,
114
+ removeComments: false,
115
+ noEmitHelpers: true,
116
+ })
117
+
118
+ /**
119
+ * Module-scoped source file used as the print target. `printList` only reads the source
120
+ * file's compiler options / language version. It never mutates it.
121
+ */
122
+ const PRINT_SOURCE_FILE = ts.createSourceFile('print.tsx', '', ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX)
123
+
124
+ // Pre-warm the printer at module load. The first `printList` call lazily initializes
125
+ // the printer's internal string-builder and identifier tables. Doing it once at import
126
+ // time keeps that cost off the critical path for short-lived CLI builds.
127
+ TS_PRINTER.printList(ts.ListFormat.MultiLine, factory.createNodeArray([]), PRINT_SOURCE_FILE)
128
+
129
+ /**
130
+ * Converts TypeScript/TSX AST nodes to a string using the TypeScript printer.
131
+ */
132
+ export function print(...elements: Array<ts.Node>): string {
133
+ const filtered = elements.filter(Boolean)
134
+ if (filtered.length === 0) return ''
135
+
136
+ const output = TS_PRINTER.printList(ts.ListFormat.MultiLine, factory.createNodeArray(filtered), PRINT_SOURCE_FILE)
137
+
138
+ return output.replace(CRLF_PATTERN, '\n')
139
+ }
140
+
141
+ /**
142
+ * Converts a {@link JSDocNode} to a JSDoc comment block string.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * printJSDoc({ comments: ['@description A pet', '@deprecated'] })
147
+ * // /**
148
+ * // * @description A pet
149
+ * // * @deprecated
150
+ * // *\/
151
+ * ```
152
+ */
153
+ export function printJSDoc(jsDoc: JSDocNode): string {
154
+ const comments = (jsDoc.comments ?? []).filter((c) => c != null)
155
+ if (comments.length === 0) return ''
156
+
157
+ const lines = comments
158
+ .flatMap((c) => c.split(/\r?\n/))
159
+ .map((l) => l.replace(JSDOC_TERMINATOR_PATTERN, '* /').replace(CARRIAGE_RETURN_PATTERN, ''))
160
+ .filter((l) => l.trim().length > 0)
161
+
162
+ if (lines.length === 0) return ''
163
+
164
+ return ['/**', ...lines.map((l) => ` * ${l}`), ' */'].join('\n')
165
+ }
166
+
167
+ /**
168
+ * Converts a {@link ConstNode} to a TypeScript `const` declaration string.
169
+ *
170
+ * Mirrors the `Const` component from `@kubb/renderer-jsx`.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * printConst(createConst({ name: 'pet', export: true, nodes: ['{}'] }))
175
+ * // 'export const pet = {}'
176
+ * ```
177
+ *
178
+ * @example With type and `as const`
179
+ * ```ts
180
+ * printConst(createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true, nodes: ['[]'] }))
181
+ * // 'export const pets: Pet[] = [] as const'
182
+ * ```
183
+ */
184
+ export function printConst(node: ConstNode): string {
185
+ const { name, export: canExport, type, JSDoc, asConst, nodes } = node
186
+
187
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
188
+ const body = printNodes(nodes)
189
+
190
+ const parts: Array<string> = []
191
+ if (canExport) parts.push('export ')
192
+ parts.push('const ')
193
+ parts.push(name)
194
+ if (type) {
195
+ parts.push(`: ${type}`)
196
+ }
197
+ parts.push(' = ')
198
+ parts.push(body)
199
+ if (asConst) parts.push(' as const')
200
+
201
+ const declaration = parts.join('')
202
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
203
+ }
204
+
205
+ /**
206
+ * Converts a {@link TypeNode} to a TypeScript `type` alias declaration string.
207
+ *
208
+ * Mirrors the `Type` component from `@kubb/renderer-jsx`.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * printType(createType({ name: 'Pet', export: true, nodes: ['{ id: number }'] }))
213
+ * // 'export type Pet = { id: number }'
214
+ * ```
215
+ */
216
+ export function printType(node: TypeNode): string {
217
+ const { name, export: canExport, JSDoc, nodes } = node
218
+
219
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
220
+ const body = printNodes(nodes)
221
+
222
+ const parts: Array<string> = []
223
+ if (canExport) parts.push('export ')
224
+ parts.push('type ')
225
+ parts.push(name)
226
+ parts.push(' = ')
227
+ parts.push(body)
228
+
229
+ const declaration = parts.join('')
230
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
231
+ }
232
+
233
+ /**
234
+ * Converts a {@link FunctionNode} to a TypeScript `function` declaration string.
235
+ *
236
+ * Mirrors the `Function` component from `@kubb/renderer-jsx`.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * printFunction(createFunction({ name: 'getPet', export: true, params: 'id: string', returnType: 'Pet', nodes: ['return fetch(id)'] }))
241
+ * // 'export function getPet(id: string): Pet {\n return fetch(id)\n}'
242
+ * ```
243
+ *
244
+ * @example Async with generics
245
+ * ```ts
246
+ * printFunction(createFunction({ name: 'fetchPet', export: true, async: true, generics: ['T'], params: 'id: string', returnType: 'T' }))
247
+ * // 'export async function fetchPet<T>(id: string): Promise<T> {\n}'
248
+ * ```
249
+ */
250
+ export function printFunction(node: FunctionNode): string {
251
+ const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes } = node
252
+
253
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
254
+ const body = printNodes(nodes)
255
+ const indented = body ? indentLines(body) : ''
256
+
257
+ const parts: Array<string> = []
258
+ if (canExport) parts.push('export ')
259
+ if (isDefault) parts.push('default ')
260
+ if (isAsync) parts.push('async ')
261
+ parts.push('function ')
262
+ parts.push(name)
263
+ parts.push(formatGenerics(generics))
264
+ parts.push(`(${params ?? ''})`)
265
+ parts.push(formatReturnType(returnType, isAsync))
266
+ parts.push(' {')
267
+ if (indented) {
268
+ parts.push(`\n${indented}\n`)
269
+ }
270
+ parts.push('}')
271
+
272
+ const declaration = parts.join('')
273
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
274
+ }
275
+
276
+ /**
277
+ * Converts an {@link ArrowFunctionNode} to a TypeScript arrow function declaration string.
278
+ *
279
+ * Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
280
+ *
281
+ * @example Multi-line arrow function
282
+ * ```ts
283
+ * printArrowFunction(createArrowFunction({ name: 'getPet', export: true, params: 'id: string', nodes: ['return fetch(id)'] }))
284
+ * // 'export const getPet = (id: string) => {\n return fetch(id)\n}'
285
+ * ```
286
+ *
287
+ * @example Single-line arrow function
288
+ * ```ts
289
+ * printArrowFunction(createArrowFunction({ name: 'double', params: 'n: number', singleLine: true, nodes: ['n * 2'] }))
290
+ * // 'const double = (n: number) => n * 2'
291
+ * ```
292
+ */
293
+ export function printArrowFunction(node: ArrowFunctionNode): string {
294
+ const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes, singleLine } = node
295
+
296
+ const jsDocStr = JSDoc ? printJSDoc(JSDoc) : ''
297
+ const body = printNodes(nodes)
298
+ const arrowBody = singleLine ? ` => ${body}` : body ? ` => {\n${indentLines(body)}\n}` : ' => {}'
299
+
300
+ const parts: Array<string> = []
301
+ if (canExport) parts.push('export ')
302
+ if (isDefault) parts.push('default ')
303
+ parts.push('const ')
304
+ parts.push(name)
305
+ parts.push(' = ')
306
+ if (isAsync) parts.push('async ')
307
+ parts.push(formatGenerics(generics))
308
+ parts.push(`(${params ?? ''})`)
309
+ parts.push(formatReturnType(returnType, isAsync))
310
+ parts.push(arrowBody)
311
+
312
+ const declaration = parts.join('')
313
+ return [jsDocStr, declaration].filter(Boolean).join('\n')
314
+ }
315
+
316
+ /**
317
+ * Converts a {@link CodeNode} to its TypeScript string representation.
318
+ *
319
+ * Dispatches to the appropriate printer based on the node's `kind`.
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * printCodeNode(createConst({ name: 'x', nodes: ['1'] }))
324
+ * // 'const x = 1'
325
+ * ```
326
+ */
327
+ export function printCodeNode(node: CodeNode): string {
328
+ if (node.kind === 'Break') return ''
329
+ if (node.kind === 'Text') return (node as TextNode).value
330
+ if (node.kind === 'Jsx') return (node as JsxNode).value
331
+ if (node.kind === 'Const') return printConst(node)
332
+ if (node.kind === 'Type') return printType(node)
333
+ if (node.kind === 'Function') return printFunction(node)
334
+ if (node.kind === 'ArrowFunction') return printArrowFunction(node)
335
+ return ''
336
+ }
337
+
338
+ /**
339
+ * Converts a {@link SourceNode} to its TypeScript string representation.
340
+ *
341
+ * Iterates `nodes` in DOM order, rendering each {@link CodeNode} via
342
+ * {@link printCodeNode}.
343
+ *
344
+ * @example From nodes
345
+ * ```ts
346
+ * printSource({ kind: 'Source', nodes: [createConst({ name: 'x', nodes: [createText('1')] }), createText('x.toString()')] })
347
+ * // 'const x = 1\nx.toString()'
348
+ * ```
349
+ */
350
+ export function printSource(node: SourceNode): string {
351
+ const nodes = node.nodes
352
+
353
+ if (!nodes || nodes.length === 0) return ''
354
+ const parts: Array<string> = []
355
+
356
+ for (const child of nodes) {
357
+ parts.push(printCodeNode(child as CodeNode))
358
+ }
359
+
360
+ return parts.join('\n')
361
+ }
362
+
363
+ export function createImport({
364
+ name,
365
+ path,
366
+ root,
367
+ isTypeOnly: isTypeOnlyRaw = false,
368
+ isNameSpace: isNameSpaceRaw = false,
369
+ }: {
370
+ name: string | Array<string | { propertyName: string; name?: string }>
371
+ path: string
372
+ root?: string | null
373
+ /** @default false */
374
+ isTypeOnly?: boolean | null
375
+ /** @default false */
376
+ isNameSpace?: boolean | null
377
+ }): ts.ImportDeclaration {
378
+ const isTypeOnly = isTypeOnlyRaw ?? false
379
+ const isNameSpace = isNameSpaceRaw ?? false
380
+ const resolvePath = root ? getRelativePath(root, path) : path
381
+
382
+ if (!Array.isArray(name)) {
383
+ if (isNameSpace) {
384
+ return factory.createImportDeclaration(
385
+ undefined,
386
+ factory.createImportClause(isTypeOnly, undefined, factory.createNamespaceImport(factory.createIdentifier(name))),
387
+ factory.createStringLiteral(resolvePath),
388
+ undefined,
389
+ )
390
+ }
391
+
392
+ return factory.createImportDeclaration(
393
+ undefined,
394
+ factory.createImportClause(isTypeOnly, factory.createIdentifier(name), undefined),
395
+ factory.createStringLiteral(resolvePath),
396
+ undefined,
397
+ )
398
+ }
399
+
400
+ const specifiers = name.map((item) => {
401
+ if (typeof item === 'object') {
402
+ const { propertyName, name: alias } = item
403
+ return factory.createImportSpecifier(false, alias ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(alias ?? propertyName))
404
+ }
405
+ return factory.createImportSpecifier(false, undefined, factory.createIdentifier(item))
406
+ })
407
+
408
+ return factory.createImportDeclaration(
409
+ undefined,
410
+ factory.createImportClause(isTypeOnly, undefined, factory.createNamedImports(specifiers)),
411
+ factory.createStringLiteral(resolvePath),
412
+ undefined,
413
+ )
414
+ }
415
+
416
+ export function createExport({
417
+ path,
418
+ asAlias: asAliasRaw,
419
+ isTypeOnly: isTypeOnlyRaw = false,
420
+ name,
421
+ }: {
422
+ path: string
423
+ /** @default false */
424
+ asAlias?: boolean | null
425
+ /** @default false */
426
+ isTypeOnly?: boolean | null
427
+ name?: string | Array<ts.Identifier | string> | null
428
+ }): ts.ExportDeclaration {
429
+ const asAlias = asAliasRaw ?? false
430
+ const isTypeOnly = isTypeOnlyRaw ?? false
431
+ if (name && !Array.isArray(name) && !asAlias) {
432
+ console.warn(`When using name as string, asAlias should be true: ${name}`)
433
+ }
434
+
435
+ if (!Array.isArray(name)) {
436
+ const parsedName = name && LEADING_DIGIT_PATTERN.test(name) ? `_${name.slice(1)}` : name
437
+
438
+ return factory.createExportDeclaration(
439
+ undefined,
440
+ isTypeOnly,
441
+ asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : undefined,
442
+ factory.createStringLiteral(path),
443
+ undefined,
444
+ )
445
+ }
446
+
447
+ return factory.createExportDeclaration(
448
+ undefined,
449
+ isTypeOnly,
450
+ factory.createNamedExports(
451
+ name.map((propertyName) =>
452
+ factory.createExportSpecifier(false, undefined, typeof propertyName === 'string' ? factory.createIdentifier(propertyName) : propertyName),
453
+ ),
454
+ ),
455
+ factory.createStringLiteral(path),
456
+ undefined,
457
+ )
458
+ }
File without changes