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 onFileNode.sources.parse(file)is called byFileProcessorafter 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:
import { } from '@kubb/core'
export const = ({
: 'parser-text',
: ['.txt'],
() {
return .
.(() => . ?? [])
.(() => (. === 'Text' ? . : ''))
.('\n')
},
(...) {
return .().('\n')
},
})Wire it into your config:
import { } from 'kubb'
import { , } from '@kubb/parser-ts'
import { } from './parserText.ts'
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.
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:
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.
bun add -d @kubb/parser-tspnpm add -D @kubb/parser-tsnpm install --save-dev @kubb/parser-tsyarn 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.
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:
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:
import { } from 'kubb'
import { } from '@kubb/parser-ts'
import { } from './parserPython.ts'
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:
import { } from '@kubb/core'
export const = ({
: 'parser-formatted',
: ['.ts'],
() {
return .
.(() => . ?? [])
.(() => (. === 'Text' ? . : ''))
.('\n')
},
(...) {
return .().('\n')
},
})