[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"bug\"\nassignees: \"\"\n---\n\n**Describe the bug**\nA 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).\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Package versions:**\n\n- Typescript: [5.0]\n- export-to-csv: [1.2.4]\n- runtime: [e.g. node 20 or bun 1.1]\n\n**Desktop (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n\n- Device: [e.g. iPhone6]\n- OS: [e.g. iOS8.1]\n- Browser [e.g. stock browser, safari]\n- Version [e.g. 22]\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: PR\n\non:\n  pull_request:\n    branches: [master, main]\n\njobs:\n  build:\n    timeout-minutes: 5\n    name: Build project\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run build step\n        uses: oven-sh/setup-bun@v2.0.1\n        with:\n          bun-version: latest\n      - run: bun install --frozen-lockfile\n      - run: bun run build\n  tests:\n    timeout-minutes: 60\n    name: Unit & E2E Tests\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run unit and e2e tests\n        uses: oven-sh/setup-bun@v2.0.1\n        with:\n          bun-version: latest\n      - run: bun install --frozen-lockfile\n      - run: bun run test\n      - run: bun run e2e-ci\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 30\n  format:\n    name: Format check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run prettier to check formatting of files\n        uses: oven-sh/setup-bun@v2.0.1\n        with:\n          bun-version: latest\n      - run: bun install --frozen-lockfile\n      - run: bunx prettier -c \".\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Publish Package to npmjs\non:\n  release:\n    types: [published]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      # Setup .npmrc file to publish to npm\n      - uses: oven-sh/setup-bun@v2.0.1\n        with:\n          bun-version: \"latest\"\n          registry-url: \"https://registry.npmjs.org\"\n      - run: bun install --frozen-lockfile\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "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.config.js\n*.map\n*.d.ts\n.idea/\n*.iml\ntest/\ncoverage/\nbuild/\noutput/\n/test-results/\n/playwright-report/\n/playwright/.cache/\n"
  },
  {
    "path": ".npmignore",
    "content": "# 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# TypeScript source files, apart generated type definitions\n*.ts\n!*.d.ts\n\n# Other generated files\n*.spec.*\n*.map\nintegration\nlib/**/*.d.ts\nbuild\n*.lock\nbun.lockb\nbunfig.toml\n\n# Editor files\n.prettierrc.json\n.todo\n\n# Other build-related files\n.travis.yml\nkarma.*\ntsconfig.*\n*.config.*\nplaywright*\ntest-results\ncoverage\n\n# nix files\n*.nix\n"
  },
  {
    "path": ".npmrc",
    "content": "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\nregistry=https://registry.npmjs.org/\nalways-auth=true\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{}\n"
  },
  {
    "path": ".weztermocil/watch.yml",
    "content": "windows:\n  - name: export-to-csv-watch\n    root: .\n    layout: tiled\n    panes:\n      - bun run test -- --watch\n      - bun run watch\n      - bun run tsc --watch\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to contribute to `export-to-csv`\n\nThank you for wanting to make `export-to-csv` better in some way!\n\n## Bugs, questions and feature requests\n\n### Bugs\n\nIf 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.\n\n### Questions and feature requests\n\nNot 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.\n\nDon'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.\n\n## Submitting a pull request\n\nThis repository uses [bun](https://bun.sh/) to build and run unit tests, and [playwright](https://playwright.dev/) for end-to-end testing.\n\nWe also do our best to keep things relatively type-safe.\n\nIf you're fixing a bug, ensure the project still builds, and all the tests still run after making your changes by running:\n\n```bash\nbun run build && bun run test && bun run e2e\n```\n\nIf 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.\nIf your new feature requires a new helper function in `helpers.ts`, add a test to cover the helper functionality as well.\n\nOnce 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.\n\n## Conventions\n\nThis 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`.\n\n### The format check in the Pull Request is failing, what do I do?\n\nRun `bun run format` from the root of the project, commit any changes to your working branch and push.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright for portions of project export-to-csv are held by Javier Telio <jtelio@gmail.com>, 2016 as part of project Angular2Csv. \nAll other copyright for project export-to-csv are held by Alex Caza <alex@alexcaza.com>, 2017.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# export-to-csv | Export to CSV Mini Library\n\nLike this library and want to support active development?\n\n<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>\n\nSmall, simple, and single purpose. Zero dependencies, functionally inspired, and fairly well-typed.\n\nIf you're looking for a fully CSV-compliant, consistently maintained, whole-package library, I'd recommend looking elsewhere! (see [alternatives](#alternatives) section below)\n\nIf you want a lightweight, stable, easy-to-use basic CSV generation and download library, feel free to install.\n\n## Installation\n\n```javascript\nnpm install --save export-to-csv\n```\n\n## Usage\n\nThis library was written with TypeScript in mind, so the examples will be in TS.\n\nYou can easily use this library in JavaScript as well. The bundle uses ES modules, which all modern browsers support.\n\nYou 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.\n\n### In-browser\n\n```typescript\nimport { mkConfig, generateCsv, download } from \"export-to-csv\";\n\n// mkConfig merges your options with the defaults\n// and returns WithDefaults<ConfigOptions>\nconst csvConfig = mkConfig({ useKeysAsHeaders: true });\n\nconst mockData = [\n  {\n    name: \"Rouky\",\n    date: \"2023-09-01\",\n    percentage: 0.4,\n    quoted: '\"Pickles\"',\n  },\n  {\n    name: \"Keiko\",\n    date: \"2023-09-01\",\n    percentage: 0.9,\n    quoted: '\"Cactus\"',\n  },\n];\n\n// Converts your Array<Object> to a CsvOutput string based on the configs\nconst csv = generateCsv(csvConfig)(mockData);\n\n// Get the button in your HTML\nconst csvBtn = document.querySelector(\"#csv\");\n\n// Add a click handler that will run the `download` function.\n// `download` takes `csvConfig` and the generated `CsvOutput`\n// from `generateCsv`.\ncsvBtn.addEventListener(\"click\", () => download(csvConfig)(csv));\n```\n\n### Node.js\n\n```typescript\nimport { mkConfig, generateCsv, asString } from \"export-to-csv\";\nimport { writeFile } from \"node:fs\";\nimport { Buffer } from \"node:buffer\";\n\n// mkConfig merges your options with the defaults\n// and returns WithDefaults<ConfigOptions>\nconst csvConfig = mkConfig({ useKeysAsHeaders: true });\n\nconst mockData = [\n  {\n    name: \"Rouky\",\n    date: \"2023-09-01\",\n    percentage: 0.4,\n    quoted: '\"Pickles\"',\n  },\n  {\n    name: \"Keiko\",\n    date: \"2023-09-01\",\n    percentage: 0.9,\n    quoted: '\"Cactus\"',\n  },\n];\n\n// Converts your Array<Object> to a CsvOutput string based on the configs\nconst csv = generateCsv(csvConfig)(mockData);\nconst filename = `${csvConfig.filename}.csv`;\nconst csvBuffer = new Uint8Array(Buffer.from(asString(csv)));\n\n// Write the csv file to disk\nwriteFile(filename, csvBuffer, (err) => {\n  if (err) throw err;\n  console.log(\"file saved: \", filename);\n});\n```\n\n### Using `generateCsv` output as a `string`\n\n**Note: this is only applicable to projects using Typescript. If you're using this library with Javascript, you might not run into this issue.**\n\nThere 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.\n\n```typescript\nimport { mkConfig, generateCsv, asString } from \"export-to-csv\";\n\nconst csvConfig = mkConfig({ useKeysAsHeaders: true });\n\nconst addNewLine = (s: string): string => s + \"\\n\";\n\nconst mockData = [\n  {\n    name: \"Rouky\",\n    date: \"2023-09-01\",\n    percentage: 0.4,\n    quoted: '\"Pickles\"',\n  },\n  {\n    name: \"Keiko\",\n    date: \"2023-09-01\",\n    percentage: 0.9,\n    quoted: '\"Cactus\"',\n  },\n];\n\n// Converts your Array<Object> to a CsvOutput string based on the configs\nconst csvOutput = generateCsv(csvConfig)(mockData);\n\n// This would result in a type error\n// const csvOutputWithNewLine = addNewLine(csvOutput);\n// ❌ => CsvOutput is not assignable to type string.\n\n// This unpacks CsvOutput which turns it into a string before use\nconst csvOutputWithNewLine = addNewLine(asString(csvOutput));\n```\n\nThe reason the `CsvOutput` type exists is to prevent accidentally passing in a string which wasn't formatted by `generateCsv` to the `download` function.\n\n### Using `generateCsv` output as a `Blob`\n\nA 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.\n\n```typescript\nimport { mkConfig, generateCsv, asBlob } from \"export-to-csv\";\n\n// mkConfig merges your options with the defaults\n// and returns WithDefaults<ConfigOptions>\nconst csvConfig = mkConfig({ useKeysAsHeaders: true });\n\nconst mockData = [\n  {\n    name: \"Rouky\",\n    date: \"2023-09-01\",\n    percentage: 0.4,\n    quoted: '\"Pickles\"',\n  },\n  {\n    name: \"Keiko\",\n    date: \"2023-09-01\",\n    percentage: 0.9,\n    quoted: '\"Cactus\"',\n  },\n];\n\n// Converts your Array<Object> to a CsvOutput string based on the configs\nconst csv = generateCsv(csvConfig)(mockData);\n\n// Generate the Blob from the CsvOutput\nconst blob = asBlob(csvConfig)(csv);\n\n// Requires URL to be available (web workers or client scripts only)\nconst url = URL.createObjectURL(blob);\n\n// Assuming there's a button with an id of csv in the DOM\nconst csvBtn = document.querySelector(\"#csv\");\n\ncsvBtn.addEventListener(\"click\", () => {\n  // Use Chrome's downloads API for extensions\n  chrome.downloads.download({\n    url,\n    body: csv,\n    filename: \"chrome-extension-output.csv\",\n  });\n});\n```\n\n## API\n\n| Option              | Default                          | Type                                                   | Description                                                                                                                                                                                                                                                                                                                               |\n| ------------------- | -------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `fieldSeparator`    | `\",\"`                            | `string`                                               | Defines the field separator character                                                                                                                                                                                                                                                                                                     |\n| `filename`          | `\"generated\"`                    | `string`                                               | Sets the name of the file created from the `download` function                                                                                                                                                                                                                                                                            |\n| `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.                                                                                                                                                                                 |\n| `quoteCharacter`    | `'\"'`                            | `string`                                               | Sets the quote character to use.                                                                                                                                                                                                                                                                                                          |\n| `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).                                                                                        |\n| `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)                                                                                                                                                                                                                       |\n| `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`])                                                                                                                                                                                                                           |\n| `showColumnHeaders` | `true`                           | `boolean`                                              | Determines if columns should have headers. When set to `false`, the first row of the CSV will be data.                                                                                                                                                                                                                                    |\n| `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. |\n| `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!**                                                                           |\n| `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`.**                                                                                                                                                                         |\n| `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 🤷‍♂️                                                                                                                                                                          |\n| `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.                                                                                                                                                            |\n| `fileExtension`     | `csv`                            | `string`                                               | File extension to use. Currently, this only applies if `useTextFile` is `false`.                                                                                                                                                                                                                                                          |\n\n# Alternatives\n\nAs 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.\n\n- https://csv.js.org/\n- https://www.papaparse.com/\n\n# Thanks!\n\nThis library was originally based on [this library](https://github.com/javiertelioz/angular2-csv) by Javier Telio\n\n| Credits and Original Authors                        |\n| :-------------------------------------------------- |\n| **[javiertelioz](https://github.com/javiertelioz)** |\n| **[sn123](https://github.com/sn123)**               |\n| **[arf1980](https://github.com/arf1980)**           |\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs?ref=nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }: \n    flake-utils.lib.eachDefaultSystem\n        (system:\n            let pkgs = nixpkgs.legacyPackages.${system}; in\n            {\n                devShells.default = pkgs.mkShell {\n                    nativeBuildInputs = with pkgs; [\n                        bun\n                        playwright-driver.browsers\n                    ];\n\n                    shellHook = ''\n                      export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}\n                    '';\n                };\n            }\n        );\n}\n"
  },
  {
    "path": "index.ts",
    "content": "export * from \"./lib/index.ts\";\n"
  },
  {
    "path": "integration/index.html",
    "content": "<html>\n  <head>\n    <script type=\"module\">\n      import { mkConfig, generateCsv, download } from \"/export-to-csv.js\";\n\n      // Configs\n      const csvConfig = mkConfig({ useKeysAsHeaders: true });\n      const txtConfig = mkConfig({\n        useKeysAsHeaders: true,\n        useTextFile: true,\n      });\n      const tsvConfig = mkConfig({\n        useKeysAsHeaders: true,\n        useTextFile: true,\n        delimiter: \"\\t\",\n      });\n      const csvConfigCustomFilename = mkConfig({\n        useKeysAsHeaders: true,\n        filename: \"Best CSV\",\n      });\n      const txtConfigCustomFilename = mkConfig({\n        useKeysAsHeaders: true,\n        useTextFile: true,\n        filename: \"Best CSV as Text\",\n      });\n      const tsvConfigCustomFilename = mkConfig({\n        useKeysAsHeaders: true,\n        fileExtension: \"tsv\",\n        delimiter: \"\\t\",\n        filename: \"Best CSV as TSV\",\n      });\n\n      const mockData = [\n        {\n          name: \"Rouky\",\n          date: \"2023-09-01\",\n          percentage: 0.4,\n          quoted: '\"Pickles\"',\n          nullish: null,\n          \"string with spaces as header\": \"value with spaces\",\n        },\n        {\n          name: \"Keiko\",\n          date: \"2023-09-01\",\n          percentage: 0.9,\n          quoted: '\"Cactus\"',\n          nullish: \"test\",\n          \"string with spaces as header\": \"more spaces\",\n        },\n      ];\n\n      const csv = generateCsv(csvConfig)(mockData);\n      const txt = generateCsv(txtConfig)(mockData);\n      const tsv = generateCsv(tsvConfig)(mockData);\n      const csvCustomFilename = generateCsv(csvConfig)(mockData);\n      const txtCustomFilename = generateCsv(txtConfig)(mockData);\n      const tsvCustomFilename = generateCsv(tsvConfig)(mockData);\n\n      const csvBtn = document.querySelector(\"#csv\");\n      const txtBtn = document.querySelector(\"#txt\");\n      const tsvBtn = document.querySelector(\"#tsv\");\n      const csvBtnCustom = document.querySelector(\"#csv-custom\");\n      const txtBtnCustom = document.querySelector(\"#txt-custom\");\n      const tsvBtnCustom = document.querySelector(\"#tsv-custom\");\n\n      csvBtn.addEventListener(\"click\", () => download(csvConfig)(csv));\n      txtBtn.addEventListener(\"click\", () => download(txtConfig)(txt));\n      tsvBtn.addEventListener(\"click\", () => download(tsvConfig)(tsv));\n      csvBtnCustom.addEventListener(\"click\", () =>\n        download(csvConfigCustomFilename)(csvCustomFilename),\n      );\n      txtBtnCustom.addEventListener(\"click\", () =>\n        download(txtConfigCustomFilename)(txtCustomFilename),\n      );\n      tsvBtnCustom.addEventListener(\"click\", () =>\n        download(tsvConfigCustomFilename)(tsvCustomFilename),\n      );\n    </script>\n  </head>\n\n  <body>\n    <h1>Export to CSV testing</h1>\n    <button id=\"csv\">Download as CSV</button>\n    <button id=\"txt\">Download as TXT</button>\n    <button id=\"tsv\">Download as TSV</button>\n    <button id=\"csv-custom\">Download as CSV (custom filename)</button>\n    <button id=\"txt-custom\">Download as TXT (custom filename)</button>\n    <button id=\"tsv-custom\">\n      Download as TSV (custom filename and extension)\n    </button>\n  </body>\n</html>\n"
  },
  {
    "path": "integration/integration.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest(\"download csv file (default filename)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#csv\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"generated.csv\");\n});\n\ntest(\"download csv file (custom filename)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#csv-custom\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"Best CSV.csv\");\n});\n\ntest(\"download txt file (default filename)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#txt\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"generated.txt\");\n});\n\ntest(\"download txt file (custom filename)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#txt-custom\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"Best CSV as Text.txt\");\n});\n\ntest(\"download tsv file (default filename)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#tsv\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"generated.txt\");\n});\n\ntest(\"download tsv file (custom filename and extension)\", async ({ page }) => {\n  await page.goto(\"http://127.0.0.1:3000\");\n\n  const [download] = await Promise.all([\n    // Start waiting for the download\n    page.waitForEvent(\"download\"),\n    // Perform the action that initiates download\n    page.locator(\"button#tsv-custom\").click(),\n  ]);\n\n  // assert filename\n  expect(download.suggestedFilename()).toBe(\"Best CSV as TSV.tsv\");\n});\n"
  },
  {
    "path": "lib/__specs__/config.spec.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport { defaults, mkConfig } from \"../config.ts\";\n\ndescribe(\"mkConfig\", () => {\n  it(\"should properly set defaults when empty\", () => {\n    const config = mkConfig({});\n    expect(config).toEqual(defaults);\n  });\n\n  it(\"should properly allow user overrides\", () => {\n    const config = mkConfig({ filename: \"test csv\" });\n    const overrides = {\n      ...defaults,\n      filename: \"test csv\",\n    };\n    expect(config).toEqual(overrides);\n  });\n});\n"
  },
  {
    "path": "lib/__specs__/helpers.spec.ts",
    "content": "import { describe, expect, it, jest } from \"bun:test\";\nimport {\n  addBOM,\n  addBody,\n  addEndOfLine,\n  addFieldSeparator,\n  addHeaders,\n  addTitle,\n  asString,\n  buildRow,\n  formatData,\n  thread,\n} from \"../helpers.ts\";\nimport { byteOrderMark, endOfLine, mkConfig } from \"../config.ts\";\nimport { mkCsvOutput, mkCsvRow, mkFormattedData, unpack } from \"../types.ts\";\n\ndescribe(\"Helpers\", () => {\n  describe(\"thread\", () => {\n    it(\"should call each function\", () => {\n      const val = \"\";\n      const one = jest.fn();\n      const two = jest.fn();\n      const three = jest.fn();\n\n      thread(val, one, two, three);\n\n      expect(one).toHaveBeenCalled();\n      expect(two).toHaveBeenCalled();\n      expect(three).toHaveBeenCalled();\n    });\n\n    it(\"should call each function with initial value\", () => {\n      const val = \"test\";\n      const one = jest.fn((x) => x);\n      const two = jest.fn((x) => x);\n      const three = jest.fn((x) => x);\n\n      thread(val, one, two, three);\n\n      expect(one.mock.results[0].value).toBe(val);\n      expect(two.mock.results[0].value).toBe(val);\n      expect(three.mock.results[0].value).toBe(val);\n    });\n  });\n\n  describe(\"addBom\", () => {\n    it(\"should add a byte order mark to input if set to true\", () => {\n      const config = mkConfig({ useBom: true });\n      const input = mkCsvOutput(\"test\");\n      const output = asString(addBOM(config)(input));\n\n      expect(output).toBe(input + byteOrderMark);\n    });\n\n    it(\"should return input unchanged if set to false\", () => {\n      const config = mkConfig({ useBom: false });\n      const input = mkCsvOutput(\"test\");\n      const output = addBOM(config)(input);\n\n      expect(output).toBe(input);\n    });\n  });\n\n  describe(\"addTitle\", () => {\n    it(\"should add title if set to true\", () => {\n      const config = mkConfig({ showTitle: true, title: \"My title\" });\n      const input = mkCsvOutput(\"\");\n      const output = asString(addTitle(config)(input));\n\n      expect(output).toBe(\"My title\\r\\n\");\n    });\n\n    it(\"should add use default if set to true without title given\", () => {\n      const config = mkConfig({ showTitle: true });\n      const input = mkCsvOutput(\"\");\n      const output = asString(addTitle(config)(input));\n\n      expect(output).toBe(\"My Generated Report\\r\\n\");\n    });\n\n    it(\"should skip title if set to false\", () => {\n      const config = mkConfig({ showTitle: false, title: \"My title\" });\n      const input = mkCsvOutput(\"\");\n      const output = asString(addTitle(config)(input));\n\n      expect(output).toBe(\"\");\n    });\n  });\n\n  describe(\"addEndOfLine\", () => {\n    it(\"should add new lines to end of row\", () => {\n      const csv = mkCsvOutput(\"\");\n      const input = mkCsvRow(\"test,one,two\");\n      const output = asString(addEndOfLine(csv)(input));\n\n      expect(output).toBe(unpack(csv) + unpack(input) + endOfLine);\n    });\n  });\n\n  describe(\"addFieldSeparator\", () => {\n    it(\"should add field separator to input with defaults\", () => {\n      const config = mkConfig({});\n      const input = mkCsvRow(\"test,one,two\");\n      const output = addFieldSeparator(config)(input);\n\n      expect(output).toBe(mkCsvRow(asString(input) + \",\"));\n    });\n\n    it(\"should add field separator to input based on config option\", () => {\n      const config = mkConfig({ fieldSeparator: \"|\" });\n      const input = mkCsvRow(\"test|one|two\");\n      const output = addFieldSeparator(config)(input);\n\n      expect(output).toBe(mkCsvRow(asString(input) + \"|\"));\n    });\n  });\n\n  describe(\"buildRow\", () => {\n    it(\"should add item to row\", () => {\n      const config = mkConfig({});\n      const input = mkCsvRow(\"test,one,two,\");\n      const output = asString(\n        buildRow(config)(input, mkFormattedData(\"house\")),\n      );\n\n      expect(output).toBe(\"test,one,two,house,\");\n    });\n  });\n\n  describe(\"addHeaders\", () => {\n    describe(\"throw error if showColumnHeaders true but headers empty\", () => {\n      it(\"should throw when useKeysAsHeaders is true\", () => {\n        const config = mkConfig({\n          showColumnHeaders: true,\n          useKeysAsHeaders: true,\n        });\n        expect(() => addHeaders(config, [])(mkCsvOutput(\"\"))).toThrow();\n      });\n\n      it(\"should throw when headers supplied but empty\", () => {\n        const config = mkConfig({\n          showColumnHeaders: true,\n          useKeysAsHeaders: false,\n        });\n        expect(() => addHeaders(config, [])(mkCsvOutput(\"\"))).toThrow();\n      });\n    });\n\n    describe(\"build headers based on input\", () => {\n      it(\"should retain order\", () => {\n        const config = mkConfig({\n          showColumnHeaders: true,\n        });\n        const nameAndDate = asString(\n          addHeaders(config, [\"name\", \"date\"])(mkCsvOutput(\"\")),\n        );\n        expect(nameAndDate).toEqual('\"name\",\"date\"' + endOfLine);\n\n        const dateAndCity = asString(\n          addHeaders(config, [\"date\", \"city\"])(mkCsvOutput(\"\")),\n        );\n        expect(dateAndCity).toEqual('\"date\",\"city\"' + endOfLine);\n      });\n    });\n\n    describe(\"pretty header mappings\", () => {\n      it(\"should allow columnHeaders to contain objects with a display label\", () => {\n        const config = mkConfig({\n          columnHeaders: [\"name\", { key: \"date\", displayLabel: \"Date\" }],\n        });\n        const nameAndDate = asString(\n          addHeaders(config, [\"name\", { key: \"date\", displayLabel: \"Date\" }])(\n            mkCsvOutput(\"\"),\n          ),\n        );\n        expect(nameAndDate).toEqual('\"name\",\"Date\"' + endOfLine);\n      });\n    });\n  });\n\n  describe(\"addBody\", () => {\n    it(\"should build csv body\", () => {\n      const config = mkConfig({});\n      const nameAndDate = asString(\n        addBody(\n          config,\n          [\"name\", \"date\"],\n          [{ name: \"rouky\", date: \"2023-09-02\" }],\n        )(mkCsvOutput(\"\")),\n      );\n      expect(nameAndDate).toEqual('\"rouky\",\"2023-09-02\"' + endOfLine);\n    });\n\n    it(\"should build csv body with pretty headers\", () => {\n      const config = mkConfig({});\n      const nameAndDate = asString(\n        addBody(\n          config,\n          [\"name\", { key: \"date\", displayLabel: \"Date\" }],\n          [{ name: \"rouky\", date: \"2023-09-02\" }],\n        )(mkCsvOutput(\"\")),\n      );\n      expect(nameAndDate).toEqual('\"rouky\",\"2023-09-02\"' + endOfLine);\n    });\n  });\n\n  describe(\"formatData\", () => {\n    it(\"should use locale string for decimals if config set\", () => {\n      const config = mkConfig({\n        decimalSeparator: \"locale\",\n      });\n      const formatted = formatData(config, 0.6);\n      expect(formatted).toEqual(mkFormattedData((0.6).toLocaleString()));\n    });\n\n    it(\"should use custom decimal separator if set\", () => {\n      const config = mkConfig({\n        decimalSeparator: \"|\",\n      });\n      const formatted = formatData(config, 0.6);\n      expect(formatted).toEqual(mkFormattedData(\"0|6\"));\n    });\n\n    it(\"should properly quote strings that may conflict with generation\", () => {\n      // Default case should quote stings\n      let config = mkConfig({});\n      const defaultQuote = formatData(config, \"test\");\n      expect(defaultQuote).toEqual(mkFormattedData('\"test\"'));\n\n      // Use custom quote strings\n      config = mkConfig({ quoteCharacter: \"^\" });\n      const customQuotes = formatData(config, \"test\");\n      expect(customQuotes).toEqual(mkFormattedData(\"^test^\"));\n\n      // Disable quoting strings\n      config = mkConfig({ quoteStrings: false });\n      const disableQuotes = formatData(config, \"test\");\n      expect(disableQuotes).toEqual(mkFormattedData(\"test\"));\n    });\n\n    describe(\"force quote problem characters\", () => {\n      it(\"should wrap problem data in quoteStrings\", () => {\n        // Quote field separator\n        let config = mkConfig({ quoteStrings: false });\n        const customQuote = formatData(config, \",\");\n        expect(customQuote).toEqual(mkFormattedData('\",\"'));\n\n        // Wrap new line\n        config = mkConfig({ quoteStrings: false });\n        const wrapNewLine = formatData(config, \"test\\n\");\n        expect(wrapNewLine).toEqual(mkFormattedData('\"test\\n\"'));\n\n        // Wrap carrage return\n        config = mkConfig({ quoteStrings: false });\n        const wrapCR = formatData(config, \"test\\r\");\n        expect(wrapCR).toEqual(mkFormattedData('\"test\\r\"'));\n\n        // Force quote with custom character\n        config = mkConfig({ quoteStrings: false, quoteCharacter: \"|\" });\n        const wrapCRWithCustom = formatData(config, \"test\\r\");\n        expect(wrapCRWithCustom).toEqual(mkFormattedData(\"|test\\r|\"));\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "lib/__specs__/main.spec.ts",
    "content": "import { describe, it, expect } from \"bun:test\";\nimport { mkConfig } from \"../config.ts\";\nimport { asBlob, generateCsv } from \"../generator.ts\";\nimport { ConfigOptions, MediaType } from \"../types.ts\";\nimport { asString } from \"../helpers.ts\";\n\nconst mockData = [\n  {\n    name: \"Test 1\",\n    age: 13,\n    average: 8.2,\n    approved: true,\n    description: \"Test 1 description\",\n    quotedNumber: \"01234\",\n  },\n  {\n    name: \"Test 2\",\n    age: 11,\n    average: 8.2,\n    approved: true,\n    description: \"Test 2 description\",\n    quotedNumber: \"05678\",\n  },\n  {\n    name: \"Test 4\",\n    age: 10,\n    average: 8.2,\n    approved: true,\n    description: \"Test 3 description\",\n    quotedNumber: \"09999\",\n  },\n];\n\ndescribe(\"ExportToCsv\", () => {\n  it(\"should create a comma seperated string\", () => {\n    const options: ConfigOptions = {\n      title: \"Test Csv\",\n      useBom: true,\n      useKeysAsHeaders: true,\n    };\n\n    const string = asString(generateCsv(options)(mockData));\n    expect(typeof string === \"string\").toBeTruthy();\n  });\n\n  it(\"should allow keys with spaces\", () => {\n    const mockDataOne = [\n      {\n        \"Hello world\": \"test\",\n        \"this is another string with many spaces\": 10,\n      },\n    ];\n\n    const optionsOne = mkConfig({ useBom: false, useKeysAsHeaders: true });\n    const stringOne = asString(generateCsv(optionsOne)(mockDataOne));\n    expect(stringOne).toEqual(\n      '\"Hello world\",\"this is another string with many spaces\"\\r\\n\"test\",10\\r\\n',\n    );\n\n    const mockDataTwo = [\n      {\n        \"Hello world\": \"test\",\n        \"this is another string with many spaces\": 10,\n      },\n    ];\n\n    const optionsTwo = mkConfig({\n      useBom: false,\n      showColumnHeaders: true,\n      columnHeaders: [\"Hello world\", \"this is another string with many spaces\"],\n    });\n    const stringTwo = asString(generateCsv(optionsTwo)(mockDataTwo));\n    expect(stringTwo).toEqual(\n      '\"Hello world\",\"this is another string with many spaces\"\\r\\n\"test\",10\\r\\n',\n    );\n  });\n\n  it(\"should use fieldSeparator if supplied\", () => {\n    const options: ConfigOptions = {\n      title: \"Test Csv\",\n      useBom: false,\n      useKeysAsHeaders: true,\n      fieldSeparator: \";\",\n    };\n\n    const string = asString(generateCsv(options)([{ test: \"hello\" }]));\n    expect(string).toEqual('\"test\"\\r\\n\"hello\"\\r\\n');\n  });\n\n  it(\"should use keys of first object in collection as headers\", () => {\n    const options: ConfigOptions = {\n      title: \"Test Csv\",\n      useBom: true,\n      useKeysAsHeaders: true,\n    };\n\n    const string = asString(generateCsv(options)(mockData));\n\n    const firstLine = string.split(\"\\n\")[0];\n    const keys = firstLine.split(\",\").map((s: string) => s.trim());\n\n    expect(keys).toEqual([\n      '\"name\"',\n      '\"age\"',\n      '\"average\"',\n      '\"approved\"',\n      '\"description\"',\n      '\"quotedNumber\"',\n    ]);\n  });\n\n  it(\"should retain order of headers when given as option\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: true,\n      showColumnHeaders: true,\n      columnHeaders: [\"name\", \"average\", \"age\", \"approved\", \"description\"],\n    };\n\n    const withDefaults = mkConfig(options);\n\n    const output = asString(generateCsv(withDefaults)(mockData));\n\n    const firstLine = output.split(\"\\n\")[0];\n    const keys = firstLine.split(\",\").map((s: string) => s.trim());\n\n    expect(keys).toEqual([\n      '\"name\"',\n      '\"average\"',\n      '\"age\"',\n      '\"approved\"',\n      '\"description\"',\n    ]);\n  });\n\n  it(\"should only use columns in columnHeaders\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: true,\n      showColumnHeaders: true,\n      columnHeaders: [\"name\", \"age\"],\n    };\n\n    const output = asString(generateCsv(options)(mockData));\n\n    const firstLine = output.split(\"\\n\")[0];\n    const keys = firstLine.split(\",\").map((s: string) => s.trim());\n\n    expect(keys).toEqual(['\"name\"', '\"age\"']);\n  });\n\n  it(\"should allow only headers to be generated\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      columnHeaders: [\"name\", \"age\"],\n    };\n\n    const output = asString(generateCsv(options)([]));\n\n    expect(output).toEqual('\"name\",\"age\"\\r\\n');\n  });\n\n  it(\"should throw when no data supplied\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: false,\n    };\n\n    expect(() => {\n      generateCsv(options)([]);\n    }).toThrow();\n  });\n\n  it(\"should allow null values\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      useKeysAsHeaders: true,\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          \"non-null\": 24,\n          nullish: null,\n        },\n      ]),\n    );\n\n    expect(output).toBe('\"non-null\",\"nullish\"\\r\\n24,\"null\"\\r\\n');\n  });\n\n  it(\"should convert undefined to empty string by default\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      useKeysAsHeaders: true,\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          car: \"toyota\",\n          color: \"blue\",\n        },\n        {\n          car: \"chevrolet\",\n        },\n      ]),\n    );\n\n    expect(output).toBe(\n      '\"car\",\"color\"\\r\\n\"toyota\",\"blue\"\\r\\n\"chevrolet\",\"\"\\r\\n',\n    );\n  });\n\n  it(\"should replace undefined with specified value\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      useKeysAsHeaders: true,\n      replaceUndefinedWith: \"TEST\",\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          car: \"toyota\",\n          color: \"blue\",\n        },\n        {\n          car: \"chevrolet\",\n        },\n      ]),\n    );\n\n    expect(output).toBe(\n      '\"car\",\"color\"\\r\\n\"toyota\",\"blue\"\\r\\n\"chevrolet\",\"TEST\"\\r\\n',\n    );\n  });\n\n  it(\"should handle varying data shapes by manually setting column headers\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      columnHeaders: [\"car\", \"color\", \"town\"],\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          car: \"toyota\",\n          color: \"blue\",\n        },\n        {\n          car: \"chevrolet\",\n        },\n        {\n          town: \"montreal\",\n        },\n      ]),\n    );\n\n    expect(output).toBe(\n      '\"car\",\"color\",\"town\"\\r\\n\"toyota\",\"blue\",\"\"\\r\\n\"chevrolet\",\"\",\"\"\\r\\n\"\",\"\",\"montreal\"\\r\\n',\n    );\n  });\n\n  it(\"should escape double quotes when quote is double quote\", () => {\n    const options: ConfigOptions = {\n      quoteCharacter: '\"',\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      useKeysAsHeaders: true,\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          \"escape-it\": 24,\n          song: 'Mack \"The Knife\"',\n        },\n      ]),\n    );\n\n    expect(output).toBe('\"escape-it\",\"song\"\\r\\n24,\"Mack \"\"The Knife\"\"\"\\r\\n');\n  });\n\n  it(\"should not escape double quotes when quote is not double quote\", () => {\n    const options: ConfigOptions = {\n      quoteCharacter: \"'\",\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      useKeysAsHeaders: true,\n    };\n\n    const output = asString(\n      generateCsv(options)([\n        {\n          \"escape-it\": 24,\n          song: 'Mack \"The Knife\"',\n        },\n      ]),\n    );\n\n    expect(output).toBe(\"'escape-it','song'\\r\\n24,'Mack \\\"The Knife\\\"'\\r\\n\");\n  });\n\n  it(\"should properly quote headers\", () => {\n    const options: ConfigOptions = {\n      filename: \"Test Csv 2\",\n      useBom: false,\n      showColumnHeaders: true,\n      columnHeaders: [\"name\", \"age\"],\n    };\n\n    const output = asString(generateCsv(options)(mockData));\n    const firstLine = output.split(\"\\n\")[0];\n\n    expect(firstLine).toBe('\"name\",\"age\"\\r');\n  });\n\n  it(\"should put the title on the first line\", () => {\n    const options: ConfigOptions = {\n      title: \"Test Csv 2\",\n      showTitle: true,\n      useBom: false,\n      showColumnHeaders: true,\n      columnHeaders: [\"name\", \"age\"],\n    };\n\n    const output = asString(generateCsv(options)(mockData));\n    const firstLine = output.split(\"\\n\")[0];\n\n    expect(firstLine).toBe(\"Test Csv 2\\r\");\n  });\n\n  it(\"should allow for custom file extensions\", () => {\n    const csvOpts: ConfigOptions = {\n      mediaType: MediaType.csv,\n    };\n    const csvConf = mkConfig(csvOpts);\n\n    const txtOpts: ConfigOptions = {\n      mediaType: MediaType.plain,\n    };\n    const txtConf = mkConfig(txtOpts);\n\n    const tsvOpts: ConfigOptions = {\n      mediaType: MediaType.tsv,\n    };\n    const tsvConf = mkConfig(tsvOpts);\n\n    expect(csvConf.mediaType).toBe(MediaType.csv);\n    expect(txtConf.mediaType).toBe(MediaType.plain);\n    expect(tsvConf.mediaType).toBe(MediaType.tsv);\n  });\n\n  describe(\"asBlob\", () => {\n    it(\"should construct a valid blob based on options\", async () => {\n      const options: ConfigOptions = {\n        title: \"Test Csv 2\",\n        showTitle: true,\n        useBom: false,\n        showColumnHeaders: true,\n        columnHeaders: [\"name\", \"age\"],\n      };\n\n      const output = generateCsv(options)(mockData);\n      const blob = asBlob(options)(output);\n      const text = await blob.text();\n\n      expect(blob.type).toBe(\"text/csv;charset=utf8;\");\n      expect(text.split(\"\\n\")[0]).toBe(\"Test Csv 2\\r\");\n      expect(blob.size).toBe(65);\n    });\n  });\n});\n"
  },
  {
    "path": "lib/config.ts",
    "content": "import { WithDefaults, ConfigOptions, MediaType } from \"./types.ts\";\n\nexport const defaults: WithDefaults<ConfigOptions> = {\n  fieldSeparator: \",\",\n  decimalSeparator: \".\",\n  quoteStrings: true,\n  quoteCharacter: '\"',\n  showTitle: false,\n  title: \"My Generated Report\",\n  filename: \"generated\",\n  showColumnHeaders: true,\n  useTextFile: false,\n  fileExtension: \"csv\",\n  mediaType: MediaType.csv,\n  useBom: true,\n  columnHeaders: [],\n  useKeysAsHeaders: false,\n  boolDisplay: { true: \"TRUE\", false: \"FALSE\" },\n  replaceUndefinedWith: \"\",\n};\n\nexport const endOfLine = \"\\r\\n\";\nexport const byteOrderMark = \"\\ufeff\";\n\nexport const mkConfig: (opts: ConfigOptions) => WithDefaults<ConfigOptions> = (\n  opts,\n) => Object.assign({}, defaults, opts);\n"
  },
  {
    "path": "lib/errors.ts",
    "content": "export class CsvGenerationError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"CsvGenerationError\";\n  }\n}\n\nexport class EmptyHeadersError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"EmptyHeadersError\";\n  }\n}\n\nexport class CsvDownloadEnvironmentError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"CsvDownloadEnvironmentError\";\n  }\n}\n\nexport class UnsupportedDataFormatError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"UnsupportedDataFormatError\";\n  }\n}\n"
  },
  {
    "path": "lib/generator.ts",
    "content": "// Required for `window` to work. Since `types` is set in `tsconfig.json`\n// `lib` no longer works\n/// <reference lib=\"dom\" />\n\nimport { mkConfig } from \"./config.ts\";\nimport { CsvDownloadEnvironmentError, CsvGenerationError } from \"./errors.ts\";\nimport { addBOM, addBody, addHeaders, addTitle, thread } from \"./helpers.ts\";\nimport {\n  CsvOutput,\n  ConfigOptions,\n  IO,\n  mkCsvOutput,\n  unpack,\n  AcceptedData,\n} from \"./types.ts\";\n\n/**\n *\n * Generates CsvOutput data from JSON collection using\n * ConfigOptions given.\n *\n * To comfortably use the data as a string around your\n * application, look at {@link asString}.\n *\n * @throws {CsvGenerationError | EmptyHeadersError}\n */\nexport const generateCsv =\n  (config: ConfigOptions) =>\n  <\n    T extends {\n      [k: string | number]: AcceptedData;\n    },\n  >(\n    data: Array<T>,\n  ): CsvOutput => {\n    const withDefaults = mkConfig(config);\n    const headers = withDefaults.useKeysAsHeaders\n      ? Object.keys(data[0])\n      : withDefaults.columnHeaders;\n\n    // Build csv output starting with an empty string\n    let output = thread(\n      mkCsvOutput(\"\"),\n      addBOM(withDefaults),\n      addTitle(withDefaults),\n      addHeaders(withDefaults, headers),\n      addBody(withDefaults, headers, data),\n    );\n\n    if (unpack(output).length < 1) {\n      throw new CsvGenerationError(\n        \"Output is empty. Is your data formatted correctly?\",\n      );\n    }\n\n    return output;\n  };\n\n/**\n * Returns the Blob representation of the CsvOutput generated\n * by `generateCsv`. This is useful if you need to access the\n * data for downloading in other contexts; like browser extensions.\n */\nexport const asBlob =\n  (config: ConfigOptions) =>\n  (csvOutput: CsvOutput): Blob => {\n    const withDefaults = mkConfig(config);\n    const data = unpack(csvOutput);\n\n    // Create blob from CsvOutput using the supplied mime type.\n    const mimeType = withDefaults.useTextFile\n      ? \"text/plain\"\n      : withDefaults.mediaType;\n    const blob = new Blob([data], {\n      type: `${mimeType};charset=utf8;`,\n    });\n\n    return blob;\n  };\n\n/**\n *\n * **Only supported in browser environment.**\n *\n * Will create a hidden anchor link in the page with the\n * download attribute set to a blob version of the CsvOutput data.\n *\n * @throws {CsvDownloadEnvironmentError}\n */\nexport const download =\n  (config: ConfigOptions) =>\n  (csvOutput: CsvOutput): IO => {\n    // Downloading is only supported in a browser environment.\n    // Node users can simply write the output from generateCsv\n    // to disk.\n    if (!window) {\n      throw new CsvDownloadEnvironmentError(\n        \"Downloading only supported in a browser environment.\",\n      );\n    }\n\n    // Create blob from CsvOutput either as text or csv file.\n    const blob = asBlob(config)(csvOutput);\n\n    const withDefaults = mkConfig(config);\n    const fileExtension = withDefaults.useTextFile\n      ? \"txt\"\n      : withDefaults.fileExtension;\n\n    const fileName = `${withDefaults.filename}.${fileExtension}`;\n\n    // Create link element in the browser and set the download\n    // attribute to the blob that was created.\n    const link = document.createElement(\"a\");\n    link.download = fileName;\n    link.href = URL.createObjectURL(blob);\n\n    // Ensure the link isn't visible to the user or cause layout shifts.\n    link.setAttribute(\"visibility\", \"hidden\");\n\n    // Add to document body, click and remove it.\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n  };\n"
  },
  {
    "path": "lib/helpers.ts",
    "content": "import { byteOrderMark, endOfLine } from \"./config.ts\";\nimport { EmptyHeadersError, UnsupportedDataFormatError } from \"./errors.ts\";\nimport {\n  AcceptedData,\n  ColumnHeader,\n  ConfigOptions,\n  CsvOutput,\n  CsvRow,\n  FormattedData,\n  HeaderDisplayLabel,\n  HeaderKey,\n  Newtype,\n  WithDefaults,\n  mkCsvOutput,\n  mkCsvRow,\n  mkFormattedData,\n  mkHeaderDisplayLabel,\n  mkHeaderKey,\n  pack,\n  unpack,\n} from \"./types.ts\";\n\nconst getHeaderKey = (columnHeader: ColumnHeader): HeaderKey =>\n  typeof columnHeader === \"object\"\n    ? mkHeaderKey(columnHeader.key)\n    : mkHeaderKey(columnHeader);\n\nconst getHeaderDisplayLabel = (\n  columnHeader: ColumnHeader,\n): HeaderDisplayLabel =>\n  typeof columnHeader === \"object\"\n    ? mkHeaderDisplayLabel(columnHeader.displayLabel)\n    : mkHeaderDisplayLabel(columnHeader);\n\nexport const thread = <T>(initialValue: T, ...fns: Array<Function>): T =>\n  fns.reduce((r, fn) => fn(r), initialValue);\n\nexport const addBOM =\n  (config: WithDefaults<ConfigOptions>) =>\n  (output: CsvOutput): CsvOutput =>\n    config.useBom ? mkCsvOutput(unpack(output) + byteOrderMark) : output;\n\nexport const addTitle =\n  (config: WithDefaults<ConfigOptions>) =>\n  (output: CsvOutput): CsvOutput =>\n    config.showTitle\n      ? addEndOfLine(mkCsvOutput(unpack(output) + config.title))(mkCsvRow(\"\"))\n      : output;\n\nexport const addEndOfLine =\n  (output: CsvOutput) =>\n  (row: CsvRow): CsvOutput =>\n    mkCsvOutput(unpack(output) + unpack(row) + endOfLine);\n\nexport const buildRow =\n  (config: WithDefaults<ConfigOptions>) =>\n  (row: CsvRow, data: FormattedData): CsvRow =>\n    addFieldSeparator(config)(mkCsvRow(unpack(row) + unpack(data)));\n\nexport const addFieldSeparator =\n  (config: WithDefaults<ConfigOptions>) =>\n  <T extends Newtype<any, string>>(output: T): T =>\n    pack<T>(unpack(output) + config.fieldSeparator);\n\nexport const addHeaders =\n  (config: WithDefaults<ConfigOptions>, headers: Array<ColumnHeader>) =>\n  (output: CsvOutput): CsvOutput => {\n    if (!config.showColumnHeaders) {\n      return output;\n    }\n\n    if (headers.length < 1) {\n      throw new EmptyHeadersError(\n        \"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.\",\n      );\n    }\n\n    let row = mkCsvRow(\"\");\n    for (let keyPos = 0; keyPos < headers.length; keyPos++) {\n      const header = getHeaderDisplayLabel(headers[keyPos]);\n      row = buildRow(config)(row, formatData(config, unpack(header)));\n    }\n\n    row = mkCsvRow(unpack(row).slice(0, -1));\n    return addEndOfLine(output)(row);\n  };\n\nexport const addBody =\n  <T extends Array<{ [k: string]: AcceptedData }>>(\n    config: WithDefaults<ConfigOptions>,\n    headers: Array<ColumnHeader>,\n    bodyData: T,\n  ) =>\n  (output: CsvOutput): CsvOutput => {\n    let body = output;\n    for (var i = 0; i < bodyData.length; i++) {\n      let row = mkCsvRow(\"\");\n      for (let keyPos = 0; keyPos < headers.length; keyPos++) {\n        const header = getHeaderKey(headers[keyPos]);\n        const data = bodyData[i][unpack(header)];\n        row = buildRow(config)(row, formatData(config, data));\n      }\n\n      // Remove trailing comma\n      row = mkCsvRow(unpack(row).slice(0, -1));\n      body = addEndOfLine(body)(row);\n    }\n\n    return body;\n  };\n\n/**\n *\n * Convert CsvOutput => string for the typechecker.\n *\n * Useful if you need to take the return value and\n * treat is as a string in the rest of your program.\n */\nexport const asString = unpack<Newtype<any, string>>;\n\nconst isFloat = (input: boolean | string | number): boolean =>\n  +input === input && (!isFinite(input) || Boolean(input % 1));\n\nconst formatNumber = (config: ConfigOptions, data: number): FormattedData => {\n  if (isFloat(data)) {\n    if (config.decimalSeparator === \"locale\") {\n      return mkFormattedData(data.toLocaleString());\n    }\n    if (config.decimalSeparator) {\n      return mkFormattedData(\n        data.toString().replace(\".\", config.decimalSeparator),\n      );\n    }\n  }\n\n  return mkFormattedData(data.toString());\n};\n\nconst formatString = (config: ConfigOptions, data: string): FormattedData => {\n  let val = data;\n  if (\n    config.quoteStrings ||\n    (config.fieldSeparator && data.indexOf(config.fieldSeparator) > -1) ||\n    (config.quoteCharacter && data.indexOf(config.quoteCharacter) > -1) ||\n    data.indexOf(\"\\n\") > -1 ||\n    data.indexOf(\"\\r\") > -1\n  ) {\n    val =\n      config.quoteCharacter +\n      escapeDoubleQuotes(data, config.quoteCharacter) +\n      config.quoteCharacter;\n  }\n  return mkFormattedData(val);\n};\n\nconst formatBoolean = (config: ConfigOptions, data: boolean): FormattedData => {\n  // Convert to string to use as lookup in config\n  const asStr = data ? \"true\" : \"false\";\n  // Return the custom boolean display. We expect the callsite to validate\n  // that `boolDisplay` is set.\n  return mkFormattedData(config.boolDisplay![asStr]);\n};\n\nconst formatNullish = (\n  config: ConfigOptions,\n  data: null | undefined,\n): FormattedData => {\n  if (\n    typeof data === \"undefined\" &&\n    config.replaceUndefinedWith !== undefined\n  ) {\n    // Coerce whatever was passed to a string\n    return formatString(config, config.replaceUndefinedWith + \"\");\n  }\n\n  if (data === null) {\n    return formatString(config, \"null\");\n  }\n\n  return formatString(config, \"\");\n};\n\nexport const formatData = (\n  config: ConfigOptions,\n  data: AcceptedData,\n): FormattedData => {\n  if (typeof data === \"number\") {\n    return formatNumber(config, data);\n  }\n\n  if (typeof data === \"string\") {\n    return formatString(config, data);\n  }\n\n  if (typeof data === \"boolean\" && config.boolDisplay) {\n    return formatBoolean(config, data);\n  }\n\n  if (data === null || typeof data === \"undefined\") {\n    return formatNullish(config, data);\n  }\n\n  throw new UnsupportedDataFormatError(\n    `\n    typeof ${typeof data} isn't supported. Only number, string, boolean, null and undefined are supported.\n    Please convert the data in your object to one of those before generating the CSV.\n    `,\n  );\n};\n\n/**\n * If double-quotes are used to enclose fields, then a double-quote\n * appearing inside a field must be escaped by preceding it with\n * another double quote.\n *\n * See https://www.rfc-editor.org/rfc/rfc4180\n */\nfunction escapeDoubleQuotes(data: string, quoteCharacter?: string): string {\n  if (quoteCharacter == '\"' && data.indexOf('\"') > -1) {\n    return data.replace(/\"/g, '\"\"');\n  }\n  return data;\n}\n"
  },
  {
    "path": "lib/index.ts",
    "content": "export * from \"./generator.ts\";\nexport { mkConfig } from \"./config.ts\";\nexport { CsvOutput, ConfigOptions, ColumnHeader, MediaType } from \"./types.ts\";\nexport { asString } from \"./helpers.ts\";\n"
  },
  {
    "path": "lib/types.ts",
    "content": "export type Newtype<URI, A> = {\n  readonly _URI: URI;\n  readonly _A: A;\n};\n\nexport type WithDefaults<T> = Required<T>;\n\nexport type ColumnHeader = string | { key: string; displayLabel: string };\n\nexport enum MediaType {\n  csv = \"text/csv\",\n  tsv = \"text/tab-separated-values\",\n  plain = \"text/plain\",\n}\n\nexport type ConfigOptions = {\n  filename?: string;\n  fieldSeparator?: string;\n  quoteStrings?: boolean;\n  quoteCharacter?: string;\n  decimalSeparator?: string;\n  showColumnHeaders?: boolean;\n  showTitle?: boolean;\n  title?: string;\n  /**\n   * Use `fileExtension` instead.\n   *\n   * Will be removed in the next major version (`2.x.x`)\n   *\n   * @deprecated\n   */\n  useTextFile?: boolean;\n  fileExtension?: string;\n  mediaType?: MediaType;\n  useBom?: boolean;\n  columnHeaders?: Array<ColumnHeader>;\n  useKeysAsHeaders?: boolean;\n  boolDisplay?: { true: string; false: string };\n  replaceUndefinedWith?: string | boolean | null;\n};\n\nexport type HeaderKey = Newtype<{ readonly HeaderKey: unique symbol }, string>;\n\nexport type HeaderDisplayLabel = Newtype<\n  { readonly HeaderDisplayLabel: unique symbol },\n  string\n>;\n\nexport type AcceptedData = number | string | boolean | null | undefined;\nexport type FormattedData = Newtype<\n  { readonly FormattedData: unique symbol },\n  string\n>;\n\nexport type CsvOutput = Newtype<{ readonly CsvOutput: unique symbol }, string>;\n\nexport type CsvRow = Newtype<{ readonly CsvRow: unique symbol }, string>;\n\nexport type IO = void;\n\nexport const pack = <T extends Newtype<any, any>>(value: T[\"_A\"]): T =>\n  value as any as T;\n\nexport const unpack = <T extends Newtype<any, any>>(newtype: T): T[\"_A\"] =>\n  newtype as any as T[\"_A\"];\n\nexport const mkFormattedData = pack<FormattedData>;\nexport const mkCsvOutput = pack<CsvOutput>;\nexport const mkCsvRow = pack<CsvRow>;\nexport const mkHeaderKey = pack<HeaderKey>;\nexport const mkHeaderDisplayLabel = pack<HeaderDisplayLabel>;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"export-to-csv\",\n  \"version\": \"1.4.0\",\n  \"description\": \"Easily create CSV data from json collection\",\n  \"type\": \"module\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/alexcaza/export-to-csv.git\"\n  },\n  \"scripts\": {\n    \"build\": \"rm -rf output && bun build index.ts --outdir ./output --minify && tsc\",\n    \"watch\": \"rm -rf output && bun build index.ts --outdir ./output --minify --watch\",\n    \"e2e\": \"rm -rf integration/export-to-csv.js && bun run build && cp output/index.js integration/export-to-csv.js && playwright test\",\n    \"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\",\n    \"e2e-server\": \"bunx http-server ./integration -p 3000\",\n    \"test\": \"bun test lib/__specs__/\",\n    \"format\": \"bunx prettier --write '.'\",\n    \"prepublishOnly\": \"bun run build\",\n    \"prepare\": \"bun run build\"\n  },\n  \"keywords\": [\n    \"export-to-csv\",\n    \"export-to-excel\",\n    \"csv\",\n    \"excel\",\n    \"libreoffice\",\n    \"openoffice\",\n    \"typescript\",\n    \"zero-dependencies\"\n  ],\n  \"author\": \"alexcaza\",\n  \"email\": \"alex@alexcaza.com\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/alexcaza/export-to-csv/issues\"\n  },\n  \"exports\": \"./output/index.js\",\n  \"types\": \"./output/index.d.ts\",\n  \"engines\": {\n    \"node\": \"^v12.20.0 || >=v14.13.0\"\n  },\n  \"homepage\": \"https://github.com/alexcaza/export-to-csv#readme\",\n  \"devDependencies\": {\n    \"@playwright/test\": \"1.50.1\",\n    \"bun-types\": \"~1.2.8\",\n    \"http-server\": \"^14.1.1\",\n    \"prettier\": \"~3.5.3\",\n    \"bun\": \"~1.2.8\",\n    \"typescript\": \"~5.8.2\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: \"./integration\",\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: \"html\",\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    // baseURL: 'http://127.0.0.1:3000',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: \"on-first-retry\",\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n\n    {\n      name: \"firefox\",\n      use: { ...devices[\"Desktop Firefox\"] },\n    },\n\n    {\n      name: \"webkit\",\n      use: { ...devices[\"Desktop Safari\"] },\n    },\n\n    /* Test against mobile viewports. */\n    // {\n    //   name: 'Mobile Chrome',\n    //   use: { ...devices['Pixel 5'] },\n    // },\n    // {\n    //   name: 'Mobile Safari',\n    //   use: { ...devices['iPhone 12'] },\n    // },\n\n    /* Test against branded browsers. */\n    // {\n    //   name: 'Microsoft Edge',\n    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },\n    // },\n    // {\n    //   name: 'Google Chrome',\n    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },\n    // },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: \"npm run e2e-server\",\n    url: \"http://127.0.0.1:3000\",\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"module\": \"node16\",\n    \"target\": \"esnext\",\n    \"declaration\": true,\n    \"types\": [\"bun-types\"],\n    \"lib\": [\"esnext\"],\n    \"moduleResolution\": \"node16\",\n    \"moduleDetection\": \"force\",\n    \"outDir\": \"./output\",\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"composite\": true,\n    \"allowImportingTsExtensions\": true,\n    \"emitDeclarationOnly\": true,\n    \"skipLibCheck\": true\n  },\n  \"files\": [\"index.ts\"],\n  \"include\": [\"lib/**/*\"],\n  \"exclude\": [\"node_modules\", \"__spec_\"]\n}\n"
  }
]