Repository: alexcaza/export-to-csv
Branch: main
Commit: b838401fd8ae
Files: 28
Total size: 62.0 KB
Directory structure:
gitextract_xjlj126q/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ ├── pr.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc.json
├── .weztermocil/
│ └── watch.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bun.lockb
├── flake.nix
├── index.ts
├── integration/
│ ├── index.html
│ └── integration.spec.ts
├── lib/
│ ├── __specs__/
│ │ ├── config.spec.ts
│ │ ├── helpers.spec.ts
│ │ └── main.spec.ts
│ ├── config.ts
│ ├── errors.ts
│ ├── generator.ts
│ ├── helpers.ts
│ ├── index.ts
│ └── types.ts
├── package.json
├── playwright.config.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: "bug"
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is. If possible, include the data you're trying to export as a CSV (with any sensitive data removed; reduced test cases are welcomed).
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Package versions:**
- Typescript: [5.0]
- export-to-csv: [1.2.4]
- runtime: [e.g. node 20 or bun 1.1]
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
================================================
FILE: .github/workflows/pr.yml
================================================
name: PR
on:
pull_request:
branches: [master, main]
jobs:
build:
timeout-minutes: 5
name: Build project
# The type of runner that the job will run on
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run build step
uses: oven-sh/setup-bun@v2.0.1
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run build
tests:
timeout-minutes: 60
name: Unit & E2E Tests
# The type of runner that the job will run on
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run unit and e2e tests
uses: oven-sh/setup-bun@v2.0.1
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run test
- run: bun run e2e-ci
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
format:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run prettier to check formatting of files
uses: oven-sh/setup-bun@v2.0.1
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bunx prettier -c "."
================================================
FILE: .github/workflows/release.yml
================================================
name: Publish Package to npmjs
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- uses: oven-sh/setup-bun@v2.0.1
with:
bun-version: "latest"
registry-url: "https://registry.npmjs.org"
- run: bun install --frozen-lockfile
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
node_modules
npm-debug.log
typings
Thumbs.db
.DS_Store
*.js
!karma.conf.js
!rollup.config.js
!babel.config.js
!jest.config.js
*.map
*.d.ts
.idea/
*.iml
test/
coverage/
build/
output/
/test-results/
/playwright-report/
/playwright/.cache/
================================================
FILE: .npmignore
================================================
# Node generated files
node_modules
npm-debug.log
# OS generated files
Thumbs.db
.DS_Store
# Git
.git
.github
# TypeScript source files, apart generated type definitions
*.ts
!*.d.ts
# Other generated files
*.spec.*
*.map
integration
lib/**/*.d.ts
build
*.lock
bun.lockb
bunfig.toml
# Editor files
.prettierrc.json
.todo
# Other build-related files
.travis.yml
karma.*
tsconfig.*
*.config.*
playwright*
test-results
coverage
# nix files
*.nix
================================================
FILE: .npmrc
================================================
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
registry=https://registry.npmjs.org/
always-auth=true
================================================
FILE: .prettierrc.json
================================================
{}
================================================
FILE: .weztermocil/watch.yml
================================================
windows:
- name: export-to-csv-watch
root: .
layout: tiled
panes:
- bun run test -- --watch
- bun run watch
- bun run tsc --watch
================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute to `export-to-csv`
Thank you for wanting to make `export-to-csv` better in some way!
## Bugs, questions and feature requests
### Bugs
If you've encountered an issue with the library and it doesn't work as outlined in the [README.md](README.md), please open an Issue and follow the template.
### Questions and feature requests
Not sure how the library works or have a suggestion to make the library better? Please start a Discussion on GitHub and add your question or ideas for features there.
Don't shy away from details, either! The better thought out your Discussion, the more likely we all are to have a better time at working together.
## Submitting a pull request
This repository uses [bun](https://bun.sh/) to build and run unit tests, and [playwright](https://playwright.dev/) for end-to-end testing.
We also do our best to keep things relatively type-safe.
If you're fixing a bug, ensure the project still builds, and all the tests still run after making your changes by running:
```bash
bun run build && bun run test && bun run e2e
```
If you're adding a new feature, please add new tests to cover the feature's functionality. Most new features can probably be covered by adding a new test to `main.spec.ts`. One for the CSV output, and one for the TXT output.
If your new feature requires a new helper function in `helpers.ts`, add a test to cover the helper functionality as well.
Once your Pull Request is ready to be reviewed, it will go through a few automated checks to ensure tests pass and that the formatting is correct. Your Pull Request will only be merged once it's been reviewed, approved and the checks pass.
## Conventions
This project uses [prettier](https://prettier.io/) for formatting source code. Upon opening a Pull Request, a check will be done to ensure consistent formatting with the repository's rules in `.prettierrc.json`.
### The format check in the Pull Request is failing, what do I do?
Run `bun run format` from the root of the project, commit any changes to your working branch and push.
================================================
FILE: LICENSE
================================================
MIT License
Copyright for portions of project export-to-csv are held by Javier Telio <jtelio@gmail.com>, 2016 as part of project Angular2Csv.
All other copyright for project export-to-csv are held by Alex Caza <alex@alexcaza.com>, 2017.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# export-to-csv | Export to CSV Mini Library
Like this library and want to support active development?
<a href='https://ko-fi.com/T6T8XO5J5' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi4.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
Small, simple, and single purpose. Zero dependencies, functionally inspired, and fairly well-typed.
If you're looking for a fully CSV-compliant, consistently maintained, whole-package library, I'd recommend looking elsewhere! (see [alternatives](#alternatives) section below)
If you want a lightweight, stable, easy-to-use basic CSV generation and download library, feel free to install.
## Installation
```javascript
npm install --save export-to-csv
```
## Usage
This library was written with TypeScript in mind, so the examples will be in TS.
You can easily use this library in JavaScript as well. The bundle uses ES modules, which all modern browsers support.
You can also look at the [integration tests](integration/index.html) for browser/JS use, and the [unit tests](lib/__specs__) to understand how the library functions.
### In-browser
```typescript
import { mkConfig, generateCsv, download } from "export-to-csv";
// mkConfig merges your options with the defaults
// and returns WithDefaults<ConfigOptions>
const csvConfig = mkConfig({ useKeysAsHeaders: true });
const mockData = [
{
name: "Rouky",
date: "2023-09-01",
percentage: 0.4,
quoted: '"Pickles"',
},
{
name: "Keiko",
date: "2023-09-01",
percentage: 0.9,
quoted: '"Cactus"',
},
];
// Converts your Array<Object> to a CsvOutput string based on the configs
const csv = generateCsv(csvConfig)(mockData);
// Get the button in your HTML
const csvBtn = document.querySelector("#csv");
// Add a click handler that will run the `download` function.
// `download` takes `csvConfig` and the generated `CsvOutput`
// from `generateCsv`.
csvBtn.addEventListener("click", () => download(csvConfig)(csv));
```
### Node.js
```typescript
import { mkConfig, generateCsv, asString } from "export-to-csv";
import { writeFile } from "node:fs";
import { Buffer } from "node:buffer";
// mkConfig merges your options with the defaults
// and returns WithDefaults<ConfigOptions>
const csvConfig = mkConfig({ useKeysAsHeaders: true });
const mockData = [
{
name: "Rouky",
date: "2023-09-01",
percentage: 0.4,
quoted: '"Pickles"',
},
{
name: "Keiko",
date: "2023-09-01",
percentage: 0.9,
quoted: '"Cactus"',
},
];
// Converts your Array<Object> to a CsvOutput string based on the configs
const csv = generateCsv(csvConfig)(mockData);
const filename = `${csvConfig.filename}.csv`;
const csvBuffer = new Uint8Array(Buffer.from(asString(csv)));
// Write the csv file to disk
writeFile(filename, csvBuffer, (err) => {
if (err) throw err;
console.log("file saved: ", filename);
});
```
### Using `generateCsv` output as a `string`
**Note: this is only applicable to projects using Typescript. If you're using this library with Javascript, you might not run into this issue.**
There might be instances where you want to use the result from `generateCsv` as a `string` instead of a `CsvOutput` type. To do that, you can use `asString`, which is exported from this library.
```typescript
import { mkConfig, generateCsv, asString } from "export-to-csv";
const csvConfig = mkConfig({ useKeysAsHeaders: true });
const addNewLine = (s: string): string => s + "\n";
const mockData = [
{
name: "Rouky",
date: "2023-09-01",
percentage: 0.4,
quoted: '"Pickles"',
},
{
name: "Keiko",
date: "2023-09-01",
percentage: 0.9,
quoted: '"Cactus"',
},
];
// Converts your Array<Object> to a CsvOutput string based on the configs
const csvOutput = generateCsv(csvConfig)(mockData);
// This would result in a type error
// const csvOutputWithNewLine = addNewLine(csvOutput);
// ❌ => CsvOutput is not assignable to type string.
// This unpacks CsvOutput which turns it into a string before use
const csvOutputWithNewLine = addNewLine(asString(csvOutput));
```
The reason the `CsvOutput` type exists is to prevent accidentally passing in a string which wasn't formatted by `generateCsv` to the `download` function.
### Using `generateCsv` output as a `Blob`
A case for this would be using browser extension download methods _instead of_ the supplied `download` function. There may be scenarios where using a `Blob` might be more ergonomic.
```typescript
import { mkConfig, generateCsv, asBlob } from "export-to-csv";
// mkConfig merges your options with the defaults
// and returns WithDefaults<ConfigOptions>
const csvConfig = mkConfig({ useKeysAsHeaders: true });
const mockData = [
{
name: "Rouky",
date: "2023-09-01",
percentage: 0.4,
quoted: '"Pickles"',
},
{
name: "Keiko",
date: "2023-09-01",
percentage: 0.9,
quoted: '"Cactus"',
},
];
// Converts your Array<Object> to a CsvOutput string based on the configs
const csv = generateCsv(csvConfig)(mockData);
// Generate the Blob from the CsvOutput
const blob = asBlob(csvConfig)(csv);
// Requires URL to be available (web workers or client scripts only)
const url = URL.createObjectURL(blob);
// Assuming there's a button with an id of csv in the DOM
const csvBtn = document.querySelector("#csv");
csvBtn.addEventListener("click", () => {
// Use Chrome's downloads API for extensions
chrome.downloads.download({
url,
body: csv,
filename: "chrome-extension-output.csv",
});
});
```
## API
| Option | Default | Type | Description |
| ------------------- | -------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fieldSeparator` | `","` | `string` | Defines the field separator character |
| `filename` | `"generated"` | `string` | Sets the name of the file created from the `download` function |
| `quoteStrings` | `false` | `boolean` | Determines whether or not to quote strings (using `quoteCharacter`'s value). Whether or not this is set, `\r`, `\n`, and `fieldSeparator` will be quoted. |
| `quoteCharacter` | `'"'` | `string` | Sets the quote character to use. |
| `decimalSeparator` | `"."` | `string` | Defines the decimal separator character (default is .). If set to "locale", it uses the [language-sensitive representation of the number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString). |
| `showTitle` | `false` | `boolean` | Sets whether or not to add the value of `title` to the start of the CSV. (This is not supported by all CSV readers) |
| `title` | `"My Generated Report"` | `string` | The title to display as the first line of the CSV file. (This **is not** the name of the file [see `filename`]) |
| `showColumnHeaders` | `true` | `boolean` | Determines if columns should have headers. When set to `false`, the first row of the CSV will be data. |
| `columnHeaders` | `[]` | `Array<string \| {key: string, displayLabel: string}>` | **Use this option if column/header order is important!** Determines the headers to use as the first line of the CSV data. If the item is a `string`, it will be used for lookup in your collection AND as the header label. If the item is an object, `key` will be used for lookup, and `displayLabel` will be used as the header label. |
| `useKeysAsHeaders` | `false` | `boolean` | If set, the CSV will use the key names in your collection as headers. **Warning: `headers` recommended for large collections. If set, it'll override the `headers` option. Column/header order also not guaranteed. Use `headers` only if order is important!** |
| `boolDisplay` | `{true: "TRUE", false: "FALSE"}` | `{true: string, false: string}` | Determines how to display boolean values in the CSV. **This only works for `true` and `false`. `1` and `0` will not be coerced and will display as `1` and `0`.** |
| `useBom` | `true` | `boolean` | Adds a [byte order mark](https://en.wikipedia.org/wiki/Byte_order_mark) which is required by Excel to display CSVs, despite it not being necessary with UTF-8 🤷♂️ |
| `useTextFile` | `false` | `boolean` | _Deprecation warning. This will be removed in the next major version._ Will download the file as `text/plain` instead of `text/csv` and use a `.txt` vs `.csv` file extension. |
| `fileExtension` | `csv` | `string` | File extension to use. Currently, this only applies if `useTextFile` is `false`. |
# Alternatives
As mentioned above, this library is intentionally small and was designed to solve a very simple need. It **was not** originally designed to be fully CSV compliant, so many things you need _might_ be missing. I'm also not the most active on it (~7 year gap between updates). So, here are some alternatives with more support and that might be more fully featured.
- https://csv.js.org/
- https://www.papaparse.com/
# Thanks!
This library was originally based on [this library](https://github.com/javiertelioz/angular2-csv) by Javier Telio
| Credits and Original Authors |
| :-------------------------------------------------- |
| **[javiertelioz](https://github.com/javiertelioz)** |
| **[sn123](https://github.com/sn123)** |
| **[arf1980](https://github.com/arf1980)** |
================================================
FILE: flake.nix
================================================
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
(system:
let pkgs = nixpkgs.legacyPackages.${system}; in
{
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
bun
playwright-driver.browsers
];
shellHook = ''
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
'';
};
}
);
}
================================================
FILE: index.ts
================================================
export * from "./lib/index.ts";
================================================
FILE: integration/index.html
================================================
<html>
<head>
<script type="module">
import { mkConfig, generateCsv, download } from "/export-to-csv.js";
// Configs
const csvConfig = mkConfig({ useKeysAsHeaders: true });
const txtConfig = mkConfig({
useKeysAsHeaders: true,
useTextFile: true,
});
const tsvConfig = mkConfig({
useKeysAsHeaders: true,
useTextFile: true,
delimiter: "\t",
});
const csvConfigCustomFilename = mkConfig({
useKeysAsHeaders: true,
filename: "Best CSV",
});
const txtConfigCustomFilename = mkConfig({
useKeysAsHeaders: true,
useTextFile: true,
filename: "Best CSV as Text",
});
const tsvConfigCustomFilename = mkConfig({
useKeysAsHeaders: true,
fileExtension: "tsv",
delimiter: "\t",
filename: "Best CSV as TSV",
});
const mockData = [
{
name: "Rouky",
date: "2023-09-01",
percentage: 0.4,
quoted: '"Pickles"',
nullish: null,
"string with spaces as header": "value with spaces",
},
{
name: "Keiko",
date: "2023-09-01",
percentage: 0.9,
quoted: '"Cactus"',
nullish: "test",
"string with spaces as header": "more spaces",
},
];
const csv = generateCsv(csvConfig)(mockData);
const txt = generateCsv(txtConfig)(mockData);
const tsv = generateCsv(tsvConfig)(mockData);
const csvCustomFilename = generateCsv(csvConfig)(mockData);
const txtCustomFilename = generateCsv(txtConfig)(mockData);
const tsvCustomFilename = generateCsv(tsvConfig)(mockData);
const csvBtn = document.querySelector("#csv");
const txtBtn = document.querySelector("#txt");
const tsvBtn = document.querySelector("#tsv");
const csvBtnCustom = document.querySelector("#csv-custom");
const txtBtnCustom = document.querySelector("#txt-custom");
const tsvBtnCustom = document.querySelector("#tsv-custom");
csvBtn.addEventListener("click", () => download(csvConfig)(csv));
txtBtn.addEventListener("click", () => download(txtConfig)(txt));
tsvBtn.addEventListener("click", () => download(tsvConfig)(tsv));
csvBtnCustom.addEventListener("click", () =>
download(csvConfigCustomFilename)(csvCustomFilename),
);
txtBtnCustom.addEventListener("click", () =>
download(txtConfigCustomFilename)(txtCustomFilename),
);
tsvBtnCustom.addEventListener("click", () =>
download(tsvConfigCustomFilename)(tsvCustomFilename),
);
</script>
</head>
<body>
<h1>Export to CSV testing</h1>
<button id="csv">Download as CSV</button>
<button id="txt">Download as TXT</button>
<button id="tsv">Download as TSV</button>
<button id="csv-custom">Download as CSV (custom filename)</button>
<button id="txt-custom">Download as TXT (custom filename)</button>
<button id="tsv-custom">
Download as TSV (custom filename and extension)
</button>
</body>
</html>
================================================
FILE: integration/integration.spec.ts
================================================
import { test, expect } from "@playwright/test";
test("download csv file (default filename)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#csv").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("generated.csv");
});
test("download csv file (custom filename)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#csv-custom").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("Best CSV.csv");
});
test("download txt file (default filename)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#txt").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("generated.txt");
});
test("download txt file (custom filename)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#txt-custom").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("Best CSV as Text.txt");
});
test("download tsv file (default filename)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#tsv").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("generated.txt");
});
test("download tsv file (custom filename and extension)", async ({ page }) => {
await page.goto("http://127.0.0.1:3000");
const [download] = await Promise.all([
// Start waiting for the download
page.waitForEvent("download"),
// Perform the action that initiates download
page.locator("button#tsv-custom").click(),
]);
// assert filename
expect(download.suggestedFilename()).toBe("Best CSV as TSV.tsv");
});
================================================
FILE: lib/__specs__/config.spec.ts
================================================
import { describe, expect, it } from "bun:test";
import { defaults, mkConfig } from "../config.ts";
describe("mkConfig", () => {
it("should properly set defaults when empty", () => {
const config = mkConfig({});
expect(config).toEqual(defaults);
});
it("should properly allow user overrides", () => {
const config = mkConfig({ filename: "test csv" });
const overrides = {
...defaults,
filename: "test csv",
};
expect(config).toEqual(overrides);
});
});
================================================
FILE: lib/__specs__/helpers.spec.ts
================================================
import { describe, expect, it, jest } from "bun:test";
import {
addBOM,
addBody,
addEndOfLine,
addFieldSeparator,
addHeaders,
addTitle,
asString,
buildRow,
formatData,
thread,
} from "../helpers.ts";
import { byteOrderMark, endOfLine, mkConfig } from "../config.ts";
import { mkCsvOutput, mkCsvRow, mkFormattedData, unpack } from "../types.ts";
describe("Helpers", () => {
describe("thread", () => {
it("should call each function", () => {
const val = "";
const one = jest.fn();
const two = jest.fn();
const three = jest.fn();
thread(val, one, two, three);
expect(one).toHaveBeenCalled();
expect(two).toHaveBeenCalled();
expect(three).toHaveBeenCalled();
});
it("should call each function with initial value", () => {
const val = "test";
const one = jest.fn((x) => x);
const two = jest.fn((x) => x);
const three = jest.fn((x) => x);
thread(val, one, two, three);
expect(one.mock.results[0].value).toBe(val);
expect(two.mock.results[0].value).toBe(val);
expect(three.mock.results[0].value).toBe(val);
});
});
describe("addBom", () => {
it("should add a byte order mark to input if set to true", () => {
const config = mkConfig({ useBom: true });
const input = mkCsvOutput("test");
const output = asString(addBOM(config)(input));
expect(output).toBe(input + byteOrderMark);
});
it("should return input unchanged if set to false", () => {
const config = mkConfig({ useBom: false });
const input = mkCsvOutput("test");
const output = addBOM(config)(input);
expect(output).toBe(input);
});
});
describe("addTitle", () => {
it("should add title if set to true", () => {
const config = mkConfig({ showTitle: true, title: "My title" });
const input = mkCsvOutput("");
const output = asString(addTitle(config)(input));
expect(output).toBe("My title\r\n");
});
it("should add use default if set to true without title given", () => {
const config = mkConfig({ showTitle: true });
const input = mkCsvOutput("");
const output = asString(addTitle(config)(input));
expect(output).toBe("My Generated Report\r\n");
});
it("should skip title if set to false", () => {
const config = mkConfig({ showTitle: false, title: "My title" });
const input = mkCsvOutput("");
const output = asString(addTitle(config)(input));
expect(output).toBe("");
});
});
describe("addEndOfLine", () => {
it("should add new lines to end of row", () => {
const csv = mkCsvOutput("");
const input = mkCsvRow("test,one,two");
const output = asString(addEndOfLine(csv)(input));
expect(output).toBe(unpack(csv) + unpack(input) + endOfLine);
});
});
describe("addFieldSeparator", () => {
it("should add field separator to input with defaults", () => {
const config = mkConfig({});
const input = mkCsvRow("test,one,two");
const output = addFieldSeparator(config)(input);
expect(output).toBe(mkCsvRow(asString(input) + ","));
});
it("should add field separator to input based on config option", () => {
const config = mkConfig({ fieldSeparator: "|" });
const input = mkCsvRow("test|one|two");
const output = addFieldSeparator(config)(input);
expect(output).toBe(mkCsvRow(asString(input) + "|"));
});
});
describe("buildRow", () => {
it("should add item to row", () => {
const config = mkConfig({});
const input = mkCsvRow("test,one,two,");
const output = asString(
buildRow(config)(input, mkFormattedData("house")),
);
expect(output).toBe("test,one,two,house,");
});
});
describe("addHeaders", () => {
describe("throw error if showColumnHeaders true but headers empty", () => {
it("should throw when useKeysAsHeaders is true", () => {
const config = mkConfig({
showColumnHeaders: true,
useKeysAsHeaders: true,
});
expect(() => addHeaders(config, [])(mkCsvOutput(""))).toThrow();
});
it("should throw when headers supplied but empty", () => {
const config = mkConfig({
showColumnHeaders: true,
useKeysAsHeaders: false,
});
expect(() => addHeaders(config, [])(mkCsvOutput(""))).toThrow();
});
});
describe("build headers based on input", () => {
it("should retain order", () => {
const config = mkConfig({
showColumnHeaders: true,
});
const nameAndDate = asString(
addHeaders(config, ["name", "date"])(mkCsvOutput("")),
);
expect(nameAndDate).toEqual('"name","date"' + endOfLine);
const dateAndCity = asString(
addHeaders(config, ["date", "city"])(mkCsvOutput("")),
);
expect(dateAndCity).toEqual('"date","city"' + endOfLine);
});
});
describe("pretty header mappings", () => {
it("should allow columnHeaders to contain objects with a display label", () => {
const config = mkConfig({
columnHeaders: ["name", { key: "date", displayLabel: "Date" }],
});
const nameAndDate = asString(
addHeaders(config, ["name", { key: "date", displayLabel: "Date" }])(
mkCsvOutput(""),
),
);
expect(nameAndDate).toEqual('"name","Date"' + endOfLine);
});
});
});
describe("addBody", () => {
it("should build csv body", () => {
const config = mkConfig({});
const nameAndDate = asString(
addBody(
config,
["name", "date"],
[{ name: "rouky", date: "2023-09-02" }],
)(mkCsvOutput("")),
);
expect(nameAndDate).toEqual('"rouky","2023-09-02"' + endOfLine);
});
it("should build csv body with pretty headers", () => {
const config = mkConfig({});
const nameAndDate = asString(
addBody(
config,
["name", { key: "date", displayLabel: "Date" }],
[{ name: "rouky", date: "2023-09-02" }],
)(mkCsvOutput("")),
);
expect(nameAndDate).toEqual('"rouky","2023-09-02"' + endOfLine);
});
});
describe("formatData", () => {
it("should use locale string for decimals if config set", () => {
const config = mkConfig({
decimalSeparator: "locale",
});
const formatted = formatData(config, 0.6);
expect(formatted).toEqual(mkFormattedData((0.6).toLocaleString()));
});
it("should use custom decimal separator if set", () => {
const config = mkConfig({
decimalSeparator: "|",
});
const formatted = formatData(config, 0.6);
expect(formatted).toEqual(mkFormattedData("0|6"));
});
it("should properly quote strings that may conflict with generation", () => {
// Default case should quote stings
let config = mkConfig({});
const defaultQuote = formatData(config, "test");
expect(defaultQuote).toEqual(mkFormattedData('"test"'));
// Use custom quote strings
config = mkConfig({ quoteCharacter: "^" });
const customQuotes = formatData(config, "test");
expect(customQuotes).toEqual(mkFormattedData("^test^"));
// Disable quoting strings
config = mkConfig({ quoteStrings: false });
const disableQuotes = formatData(config, "test");
expect(disableQuotes).toEqual(mkFormattedData("test"));
});
describe("force quote problem characters", () => {
it("should wrap problem data in quoteStrings", () => {
// Quote field separator
let config = mkConfig({ quoteStrings: false });
const customQuote = formatData(config, ",");
expect(customQuote).toEqual(mkFormattedData('","'));
// Wrap new line
config = mkConfig({ quoteStrings: false });
const wrapNewLine = formatData(config, "test\n");
expect(wrapNewLine).toEqual(mkFormattedData('"test\n"'));
// Wrap carrage return
config = mkConfig({ quoteStrings: false });
const wrapCR = formatData(config, "test\r");
expect(wrapCR).toEqual(mkFormattedData('"test\r"'));
// Force quote with custom character
config = mkConfig({ quoteStrings: false, quoteCharacter: "|" });
const wrapCRWithCustom = formatData(config, "test\r");
expect(wrapCRWithCustom).toEqual(mkFormattedData("|test\r|"));
});
});
});
});
================================================
FILE: lib/__specs__/main.spec.ts
================================================
import { describe, it, expect } from "bun:test";
import { mkConfig } from "../config.ts";
import { asBlob, generateCsv } from "../generator.ts";
import { ConfigOptions, MediaType } from "../types.ts";
import { asString } from "../helpers.ts";
const mockData = [
{
name: "Test 1",
age: 13,
average: 8.2,
approved: true,
description: "Test 1 description",
quotedNumber: "01234",
},
{
name: "Test 2",
age: 11,
average: 8.2,
approved: true,
description: "Test 2 description",
quotedNumber: "05678",
},
{
name: "Test 4",
age: 10,
average: 8.2,
approved: true,
description: "Test 3 description",
quotedNumber: "09999",
},
];
describe("ExportToCsv", () => {
it("should create a comma seperated string", () => {
const options: ConfigOptions = {
title: "Test Csv",
useBom: true,
useKeysAsHeaders: true,
};
const string = asString(generateCsv(options)(mockData));
expect(typeof string === "string").toBeTruthy();
});
it("should allow keys with spaces", () => {
const mockDataOne = [
{
"Hello world": "test",
"this is another string with many spaces": 10,
},
];
const optionsOne = mkConfig({ useBom: false, useKeysAsHeaders: true });
const stringOne = asString(generateCsv(optionsOne)(mockDataOne));
expect(stringOne).toEqual(
'"Hello world","this is another string with many spaces"\r\n"test",10\r\n',
);
const mockDataTwo = [
{
"Hello world": "test",
"this is another string with many spaces": 10,
},
];
const optionsTwo = mkConfig({
useBom: false,
showColumnHeaders: true,
columnHeaders: ["Hello world", "this is another string with many spaces"],
});
const stringTwo = asString(generateCsv(optionsTwo)(mockDataTwo));
expect(stringTwo).toEqual(
'"Hello world","this is another string with many spaces"\r\n"test",10\r\n',
);
});
it("should use fieldSeparator if supplied", () => {
const options: ConfigOptions = {
title: "Test Csv",
useBom: false,
useKeysAsHeaders: true,
fieldSeparator: ";",
};
const string = asString(generateCsv(options)([{ test: "hello" }]));
expect(string).toEqual('"test"\r\n"hello"\r\n');
});
it("should use keys of first object in collection as headers", () => {
const options: ConfigOptions = {
title: "Test Csv",
useBom: true,
useKeysAsHeaders: true,
};
const string = asString(generateCsv(options)(mockData));
const firstLine = string.split("\n")[0];
const keys = firstLine.split(",").map((s: string) => s.trim());
expect(keys).toEqual([
'"name"',
'"age"',
'"average"',
'"approved"',
'"description"',
'"quotedNumber"',
]);
});
it("should retain order of headers when given as option", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: true,
showColumnHeaders: true,
columnHeaders: ["name", "average", "age", "approved", "description"],
};
const withDefaults = mkConfig(options);
const output = asString(generateCsv(withDefaults)(mockData));
const firstLine = output.split("\n")[0];
const keys = firstLine.split(",").map((s: string) => s.trim());
expect(keys).toEqual([
'"name"',
'"average"',
'"age"',
'"approved"',
'"description"',
]);
});
it("should only use columns in columnHeaders", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: true,
showColumnHeaders: true,
columnHeaders: ["name", "age"],
};
const output = asString(generateCsv(options)(mockData));
const firstLine = output.split("\n")[0];
const keys = firstLine.split(",").map((s: string) => s.trim());
expect(keys).toEqual(['"name"', '"age"']);
});
it("should allow only headers to be generated", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
columnHeaders: ["name", "age"],
};
const output = asString(generateCsv(options)([]));
expect(output).toEqual('"name","age"\r\n');
});
it("should throw when no data supplied", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: false,
};
expect(() => {
generateCsv(options)([]);
}).toThrow();
});
it("should allow null values", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
useKeysAsHeaders: true,
};
const output = asString(
generateCsv(options)([
{
"non-null": 24,
nullish: null,
},
]),
);
expect(output).toBe('"non-null","nullish"\r\n24,"null"\r\n');
});
it("should convert undefined to empty string by default", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
useKeysAsHeaders: true,
};
const output = asString(
generateCsv(options)([
{
car: "toyota",
color: "blue",
},
{
car: "chevrolet",
},
]),
);
expect(output).toBe(
'"car","color"\r\n"toyota","blue"\r\n"chevrolet",""\r\n',
);
});
it("should replace undefined with specified value", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
useKeysAsHeaders: true,
replaceUndefinedWith: "TEST",
};
const output = asString(
generateCsv(options)([
{
car: "toyota",
color: "blue",
},
{
car: "chevrolet",
},
]),
);
expect(output).toBe(
'"car","color"\r\n"toyota","blue"\r\n"chevrolet","TEST"\r\n',
);
});
it("should handle varying data shapes by manually setting column headers", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
columnHeaders: ["car", "color", "town"],
};
const output = asString(
generateCsv(options)([
{
car: "toyota",
color: "blue",
},
{
car: "chevrolet",
},
{
town: "montreal",
},
]),
);
expect(output).toBe(
'"car","color","town"\r\n"toyota","blue",""\r\n"chevrolet","",""\r\n"","","montreal"\r\n',
);
});
it("should escape double quotes when quote is double quote", () => {
const options: ConfigOptions = {
quoteCharacter: '"',
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
useKeysAsHeaders: true,
};
const output = asString(
generateCsv(options)([
{
"escape-it": 24,
song: 'Mack "The Knife"',
},
]),
);
expect(output).toBe('"escape-it","song"\r\n24,"Mack ""The Knife"""\r\n');
});
it("should not escape double quotes when quote is not double quote", () => {
const options: ConfigOptions = {
quoteCharacter: "'",
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
useKeysAsHeaders: true,
};
const output = asString(
generateCsv(options)([
{
"escape-it": 24,
song: 'Mack "The Knife"',
},
]),
);
expect(output).toBe("'escape-it','song'\r\n24,'Mack \"The Knife\"'\r\n");
});
it("should properly quote headers", () => {
const options: ConfigOptions = {
filename: "Test Csv 2",
useBom: false,
showColumnHeaders: true,
columnHeaders: ["name", "age"],
};
const output = asString(generateCsv(options)(mockData));
const firstLine = output.split("\n")[0];
expect(firstLine).toBe('"name","age"\r');
});
it("should put the title on the first line", () => {
const options: ConfigOptions = {
title: "Test Csv 2",
showTitle: true,
useBom: false,
showColumnHeaders: true,
columnHeaders: ["name", "age"],
};
const output = asString(generateCsv(options)(mockData));
const firstLine = output.split("\n")[0];
expect(firstLine).toBe("Test Csv 2\r");
});
it("should allow for custom file extensions", () => {
const csvOpts: ConfigOptions = {
mediaType: MediaType.csv,
};
const csvConf = mkConfig(csvOpts);
const txtOpts: ConfigOptions = {
mediaType: MediaType.plain,
};
const txtConf = mkConfig(txtOpts);
const tsvOpts: ConfigOptions = {
mediaType: MediaType.tsv,
};
const tsvConf = mkConfig(tsvOpts);
expect(csvConf.mediaType).toBe(MediaType.csv);
expect(txtConf.mediaType).toBe(MediaType.plain);
expect(tsvConf.mediaType).toBe(MediaType.tsv);
});
describe("asBlob", () => {
it("should construct a valid blob based on options", async () => {
const options: ConfigOptions = {
title: "Test Csv 2",
showTitle: true,
useBom: false,
showColumnHeaders: true,
columnHeaders: ["name", "age"],
};
const output = generateCsv(options)(mockData);
const blob = asBlob(options)(output);
const text = await blob.text();
expect(blob.type).toBe("text/csv;charset=utf8;");
expect(text.split("\n")[0]).toBe("Test Csv 2\r");
expect(blob.size).toBe(65);
});
});
});
================================================
FILE: lib/config.ts
================================================
import { WithDefaults, ConfigOptions, MediaType } from "./types.ts";
export const defaults: WithDefaults<ConfigOptions> = {
fieldSeparator: ",",
decimalSeparator: ".",
quoteStrings: true,
quoteCharacter: '"',
showTitle: false,
title: "My Generated Report",
filename: "generated",
showColumnHeaders: true,
useTextFile: false,
fileExtension: "csv",
mediaType: MediaType.csv,
useBom: true,
columnHeaders: [],
useKeysAsHeaders: false,
boolDisplay: { true: "TRUE", false: "FALSE" },
replaceUndefinedWith: "",
};
export const endOfLine = "\r\n";
export const byteOrderMark = "\ufeff";
export const mkConfig: (opts: ConfigOptions) => WithDefaults<ConfigOptions> = (
opts,
) => Object.assign({}, defaults, opts);
================================================
FILE: lib/errors.ts
================================================
export class CsvGenerationError extends Error {
constructor(message: string) {
super(message);
this.name = "CsvGenerationError";
}
}
export class EmptyHeadersError extends Error {
constructor(message: string) {
super(message);
this.name = "EmptyHeadersError";
}
}
export class CsvDownloadEnvironmentError extends Error {
constructor(message: string) {
super(message);
this.name = "CsvDownloadEnvironmentError";
}
}
export class UnsupportedDataFormatError extends Error {
constructor(message: string) {
super(message);
this.name = "UnsupportedDataFormatError";
}
}
================================================
FILE: lib/generator.ts
================================================
// Required for `window` to work. Since `types` is set in `tsconfig.json`
// `lib` no longer works
/// <reference lib="dom" />
import { mkConfig } from "./config.ts";
import { CsvDownloadEnvironmentError, CsvGenerationError } from "./errors.ts";
import { addBOM, addBody, addHeaders, addTitle, thread } from "./helpers.ts";
import {
CsvOutput,
ConfigOptions,
IO,
mkCsvOutput,
unpack,
AcceptedData,
} from "./types.ts";
/**
*
* Generates CsvOutput data from JSON collection using
* ConfigOptions given.
*
* To comfortably use the data as a string around your
* application, look at {@link asString}.
*
* @throws {CsvGenerationError | EmptyHeadersError}
*/
export const generateCsv =
(config: ConfigOptions) =>
<
T extends {
[k: string | number]: AcceptedData;
},
>(
data: Array<T>,
): CsvOutput => {
const withDefaults = mkConfig(config);
const headers = withDefaults.useKeysAsHeaders
? Object.keys(data[0])
: withDefaults.columnHeaders;
// Build csv output starting with an empty string
let output = thread(
mkCsvOutput(""),
addBOM(withDefaults),
addTitle(withDefaults),
addHeaders(withDefaults, headers),
addBody(withDefaults, headers, data),
);
if (unpack(output).length < 1) {
throw new CsvGenerationError(
"Output is empty. Is your data formatted correctly?",
);
}
return output;
};
/**
* Returns the Blob representation of the CsvOutput generated
* by `generateCsv`. This is useful if you need to access the
* data for downloading in other contexts; like browser extensions.
*/
export const asBlob =
(config: ConfigOptions) =>
(csvOutput: CsvOutput): Blob => {
const withDefaults = mkConfig(config);
const data = unpack(csvOutput);
// Create blob from CsvOutput using the supplied mime type.
const mimeType = withDefaults.useTextFile
? "text/plain"
: withDefaults.mediaType;
const blob = new Blob([data], {
type: `${mimeType};charset=utf8;`,
});
return blob;
};
/**
*
* **Only supported in browser environment.**
*
* Will create a hidden anchor link in the page with the
* download attribute set to a blob version of the CsvOutput data.
*
* @throws {CsvDownloadEnvironmentError}
*/
export const download =
(config: ConfigOptions) =>
(csvOutput: CsvOutput): IO => {
// Downloading is only supported in a browser environment.
// Node users can simply write the output from generateCsv
// to disk.
if (!window) {
throw new CsvDownloadEnvironmentError(
"Downloading only supported in a browser environment.",
);
}
// Create blob from CsvOutput either as text or csv file.
const blob = asBlob(config)(csvOutput);
const withDefaults = mkConfig(config);
const fileExtension = withDefaults.useTextFile
? "txt"
: withDefaults.fileExtension;
const fileName = `${withDefaults.filename}.${fileExtension}`;
// Create link element in the browser and set the download
// attribute to the blob that was created.
const link = document.createElement("a");
link.download = fileName;
link.href = URL.createObjectURL(blob);
// Ensure the link isn't visible to the user or cause layout shifts.
link.setAttribute("visibility", "hidden");
// Add to document body, click and remove it.
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
================================================
FILE: lib/helpers.ts
================================================
import { byteOrderMark, endOfLine } from "./config.ts";
import { EmptyHeadersError, UnsupportedDataFormatError } from "./errors.ts";
import {
AcceptedData,
ColumnHeader,
ConfigOptions,
CsvOutput,
CsvRow,
FormattedData,
HeaderDisplayLabel,
HeaderKey,
Newtype,
WithDefaults,
mkCsvOutput,
mkCsvRow,
mkFormattedData,
mkHeaderDisplayLabel,
mkHeaderKey,
pack,
unpack,
} from "./types.ts";
const getHeaderKey = (columnHeader: ColumnHeader): HeaderKey =>
typeof columnHeader === "object"
? mkHeaderKey(columnHeader.key)
: mkHeaderKey(columnHeader);
const getHeaderDisplayLabel = (
columnHeader: ColumnHeader,
): HeaderDisplayLabel =>
typeof columnHeader === "object"
? mkHeaderDisplayLabel(columnHeader.displayLabel)
: mkHeaderDisplayLabel(columnHeader);
export const thread = <T>(initialValue: T, ...fns: Array<Function>): T =>
fns.reduce((r, fn) => fn(r), initialValue);
export const addBOM =
(config: WithDefaults<ConfigOptions>) =>
(output: CsvOutput): CsvOutput =>
config.useBom ? mkCsvOutput(unpack(output) + byteOrderMark) : output;
export const addTitle =
(config: WithDefaults<ConfigOptions>) =>
(output: CsvOutput): CsvOutput =>
config.showTitle
? addEndOfLine(mkCsvOutput(unpack(output) + config.title))(mkCsvRow(""))
: output;
export const addEndOfLine =
(output: CsvOutput) =>
(row: CsvRow): CsvOutput =>
mkCsvOutput(unpack(output) + unpack(row) + endOfLine);
export const buildRow =
(config: WithDefaults<ConfigOptions>) =>
(row: CsvRow, data: FormattedData): CsvRow =>
addFieldSeparator(config)(mkCsvRow(unpack(row) + unpack(data)));
export const addFieldSeparator =
(config: WithDefaults<ConfigOptions>) =>
<T extends Newtype<any, string>>(output: T): T =>
pack<T>(unpack(output) + config.fieldSeparator);
export const addHeaders =
(config: WithDefaults<ConfigOptions>, headers: Array<ColumnHeader>) =>
(output: CsvOutput): CsvOutput => {
if (!config.showColumnHeaders) {
return output;
}
if (headers.length < 1) {
throw new EmptyHeadersError(
"Option to show headers but none supplied. Make sure there are keys in your collection or that you've supplied headers through the config options.",
);
}
let row = mkCsvRow("");
for (let keyPos = 0; keyPos < headers.length; keyPos++) {
const header = getHeaderDisplayLabel(headers[keyPos]);
row = buildRow(config)(row, formatData(config, unpack(header)));
}
row = mkCsvRow(unpack(row).slice(0, -1));
return addEndOfLine(output)(row);
};
export const addBody =
<T extends Array<{ [k: string]: AcceptedData }>>(
config: WithDefaults<ConfigOptions>,
headers: Array<ColumnHeader>,
bodyData: T,
) =>
(output: CsvOutput): CsvOutput => {
let body = output;
for (var i = 0; i < bodyData.length; i++) {
let row = mkCsvRow("");
for (let keyPos = 0; keyPos < headers.length; keyPos++) {
const header = getHeaderKey(headers[keyPos]);
const data = bodyData[i][unpack(header)];
row = buildRow(config)(row, formatData(config, data));
}
// Remove trailing comma
row = mkCsvRow(unpack(row).slice(0, -1));
body = addEndOfLine(body)(row);
}
return body;
};
/**
*
* Convert CsvOutput => string for the typechecker.
*
* Useful if you need to take the return value and
* treat is as a string in the rest of your program.
*/
export const asString = unpack<Newtype<any, string>>;
const isFloat = (input: boolean | string | number): boolean =>
+input === input && (!isFinite(input) || Boolean(input % 1));
const formatNumber = (config: ConfigOptions, data: number): FormattedData => {
if (isFloat(data)) {
if (config.decimalSeparator === "locale") {
return mkFormattedData(data.toLocaleString());
}
if (config.decimalSeparator) {
return mkFormattedData(
data.toString().replace(".", config.decimalSeparator),
);
}
}
return mkFormattedData(data.toString());
};
const formatString = (config: ConfigOptions, data: string): FormattedData => {
let val = data;
if (
config.quoteStrings ||
(config.fieldSeparator && data.indexOf(config.fieldSeparator) > -1) ||
(config.quoteCharacter && data.indexOf(config.quoteCharacter) > -1) ||
data.indexOf("\n") > -1 ||
data.indexOf("\r") > -1
) {
val =
config.quoteCharacter +
escapeDoubleQuotes(data, config.quoteCharacter) +
config.quoteCharacter;
}
return mkFormattedData(val);
};
const formatBoolean = (config: ConfigOptions, data: boolean): FormattedData => {
// Convert to string to use as lookup in config
const asStr = data ? "true" : "false";
// Return the custom boolean display. We expect the callsite to validate
// that `boolDisplay` is set.
return mkFormattedData(config.boolDisplay![asStr]);
};
const formatNullish = (
config: ConfigOptions,
data: null | undefined,
): FormattedData => {
if (
typeof data === "undefined" &&
config.replaceUndefinedWith !== undefined
) {
// Coerce whatever was passed to a string
return formatString(config, config.replaceUndefinedWith + "");
}
if (data === null) {
return formatString(config, "null");
}
return formatString(config, "");
};
export const formatData = (
config: ConfigOptions,
data: AcceptedData,
): FormattedData => {
if (typeof data === "number") {
return formatNumber(config, data);
}
if (typeof data === "string") {
return formatString(config, data);
}
if (typeof data === "boolean" && config.boolDisplay) {
return formatBoolean(config, data);
}
if (data === null || typeof data === "undefined") {
return formatNullish(config, data);
}
throw new UnsupportedDataFormatError(
`
typeof ${typeof data} isn't supported. Only number, string, boolean, null and undefined are supported.
Please convert the data in your object to one of those before generating the CSV.
`,
);
};
/**
* If double-quotes are used to enclose fields, then a double-quote
* appearing inside a field must be escaped by preceding it with
* another double quote.
*
* See https://www.rfc-editor.org/rfc/rfc4180
*/
function escapeDoubleQuotes(data: string, quoteCharacter?: string): string {
if (quoteCharacter == '"' && data.indexOf('"') > -1) {
return data.replace(/"/g, '""');
}
return data;
}
================================================
FILE: lib/index.ts
================================================
export * from "./generator.ts";
export { mkConfig } from "./config.ts";
export { CsvOutput, ConfigOptions, ColumnHeader, MediaType } from "./types.ts";
export { asString } from "./helpers.ts";
================================================
FILE: lib/types.ts
================================================
export type Newtype<URI, A> = {
readonly _URI: URI;
readonly _A: A;
};
export type WithDefaults<T> = Required<T>;
export type ColumnHeader = string | { key: string; displayLabel: string };
export enum MediaType {
csv = "text/csv",
tsv = "text/tab-separated-values",
plain = "text/plain",
}
export type ConfigOptions = {
filename?: string;
fieldSeparator?: string;
quoteStrings?: boolean;
quoteCharacter?: string;
decimalSeparator?: string;
showColumnHeaders?: boolean;
showTitle?: boolean;
title?: string;
/**
* Use `fileExtension` instead.
*
* Will be removed in the next major version (`2.x.x`)
*
* @deprecated
*/
useTextFile?: boolean;
fileExtension?: string;
mediaType?: MediaType;
useBom?: boolean;
columnHeaders?: Array<ColumnHeader>;
useKeysAsHeaders?: boolean;
boolDisplay?: { true: string; false: string };
replaceUndefinedWith?: string | boolean | null;
};
export type HeaderKey = Newtype<{ readonly HeaderKey: unique symbol }, string>;
export type HeaderDisplayLabel = Newtype<
{ readonly HeaderDisplayLabel: unique symbol },
string
>;
export type AcceptedData = number | string | boolean | null | undefined;
export type FormattedData = Newtype<
{ readonly FormattedData: unique symbol },
string
>;
export type CsvOutput = Newtype<{ readonly CsvOutput: unique symbol }, string>;
export type CsvRow = Newtype<{ readonly CsvRow: unique symbol }, string>;
export type IO = void;
export const pack = <T extends Newtype<any, any>>(value: T["_A"]): T =>
value as any as T;
export const unpack = <T extends Newtype<any, any>>(newtype: T): T["_A"] =>
newtype as any as T["_A"];
export const mkFormattedData = pack<FormattedData>;
export const mkCsvOutput = pack<CsvOutput>;
export const mkCsvRow = pack<CsvRow>;
export const mkHeaderKey = pack<HeaderKey>;
export const mkHeaderDisplayLabel = pack<HeaderDisplayLabel>;
================================================
FILE: package.json
================================================
{
"name": "export-to-csv",
"version": "1.4.0",
"description": "Easily create CSV data from json collection",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/alexcaza/export-to-csv.git"
},
"scripts": {
"build": "rm -rf output && bun build index.ts --outdir ./output --minify && tsc",
"watch": "rm -rf output && bun build index.ts --outdir ./output --minify --watch",
"e2e": "rm -rf integration/export-to-csv.js && bun run build && cp output/index.js integration/export-to-csv.js && playwright test",
"e2e-ci": "rm -rf integration/export-to-csv.js && bun run build && cp output/index.js integration/export-to-csv.js && bunx playwright install --with-deps && playwright test",
"e2e-server": "bunx http-server ./integration -p 3000",
"test": "bun test lib/__specs__/",
"format": "bunx prettier --write '.'",
"prepublishOnly": "bun run build",
"prepare": "bun run build"
},
"keywords": [
"export-to-csv",
"export-to-excel",
"csv",
"excel",
"libreoffice",
"openoffice",
"typescript",
"zero-dependencies"
],
"author": "alexcaza",
"email": "alex@alexcaza.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/alexcaza/export-to-csv/issues"
},
"exports": "./output/index.js",
"types": "./output/index.d.ts",
"engines": {
"node": "^v12.20.0 || >=v14.13.0"
},
"homepage": "https://github.com/alexcaza/export-to-csv#readme",
"devDependencies": {
"@playwright/test": "1.50.1",
"bun-types": "~1.2.8",
"http-server": "^14.1.1",
"prettier": "~3.5.3",
"bun": "~1.2.8",
"typescript": "~5.8.2"
}
}
================================================
FILE: playwright.config.ts
================================================
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./integration",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run e2e-server",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"noImplicitAny": true,
"module": "node16",
"target": "esnext",
"declaration": true,
"types": ["bun-types"],
"lib": ["esnext"],
"moduleResolution": "node16",
"moduleDetection": "force",
"outDir": "./output",
"strict": true,
"forceConsistentCasingInFileNames": true,
"composite": true,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"skipLibCheck": true
},
"files": ["index.ts"],
"include": ["lib/**/*"],
"exclude": ["node_modules", "__spec_"]
}
gitextract_xjlj126q/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc.json ├── .weztermocil/ │ └── watch.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lockb ├── flake.nix ├── index.ts ├── integration/ │ ├── index.html │ └── integration.spec.ts ├── lib/ │ ├── __specs__/ │ │ ├── config.spec.ts │ │ ├── helpers.spec.ts │ │ └── main.spec.ts │ ├── config.ts │ ├── errors.ts │ ├── generator.ts │ ├── helpers.ts │ ├── index.ts │ └── types.ts ├── package.json ├── playwright.config.ts └── tsconfig.json
SYMBOL INDEX (21 symbols across 3 files)
FILE: lib/errors.ts
class CsvGenerationError (line 1) | class CsvGenerationError extends Error {
method constructor (line 2) | constructor(message: string) {
class EmptyHeadersError (line 8) | class EmptyHeadersError extends Error {
method constructor (line 9) | constructor(message: string) {
class CsvDownloadEnvironmentError (line 15) | class CsvDownloadEnvironmentError extends Error {
method constructor (line 16) | constructor(message: string) {
class UnsupportedDataFormatError (line 22) | class UnsupportedDataFormatError extends Error {
method constructor (line 23) | constructor(message: string) {
FILE: lib/helpers.ts
function escapeDoubleQuotes (line 218) | function escapeDoubleQuotes(data: string, quoteCharacter?: string): stri...
FILE: lib/types.ts
type Newtype (line 1) | type Newtype<URI, A> = {
type WithDefaults (line 6) | type WithDefaults<T> = Required<T>;
type ColumnHeader (line 8) | type ColumnHeader = string | { key: string; displayLabel: string };
type MediaType (line 10) | enum MediaType {
type ConfigOptions (line 16) | type ConfigOptions = {
type HeaderKey (line 42) | type HeaderKey = Newtype<{ readonly HeaderKey: unique symbol }, string>;
type HeaderDisplayLabel (line 44) | type HeaderDisplayLabel = Newtype<
type AcceptedData (line 49) | type AcceptedData = number | string | boolean | null | undefined;
type FormattedData (line 50) | type FormattedData = Newtype<
type CsvOutput (line 55) | type CsvOutput = Newtype<{ readonly CsvOutput: unique symbol }, string>;
type CsvRow (line 57) | type CsvRow = Newtype<{ readonly CsvRow: unique symbol }, string>;
type IO (line 59) | type IO = void;
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (68K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 999,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"bug\"\nassignees: \"\"\n---\n\n**Describe the"
},
{
"path": ".github/workflows/pr.yml",
"chars": 1320,
"preview": "name: PR\n\non:\n pull_request:\n branches: [master, main]\n\njobs:\n build:\n timeout-minutes: 5\n name: Build projec"
},
{
"path": ".github/workflows/release.yml",
"chars": 471,
"preview": "name: Publish Package to npmjs\non:\n release:\n types: [published]\njobs:\n build:\n runs-on: ubuntu-latest\n steps"
},
{
"path": ".gitignore",
"chars": 239,
"preview": "node_modules\nnpm-debug.log\ntypings\nThumbs.db\n.DS_Store\n\n*.js\n!karma.conf.js\n!rollup.config.js\n!babel.config.js\n!jest.con"
},
{
"path": ".npmignore",
"chars": 450,
"preview": "# Node generated files\nnode_modules\nnpm-debug.log\n\n# OS generated files\nThumbs.db\n.DS_Store\n\n# Git\n.git\n.github\n\n# TypeS"
},
{
"path": ".npmrc",
"chars": 106,
"preview": "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\nregistry=https://registry.npmjs.org/\nalways-auth=true\n"
},
{
"path": ".prettierrc.json",
"chars": 3,
"preview": "{}\n"
},
{
"path": ".weztermocil/watch.yml",
"chars": 162,
"preview": "windows:\n - name: export-to-csv-watch\n root: .\n layout: tiled\n panes:\n - bun run test -- --watch\n - "
},
{
"path": "CONTRIBUTING.md",
"chars": 2067,
"preview": "# How to contribute to `export-to-csv`\n\nThank you for wanting to make `export-to-csv` better in some way!\n\n## Bugs, ques"
},
{
"path": "LICENSE",
"chars": 1263,
"preview": "MIT License\n\nCopyright for portions of project export-to-csv are held by Javier Telio <jtelio@gmail.com>, 2016 as part o"
},
{
"path": "README.md",
"chars": 13602,
"preview": "# export-to-csv | Export to CSV Mini Library\n\nLike this library and want to support active development?\n\n<a href='https:"
},
{
"path": "flake.nix",
"chars": 709,
"preview": "{\n inputs = {\n nixpkgs.url = \"github:nixos/nixpkgs?ref=nixos-unstable\";\n flake-utils.url = \"github:numtide/flake-"
},
{
"path": "index.ts",
"chars": 32,
"preview": "export * from \"./lib/index.ts\";\n"
},
{
"path": "integration/index.html",
"chars": 3125,
"preview": "<html>\n <head>\n <script type=\"module\">\n import { mkConfig, generateCsv, download } from \"/export-to-csv.js\";\n\n "
},
{
"path": "integration/integration.spec.ts",
"chars": 2559,
"preview": "import { test, expect } from \"@playwright/test\";\n\ntest(\"download csv file (default filename)\", async ({ page }) => {\n a"
},
{
"path": "lib/__specs__/config.spec.ts",
"chars": 499,
"preview": "import { describe, expect, it } from \"bun:test\";\nimport { defaults, mkConfig } from \"../config.ts\";\n\ndescribe(\"mkConfig\""
},
{
"path": "lib/__specs__/helpers.spec.ts",
"chars": 8543,
"preview": "import { describe, expect, it, jest } from \"bun:test\";\nimport {\n addBOM,\n addBody,\n addEndOfLine,\n addFieldSeparator"
},
{
"path": "lib/__specs__/main.spec.ts",
"chars": 9610,
"preview": "import { describe, it, expect } from \"bun:test\";\nimport { mkConfig } from \"../config.ts\";\nimport { asBlob, generateCsv }"
},
{
"path": "lib/config.ts",
"chars": 742,
"preview": "import { WithDefaults, ConfigOptions, MediaType } from \"./types.ts\";\n\nexport const defaults: WithDefaults<ConfigOptions>"
},
{
"path": "lib/errors.ts",
"chars": 615,
"preview": "export class CsvGenerationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"Cs"
},
{
"path": "lib/generator.ts",
"chars": 3493,
"preview": "// Required for `window` to work. Since `types` is set in `tsconfig.json`\n// `lib` no longer works\n/// <reference lib=\"d"
},
{
"path": "lib/helpers.ts",
"chars": 6429,
"preview": "import { byteOrderMark, endOfLine } from \"./config.ts\";\nimport { EmptyHeadersError, UnsupportedDataFormatError } from \"."
},
{
"path": "lib/index.ts",
"chars": 193,
"preview": "export * from \"./generator.ts\";\nexport { mkConfig } from \"./config.ts\";\nexport { CsvOutput, ConfigOptions, ColumnHeader,"
},
{
"path": "lib/types.ts",
"chars": 1908,
"preview": "export type Newtype<URI, A> = {\n readonly _URI: URI;\n readonly _A: A;\n};\n\nexport type WithDefaults<T> = Required<T>;\n\n"
},
{
"path": "package.json",
"chars": 1670,
"preview": "{\n \"name\": \"export-to-csv\",\n \"version\": \"1.4.0\",\n \"description\": \"Easily create CSV data from json collection\",\n \"ty"
},
{
"path": "playwright.config.ts",
"chars": 2084,
"preview": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * Read environment variables from file.\n * https://githu"
},
{
"path": "tsconfig.json",
"chars": 555,
"preview": "{\n \"compilerOptions\": {\n \"noImplicitAny\": true,\n \"module\": \"node16\",\n \"target\": \"esnext\",\n \"declaration\": t"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the alexcaza/export-to-csv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (62.0 KB), approximately 15.6k tokens, and a symbol index with 21 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.