npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nds-stack/bun-csv

v0.1.0-alpha.0

Published

Bun-native CSV parser and generator — zero dependencies, streaming via Bun.file()

Readme

@nds-stack/bun-csv

Bun-native CSV parser and generator — zero dependencies, streaming via Bun.file().

npm version Bun TypeScript License


How It Works

bun-csv uses a single-pass, character-by-character state machine parser that processes CSV without intermediate allocations. The parser tracks field boundaries, quoted sections, and escaped characters in one pass — no regex backtracking, no array-of-arrays overhead.

For streaming, parseStream() uses Bun.file().stream() to read chunks via ReadableStream, decodes bytes to text progressively, and yields parsed row batches as they arrive.

The stringifier builds rows by scanning fields for characters that require quoting (delimiter, quote, newline) and applies quote escaping — then joins fields with the delimiter in a single pass.

Installation

bun add @nds-stack/bun-csv

API

parse(csv: string, options?: ParseOptions): ParseResult

Parses a CSV string into structured data.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | csv | string | — | CSV content | | options.delimiter | string | "," | Field delimiter | | options.quoteChar | string | '"' | Quote character | | options.escapeChar | string | '"' | Escape character (doubled) | | options.hasHeader | boolean | true | First row is header | | options.skipEmptyLines | boolean | true | Skip blank lines | | options.trim | boolean | false | Trim whitespace from fields | | options.cast | boolean | false | Auto-cast to number/boolean |

Returns: ParseResult<T>

interface ParseResult<T = Record<string, string>> {
  data: T[]
  headers: string[]
  rows: number
}

stringify(data: Record<string, unknown>[], options?: StringifyOptions): string

Converts an array of objects into a CSV string.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | data | Record<string, unknown>[] | — | Array of objects | | options.delimiter | string | "," | Field delimiter | | options.quoteChar | string | '"' | Quote character | | options.escapeChar | string | '"' | Escape character (doubled) | | options.header | boolean | true | Include header row | | options.trailingNewline | boolean | true | Append trailing newline |

parseStream(file: BunFile, options?: ParseOptions): AsyncGenerator<StreamChunk<T>>

Streams a CSV file using Bun.file().stream(), yielding parsed row chunks.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | file | BunFile | — | A BunFile instance | | options.batchSize | number | 100 | Rows per yielded chunk |

Yields: StreamChunk<T>

interface StreamChunk<T = Record<string, string>> {
  data: T[]
  chunkIndex: number
  byteOffset: number
}

Error Handling

| Scenario | Behavior | |----------|----------| | Malformed quotes | Parser continues; unclosed quotes consume rest of field | | Empty input | Returns { data: [], headers: [], rows: 0 } | | Mismatched column count | Missing fields become empty string; extra fields ignored | | Empty data array for stringify | Returns empty string "" | | Missing keys in row | Treated as empty string |

No exceptions thrown for formatting issues — the parser is lenient by design, similar to papaparse.

Limitations

  • No streaming writestringify() is synchronous, returns full string. For large datasets, consider chunked writes.
  • No type schema — Unlike csv-parse with cast functions, bun-csv provides simple auto-cast only (cast: true). For complex type coercion, post-process the result.
  • No web worker — CSV parsing is synchronous. For very large files (>100MB), use parseStream() to process incrementally.
  • No auto-delimiter detection — You must specify the delimiter explicitly if not comma.
  • No comment lines — Lines starting with # are not skipped.

Multi-Instance / Cross-Boundary

Since bun-csv is a stateless parser (no internal state), it is safe to use across multiple instances, workers, or processes:

// Worker 1
const result1 = parse(csv1);

// Worker 2 (same import)
const result2 = parse(csv2);

// Streaming in separate processes
for await (const chunk of parseStream(file)) { ... }

parseStream() creates a fresh ReadableStream reader per call, so concurrent streams on the same file work independently.

Customization Guide

Custom Delimiter (TSV)

import { parse, stringify } from "@nds-stack/bun-csv";

const tsv = parse(data, { delimiter: "\t" });
const output = stringify(tsv.data as Record<string, unknown>[], { delimiter: "\t" });

Headerless Mode

const result = parse(csv, { hasHeader: false });
// headers: ["column0", "column1", ...]

Auto-Type Casting

const result = parse(csv, { cast: true });
// "30" → 30, "true" → true, "false" → false

Streaming Large File

import { parseStream } from "@nds-stack/bun-csv";

const file = Bun.file("./massive.csv");
for await (const chunk of parseStream(file)) {
  await processBatch(chunk.data);
}

Extending Parser

import { parse } from "@nds-stack/bun-csv";

function parseWithValidation(csv: string) {
  const result = parse(csv);
  if (result.rows === 0) throw new Error("Empty CSV");
  return result;
}

Comparison Table

| Feature | bun-csv | csv-parse | papaparse | |---------|---------|-----------|-----------| | Dependencies | 0 | 8 (sub-deps) | 0 | | Bun-native | Yes | No (Node.js) | No (browser) | | Streaming | Yes (Bun.file()) | Yes (Node stream) | Yes (File API) | | Auto-cast | Yes | Yes (via cast fn) | Yes (dynamic) | | TypeScript | Strict | Loose | Loose | | Bundle size | ~3 KB | ~45 KB | ~30 KB | | Tree-shakeable | Yes | Partial | No |

Benchmarks

Results on Bun 1.3.14 (Windows), 500 iterations, 100 rows CSV with 9 columns:

  Library                                          Throughput      vs Base
  ------------------------------------------------------------------------
  @nds-stack/bun-csv (parse)                        8,120 ops/s    +534.4%
  csv-parse/sync                                    1,280 ops/s       0.0%
  papaparse                                         3,654 ops/s    +185.5%
  @nds-stack/bun-csv (stringify)                    9,438 ops/s    +637.3%

bun-csv parse is 5.3× faster than csv-parse and 2.2× faster than papaparse.

Results vary by machine. Run bun run bench in the module directory.

Real-World Example

import { parse, stringify, parseStream } from "@nds-stack/bun-csv";
import { write } from "bun";

// Parse downloaded CSV
const file = Bun.file("./users.csv");
const text = await file.text();
const users = parse(text, { cast: true });

// Filter + transform
const activeUsers = users.data.filter(u => u.active === true);
const transformed = activeUsers.map(u => ({
  name: u.name,
  email: u.email,
  age: u.age,
}));

// Write back as CSV
const output = stringify(transformed);
await write("./active-users.csv", output);

// Or stream directly from URL
const response = await fetch("https://example.com/report.csv");
const text = await response.text();
const data = parse(text);
console.log(`Parsed ${data.rows} rows`);

License: MIT