Skip to content

Parsers

A parser turns a FileNode into the source string that storage writes to disk. Every parser registers the file extensions it handles, and FileProcessor routes each emitted file to the matching parser.

There are two distinct jobs in a parser:

  • print(...nodes) is called by plugins to render language-specific AST nodes into a string before staging them on FileNode.sources.
  • parse(file) is called by FileProcessor after all plugins have run, to join the staged sources into the final output string.

Plugins call print(), and the build driver calls parse(). This split is the key to writing a correct custom parser.

TIP

For TypeScript and JavaScript output use the built-in @kubb/parser-ts. It is added by default when you import defineConfig from the top-level kubb package. Build a custom parser only when you target a different language, such as Python, Kotlin, or Rust.

Quick start

A minimal parser registers its extensions and concatenates each source:

parserText.ts
typescript
import {  } from '@kubb/core'

export const  = ({
  : 'parser-text',
  : ['.txt'],
  () {
    return .
      .(() => . ?? [])
      .(() => (. === 'Text' ? . : ''))
      .('\n')
  },
  (...) {
    return .().('\n')
  },
})

Wire it into your config:

kubb.config.ts
typescript
import {  } from 'kubb'
import { ,  } from '@kubb/parser-ts'
import {  } from './parserText.ts'
Cannot find module './parserText.ts' or its corresponding type declarations.
export default ({ : { : './petStore.yaml' }, : { : './src/gen' }, : [, , ], })

Anatomy

Every value returned from defineParser matches the Parser interface from @kubb/core:

Property Type Required When called Purpose
name string Yes Unique parser identifier. Convention is parser-<id>.
extNames Array<FileNode['extname']> | undefined Yes File extensions this parser handles. Set to undefined to register a catch-all fallback.
parse (file: FileNode, options?: { extname?: FileNode['extname'] }) => string Yes By FileProcessor after all plugins have run Serializes the file's staged sources into the final output string. Must return synchronously.
print (...nodes: TNode[]) => string Yes By plugins, before files are staged Renders compiler AST nodes to source text. The node type is parser-specific, for example ts.Node for parserTs.

IMPORTANT

If two parsers register the same extension, the first one in the parsers array wins. Order matters.

NOTE

parse() is synchronous. FileProcessor runs files through a synchronous generator, so returning a Promise is not supported. Do async work before the file reaches the parser and pass the result through FileNode.

NOTE

Formatting and linting (Prettier, Biome, oxlint) run after parse(). Keep parse() focused on producing syntactically valid output.

When no parser matches a file's extension, FileProcessor falls back to joining the file's source strings directly.

Streaming

FileProcessor processes files one at a time through a synchronous generator. Call run() to start the full lifecycle. It emits a start event, streams every file through parse(), emits an update event per file, and finishes with an end event.

progress.ts
typescript
import { FileProcessor } from '@kubb/core'

const processor = new FileProcessor()

processor.events.on('start', (files) => {
  console.log(`processing ${files.length} files`)
})

processor.events.on('update', ({ file, processed, total }) => {
  console.log(`[${processed}/${total}] ${file.path}`)
})

processor.events.on('end', (files) => {
  console.log(`done — ${files.length} files written`)
})

The underlying stream() method is a Generator<ParsedFile> that yields { file, source, processed, total, percentage } for each file. Memory stays flat regardless of build size because the generator pulls one file at a time and never buffers the full list.

Naming convention

Parsers follow the same layout as plugins and adapters:

Surface Pattern Example
npm package @<scope>/parser-<name> or kubb-parser-<name> @kubb/parser-ts
Parser runtime name The output language or format (lowercase) 'typescript', 'markdown'
Factory export parser<Name> (camelCase) parserTs, parserMd

Parsers export a plain Parser object, not a factory function. Pass them directly to parsers: in defineConfig:

naming.ts
typescript
import {  } from '@kubb/core'

export const  = ({
  : 'custom',
  : ['.custom'],
  () {
    return ..(() => . ?? '').('\n')
  },
  (...) {
    return .().('\n')
  },
})

TIP

Parsers compose by extension. parserTs (.ts, .js) and parserTsx (.tsx, .jsx) ship in the same @kubb/parser-ts package and are registered side by side.

Built-in parsers

@kubb/parser-ts

The default parser for TypeScript and JavaScript output. It uses the official TypeScript compiler to resolve import paths, deduplicate declarations, print JSDoc, and rewrite extensions based on output.extension. See the @kubb/parser-ts reference for the full option list.

shell
bun add -d @kubb/parser-ts
shell
pnpm add -D @kubb/parser-ts
shell
npm install --save-dev @kubb/parser-ts
shell
yarn add -D @kubb/parser-ts
Export Extensions handled Notes
parserTs .ts, .js TypeScript and plain JavaScript output.
parserTsx .tsx, .jsx Same as parserTs with JSX support.

Both expose parse(file, options?) and print(...nodes: ts.Node[]). Call parserTs.print(node) from a plugin to render a TypeScript compiler node to its source string before staging it on FileNode.sources.

kubb.config.ts
typescript
import {  } from 'kubb'
import { ,  } from '@kubb/parser-ts'

export default ({
  : { : './petStore.yaml' },
  : { : './src/gen' },
  : [, ],
})

TIP

defineConfig from the top-level kubb package installs parserTs and parserTsx automatically. Set parsers: explicitly only when you add a custom parser or need to change the registration order.

Creating a custom parser

Use defineParser from @kubb/core. It is an identity wrapper that infers the parser type. It returns the object you pass in unchanged, with no per-build options:

parserPython.ts
typescript
import {  } from '@kubb/core'

export const  = ({
  : 'parser-python',
  : ['.py', '.pyi'],
  () {
    const : <string> = []

    if (.) {
      .(.)
    }

    for (const  of .) {
      for (const  of . ?? []) {
        if (. === 'Text') {
          .(.)
        }
      }
    }

    if (.) {
      .(.)
    }

    return .('\n')
  },
  (...) {
    return .().('\n')
  },
})

Register it alongside the built-ins:

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from '@kubb/parser-ts'
import {  } from './parserPython.ts'
Cannot find module './parserPython.ts' or its corresponding type declarations.
export default ({ : { : './petStore.yaml' }, : { : './src/gen' }, : [, ], })

TIP

Set extNames: undefined to register a catch-all fallback that runs when no other parser matches. Useful for a default .txt writer or for inspecting what files the build produces.

Examples

Parser that joins pre-formatted sources

parse() runs synchronously, so external formatting (a service call, a child process, or a worker thread) must finish before the file reaches the parser. Stage the pre-formatted output on FileNode.sources[].nodes inside a generator, then let the parser join it verbatim:

parserFormatted.ts
typescript
import {  } from '@kubb/core'

export const  = ({
  : 'parser-formatted',
  : ['.ts'],
  () {
    return .
      .(() => . ?? [])
      .(() => (. === 'Text' ? . : ''))
      .('\n')
  },
  (...) {
    return .().('\n')
  },
})