@kubb/parser-ts 5.0.0-beta.6 → 5.0.0-beta.61

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