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 , 2016 as part of project Angular2Csv. All other copyright for project export-to-csv are held by Alex Caza , 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? Buy Me a Coffee at ko-fi.com 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 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 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 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 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 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 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 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` | **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 ================================================

Export to CSV testing

================================================ 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 = { 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 = ( 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 /// 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, ): 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 = (initialValue: T, ...fns: Array): T => fns.reduce((r, fn) => fn(r), initialValue); export const addBOM = (config: WithDefaults) => (output: CsvOutput): CsvOutput => config.useBom ? mkCsvOutput(unpack(output) + byteOrderMark) : output; export const addTitle = (config: WithDefaults) => (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) => (row: CsvRow, data: FormattedData): CsvRow => addFieldSeparator(config)(mkCsvRow(unpack(row) + unpack(data))); export const addFieldSeparator = (config: WithDefaults) => >(output: T): T => pack(unpack(output) + config.fieldSeparator); export const addHeaders = (config: WithDefaults, headers: Array) => (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 = >( config: WithDefaults, headers: Array, 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>; 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 = { readonly _URI: URI; readonly _A: A; }; export type WithDefaults = Required; 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; 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 = >(value: T["_A"]): T => value as any as T; export const unpack = >(newtype: T): T["_A"] => newtype as any as T["_A"]; export const mkFormattedData = pack; export const mkCsvOutput = pack; export const mkCsvRow = pack; export const mkHeaderKey = pack; export const mkHeaderDisplayLabel = pack; ================================================ 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_"] }