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

datareader-csv

v0.2.0

Published

Correct, zero-dependency CSV/TSV/delimited-text reader for the browser and Node.

Readme

datareader-csv

Correct, zero-dependency CSV/TSV/delimited-text reader for the browser and Node.

datareader-csv parses CSV, TSV, and any single-delimiter text into a typed, row-major grid of plain JavaScript values: strings, coerced finite numbers, and null for blank cells. It is written in pure TypeScript against a Web platform API (TextDecoder) — no runtime dependencies, it is fully synchronous, and the same build runs in the browser and in Node.

Why

The ecosystem has no shortage of CSV parsers, but most drag in a dependency, hand you a streaming/async API you don't need for an in-memory file, or return untyped strings you then re-coerce by hand. Real-world exports also break the naive split(",") approach: quoted fields with embedded commas and newlines, "" escapes, \r\n/\r line endings, a leading BOM, and the semicolon or tab delimiters that European Excel, SmartPLS, and Qualtrics emit.

datareader-csv was built to read those files correctly with no fuss: a single-pass RFC-4180 tokenizer, a delimiter sniff over ,/;/\t, and opt-out numeric typing — all synchronous, all in one dependency-free ESM module that behaves identically in a browser upload flow or a Node script. The tokenizer and delimiter sniff are battle-tested on real Qualtrics and SmartPLS exports.

Install

npm i datareader-csv
bun add datareader-csv
pnpm add datareader-csv
yarn add datareader-csv

Usage

Browser — a picked/dropped File

import { readCsv } from "datareader-csv";

const input =
  document.querySelector<HTMLInputElement>("#file");

input.addEventListener("change", async () => {
  const file = input.files?.[0];
  if (!file) return;

  // File.text() is async; readCsv itself is sync.
  const { header, rows } = readCsv(await file.text(), {
    header: true,
  });

  console.log(header); // column names
  console.log(rows.length); // row count
  console.log(rows[0]); // first data row
});

React — a file input

import { useState } from "react";
import { readCsv, type CellValue } from "datareader-csv";

export function CsvImporter() {
  const [header, setHeader] = useState<string[]>([]);
  const [rows, setRows] = useState<CellValue[][]>([]);

  async function onFile(
    e: React.ChangeEvent<HTMLInputElement>,
  ) {
    const file = e.target.files?.[0];
    if (!file) return;
    const parsed = readCsv(await file.text(), {
      header: true,
    });
    setHeader(parsed.header ?? []);
    setRows(parsed.rows);
  }

  return (
    <>
      <input
        type="file"
        accept=".csv,.tsv,.txt"
        onChange={onFile}
      />
      <p>
        {rows.length} rows × {header.length} columns
      </p>
    </>
  );
}

Node — a file on disk

import { readFile } from "node:fs/promises";
import { readCsv } from "datareader-csv";

// readFile returns a Buffer (a Uint8Array), which
// readCsv decodes as UTF-8 directly — no .toString().
const { header, rows } = readCsv(
  await readFile("data.csv"),
  { header: true },
);

console.log(header);
console.log(`${rows.length} data rows`);

Typed vs. raw

import { readCsv } from "datareader-csv";

const csv = "id,score\n007,12.5\n,\n";

// Default: numbers are coerced, blanks become null.
readCsv(csv).rows;
// => [["id", "score"], [7, 12.5], [null, null]]

// typed: false keeps every cell a trimmed string.
readCsv(csv, { typed: false }).rows;
// => [["id", "score"], ["007", "12.5"], ["", ""]]

API

readCsv(input, opts?)

function readCsv(
  input: string | ArrayBuffer | Uint8Array,
  opts?: CsvOptions,
): CsvResult;

Reads delimited text end-to-end — decode → sniff delimiter → RFC-4180 tokenize → optionally coerce cells and split off a header — and returns a CsvResult. Synchronous: no Promise, no await on the call itself. Throws a CsvError when the parsed grid exceeds maxCells (see Security & limits).

CsvOptions

type CsvOptions = {
  // Force the delimiter instead of sniffing it
  // (e.g. ",", ";", "\t", "|").
  delimiter?: string;
  // Treat row 0 as a header: return it as `header`
  // and drop it from `rows`. Default false.
  header?: boolean;
  // Coerce numbers and map blank cells to null.
  // false keeps every cell a trimmed string.
  // Default true.
  typed?: boolean;
  // Trim whitespace around each cell. Default true.
  trim?: boolean;
  // Reject inputs whose grid (rows × columns)
  // exceeds this many cells. Default 5,000,000.
  maxCells?: number;
};

CsvResult

type CsvResult = {
  // The delimiter used — the sniffed one, or
  // opts.delimiter when provided.
  delimiter: string;
  // The header row, present only when
  // opts.header was true.
  header?: string[];
  // Row-major cells. Excludes the header row when
  // opts.header was true.
  rows: CellValue[][];
};

// A parsed cell: a string, a coerced finite number,
// or null (a blank cell).
type CellValue = string | number | null;

CsvError

class CsvError extends Error {}

Thrown when a resource bound rejects an oversized input (a bad file, not a reader bug). Because it extends Error, a plain catch still catches it; check err instanceof CsvError to tell a rejected input apart from an unexpected bug.

DEFAULT_LIMITS

const DEFAULT_LIMITS: { maxCells: number } = {
  maxCells: 5_000_000, // rows × columns ceiling
};

The default ceiling readCsv uses when you pass no maxCells. No real spreadsheet is affected; pass a stricter value where memory is tight.

Type inference

With typed true (the default), each raw cell is coerced by one rule:

  • A blank cell (empty after trimming) → null.
  • A cell that parses to a finite number → that number.
  • Everything else stays the string.

Blank is tested first because Number("") is 0. The number test uses JavaScript's Number(), so any numeric-looking string coerces — including ones you may want to keep as text:

readCsv("code\n007").rows; // => [["code"], [7]]
readCsv("v\n1e3").rows; // => [["v"], [1000]]
readCsv("v\n0x10").rows; // => [["v"], [16]]

Zip codes, product codes, and leading-zero IDs lose their form ("007"7). When you need the exact text, pass typed: false — every cell stays a trimmed string and nothing is coerced:

readCsv("code\n007", { typed: false }).rows;
// => [["code"], ["007"]]

Format coverage

| Area | Handled | | ------------ | ------------------------------------------------------------------------------------ | | Delimiters | , ; \t sniffed quote-awarely — a sample is tokenized under each candidate and the one yielding the most cells wins, so delimiters inside quoted fields don't miscount (comma breaks ties); any single character via delimiter | | Quoting | "…" fields with embedded delimiters and newlines; "" escapes a literal quote | | Line endings | \r\n, \r, and \n; a trailing newline adds no empty row | | BOM | a single leading UTF-8 BOM (U+FEFF) is stripped | | Encoding | UTF-8, plus UTF-16 (LE/BE) selected by BOM (ArrayBuffer/Uint8Array in). Bytes that aren't valid UTF-8/UTF-16 throw a CsvError — never a silent U+FFFD | | Whitespace | each unquoted cell trimmed by default (trim: false keeps it); a quoted field's content is significant and never trimmed |

Security & limits

datareader-csv bounds the one attacker-controllable allocation — the parsed grid. After tokenizing, it rejects any input whose cell count (rows × widest row) exceeds maxCells (default 5,000,000) with a catchable CsvError, so a pathologically wide or tall file can't silently balloon memory in whatever consumes the grid.

import { readCsv, CsvError } from "datareader-csv";

try {
  const { rows } = readCsv(text, { maxCells: 200_000 });
} catch (err) {
  if (err instanceof CsvError) {
    // input too large — reject it
  }
}

Recommendations for consumers:

  • Still bound the input size before you hand text/bytes to readCsv — reject files larger than you expect. maxCells caps the parsed grid, not the raw byte length the tokenizer reads.
  • Treat header strings (and any file-derived cell) as untrusted — don't use a header label as a plain-object key without care; prefer a Map or a null-prototype object to avoid prototype-pollution surprises from adversarial names.

Roadmap

datareader-csv reads delimited text correctly today and is intentionally read-only (no writer). On the map for future releases:

  • More encodings — UTF-8 and UTF-16 are handled today (UTF-16 by BOM); a declared or content-detected single-byte fallback (e.g. windows-1252) via TextDecoder is a natural extension — for now such files fail loud with a CsvError rather than corrupting silently.
  • Streaming — a chunked API for files too large to hold in memory, alongside today's whole-string read.

Issues and contributions are welcome.

License

MIT © 2026 everydaydesign