├── .cursor └── rules │ ├── code-style.mdc │ ├── documentation.mdc │ ├── error-handling.mdc │ ├── key-conventions.mdc │ ├── project-structure.mdc │ ├── readme.mdc │ ├── syntax-formatting.mdc │ ├── testing.mdc │ └── typescript.mdc ├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── SECURITY.md ├── art │ ├── cover.jpg │ └── og-image (14).png ├── renovate.json ├── stale.yml └── workflows │ ├── README.md │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .vscode ├── dictionary.txt ├── extensions.json └── settings.json ├── .zed └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── cli.ts ├── build.ts ├── bun.lock ├── bunfig.toml ├── docs ├── .vitepress │ ├── components.d.ts │ ├── config.ts │ ├── sw.ts │ ├── theme │ │ ├── components │ │ │ ├── Home.vue │ │ │ ├── HomeContributors.vue │ │ │ ├── HomeSponsors.vue │ │ │ ├── HomeTeam.vue │ │ │ ├── TeamMember.vue │ │ │ ├── contributors.json │ │ │ └── contributors.ts │ │ ├── index.ts │ │ └── styles │ │ │ ├── main.css │ │ │ ├── overrides.css │ │ │ └── vars.css │ ├── unocss.config.ts │ └── vite.config.ts ├── _data │ └── team.js ├── index.md ├── install.md ├── intro.md ├── license.md ├── partners.md ├── postcardware.md ├── public │ └── images │ │ ├── favicon-dark.svg │ │ ├── favicon.svg │ │ ├── logo-mini.svg │ │ ├── logo-transparent.svg │ │ ├── logo-white-transparent.svg │ │ ├── logo-white.png │ │ ├── logo.png │ │ ├── logo.svg │ │ └── og-image.png ├── showcase.md ├── sponsors.md ├── stargazers.md ├── team.md └── usage.md ├── eslint.config.ts ├── package.json ├── pkgx.yaml ├── spreadsheets ├── src ├── index.ts └── types.ts ├── test └── spreadsheets.test.ts └── tsconfig.json /.cursor/rules/code-style.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Code Style & Structure specifics 3 | globs: 4 | --- 5 | ## Code Style & Structure 6 | 7 | - Write concise, technical TypeScript code with accurate examples in the docblock 8 | - If Bun native modules are available, use them 9 | - Use functional and declarative programming patterns; avoid classes unless needed 10 | - Prefer iteration and modularization over code duplication 11 | - Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`) 12 | - Use proper jsdoc comments for functions, types, interfaces, and ensure examples are accurate 13 | -------------------------------------------------------------------------------- /.cursor/rules/documentation.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Documentation specific rules 3 | globs: docs/**/*.md 4 | --- 5 | ## Documentation 6 | 7 | - Write documentation for all functions, types, interfaces, and ensure examples are accurate 8 | - The `./docs` directory is where the vitepress markdown documentation is stored 9 | - Make sure to update the docs markdown files -------------------------------------------------------------------------------- /.cursor/rules/error-handling.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Error Handling and Validation specifics 3 | globs: 4 | --- 5 | ## Error Handling and Validation 6 | 7 | - Prioritize error handling: handle errors and edge cases early 8 | - Use early returns and guard clauses 9 | - Implement proper error logging and user-friendly messages 10 | - Use error boundaries for unexpected errors 11 | - when `neverthrow` is available, ensure errors are typed -------------------------------------------------------------------------------- /.cursor/rules/key-conventions.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Key code conventions 3 | globs: 4 | --- 5 | ## Key Conventions 6 | 7 | - If there are two equally valid implementations, the browser version should be preferred 8 | - Aim for 100% test coverage 9 | - Avoid usage of `any` 10 | - Reuse eslint-ignore comments where present, unless not needed 11 | - ensure we log everything properly, including for debug reasons -------------------------------------------------------------------------------- /.cursor/rules/project-structure.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Project structure information 3 | globs: 4 | --- 5 | ## Project Structure 6 | 7 | - the `./src` directory is the source code 8 | - the `./test` directory is the test code 9 | - the `./bin` directory is the command-line code 10 | - you can also call the CLI via `./clarity ...` 11 | - the `./docs` directory is the documentation 12 | -------------------------------------------------------------------------------- /.cursor/rules/readme.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: General information based on the latest ./README.md content 3 | globs: 4 | --- 5 | Update it if APIs change: 6 | 7 | # ts-spreadsheets 8 | 9 | Easily generate spreadsheets, like CSVs and Excel files. 10 | 11 | ## Features 12 | 13 | - Generate CSV & Excel files 14 | - Store spreadsheets to disk 15 | - Download spreadsheets as a Response object 16 | - Simple API for creating and manipulating spreadsheets 17 | - Performant & dependency-free 18 | - Library & CLI support 19 | - Fully typed 20 | 21 | ## Usage 22 | 23 | There are two ways to interact with `ts-spreadsheets`: via the CLI or as a library. 24 | 25 | ### Library 26 | 27 | As a library, you will want to make sure to install it in your project: 28 | 29 | ```bash 30 | bun install ts-spreadsheets 31 | ``` 32 | 33 | _Now, you can use the library in your project:_ 34 | 35 | ```ts 36 | import { createSpreadsheet, spreadsheet } from 'ts-spreadsheets' 37 | 38 | // Create a spreadsheet 39 | const data = { 40 | headings: ['Name', 'Age', 'City'], 41 | data: [ 42 | ['Chris Breuer', 30, 'Playa Vista'], 43 | ['Avery Hill', 25, 'Santa Monica'], 44 | ['Danny Johnson', 35, 'San Francisco'] 45 | ] 46 | } 47 | 48 | // Generate and manipulate spreadsheets 49 | 50 | // 1. Using createSpreadsheet function 51 | const spreadsheet = createSpreadsheet(data) // defaults to csv 52 | const csvSpreadsheet = createSpreadsheet(data, { type: 'csv' }) 53 | const excelSpreadsheet = createSpreadsheet(data, { type: 'excel' }) 54 | 55 | // Store the spreadsheet to disk 56 | await spreadsheet.store('output.csv') 57 | 58 | // Create a download response 59 | const response1 = excelSpreadsheet.download('data.xlsx') // downloads and stores as data.xlsx on your filesystem 60 | 61 | // 2. Using spreadsheet object directly, and chain if desired 62 | const csvContent = spreadsheet(data).generateCSV().store('output2.csv') 63 | const csvContent2 = spreadsheet(data).csv().store('output3.csv') // same as above 64 | 65 | const excelContent = spreadsheet(data).generateExcel() 66 | await excelContent.store('output3.xlsx') 67 | const response2 = await excelContent.download('output3.xlsx') // downloads and stores as output3.xlsx 68 | 69 | // 3. Accessing raw content 70 | const rawCsvContent = spreadsheet(data).csv().getContent() 71 | const rawCsvContent2 = spreadsheet(data).generateCSV().getContent() 72 | const rawExcelContent = spreadsheet(data).excel().getContent() 73 | const rawExcelContent2 = spreadsheet(data).generateExcel().getContent() 74 | 75 | console.log('CSV Content:', rawCsvContent) 76 | console.log('Excel Content:', rawExcelContent) 77 | ``` 78 | 79 | ### Main Functions 80 | 81 | #### spreadsheet(data: Content) 82 | 83 | Creates a spreadsheemethods. 84 | 85 | - `data`: An object containing `headings` and `data` for the spreadsheet. 86 | 87 | Returns an object with the following methods: 88 | 89 | - `csv()`: Generates a CSV SpreadsheetWrapper 90 | - `excel()`: Generates an Excel SpreadsheetWrapper 91 | - `store(path: string)`: Stores the spreadsheet to a file 92 | - `generateCSV()`: Generates a CSV SpreadsheetWrapper 93 | - `generateExcel()`: Generates an Excel SpreadsheetWrapper 94 | 95 | _Example:_ 96 | 97 | ```typescript 98 | const csvWrapper = await spreadsheet(data).csv() // equivalent to spreadsheet(data).generateCSV() 99 | ``` 100 | 101 | #### createSpreadsheet(data: Content, options?: SpreadsheetOptions) 102 | 103 | Creates a SpreadsheetWrapper with the given data and options. 104 | 105 | - `data`: An object containing `headings` and `data` for the spreadsheet. 106 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 107 | 108 | Returns a SpreadsheetWrapper. 109 | 110 | _Example:_ 111 | 112 | ```typescript 113 | const spreadsheet = createSpreadsheet(data, { type: 'csv' }) 114 | ``` 115 | 116 | ### SpreadsheetWrapper Methods 117 | 118 | #### getContent() 119 | 120 | Returns the content of the spreadsheet as a string or Uint8Array. 121 | 122 | #### download(filename: string) 123 | 124 | Creates a download Response for the spreadsheet. 125 | 126 | - `filename`: The name of the file to be downloaded. 127 | 128 | Returns a Response object. 129 | 130 | #### store(path: string) 131 | 132 | Stores the spreadsheet to a file. 133 | 134 | - `path`: The file path where the spreadsheet will be stored. 135 | 136 | Returns a Promise that resolves when the file is written. 137 | 138 | ### Utility Functions 139 | 140 | #### spreadsheet.create(data: Content, options?: SpreadsheetOptions) 141 | 142 | Creates a SpreadsheetContent object. 143 | 144 | - `data`: An object containing `headings` and `data` for the spreadsheet. 145 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 146 | 147 | Returns a SpreadsheetContent object. 148 | 149 | #### spreadsheet.generate(data: Content, options?: SpreadsheetOptions) 150 | 151 | Generates spreadsheet content based on the given data and o An object containing `headings` and `data` for the spreadsheet. 152 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 153 | 154 | Returns a string or Uint8Array representing the spreadsheet content. 155 | 156 | #### spreadsheet.generateCSV(content: Content) 157 | 158 | Generates a CSV SpreadsheetWrapper. 159 | 160 | - `content`: An object containing `headings` and `data` for the spreadsheet. 161 | 162 | Returns a SpreadsheetWrapper for CSV, which can be used to chain other methods like `store()` or `download()`. 163 | 164 | _Example:_ 165 | 166 | ```typescript 167 | await spreadsheet(data).generateCSV().store('output.csv') 168 | 169 | // if one can rely on the file extension to determine the type, you may do this: 170 | await spreadsheet(data).store('output.csv') 171 | ``` 172 | 173 | #### spreadsheet.generateExcel(content: Content) 174 | 175 | Generates an Excel SpreadsheetWrapper. 176 | 177 | - `content`: An object containing `headings` and `data` for the spreadsheet. 178 | 179 | Returns a SpreadsheetWrapper for Excel, which can be used to chain other methods like `store()` or `download()`. 180 | 181 | _Example:_ 182 | 183 | ```ts 184 | await spreadsheet(data).store('output.xlsx') 185 | // or 186 | await spreadsheet(data).generateExcel().store('output.xlsx') 187 | ``` 188 | 189 | To view the full documentation, please visit [https://ts-spreadsheets.netlify.app](mdc:https:/ts-spreadsheets.netlify.app). 190 | 191 | ### CLI 192 | 193 | You can also use the CLI to generate spreadsheets from JSON input: 194 | 195 | ```bash 196 | # Create a spreadsheet from JSON input 197 | spreadsheets create data.json -o output.csv 198 | spreadsheets create data.json --type excel -o output.xlsx 199 | 200 | # Convert between formats 201 | spreadsheets convert input.csv output.xlsx 202 | spreadsheets convert input.xlsx output.csv 203 | 204 | # Validate JSON input format 205 | spreadsheets validate input.json 206 | ``` 207 | 208 | The input json should follow this format: 209 | 210 | ```json 211 | { 212 | "headings": ["Name", "Age", "City"], 213 | "data": [ 214 | ["Chris Breuer", 30, "Playa Vista"], 215 | ["Avery Hill", 25, "Santa Monica"], 216 | ["Danny Johnson", 35, "San Francisco"] 217 | ] 218 | } 219 | ``` 220 | 221 | #### CLI Commands 222 | 223 | - `create`: Generate a spreadsheet from JSON input 224 | - Options: 225 | - `-t, --type `: Output type ('csv' or 'excel'), defaults to 'csv' 226 | - `-o, --output `: Output file path 227 | - `convert`: Convert between spreadsheet formats 228 | - Automatically detects format from file extensions 229 | - Supports conversion between CSV and Excel formats 230 | - `validate`: Check if JSON input meets the required format 231 | - Validates structure and data types 232 | - Provides helpful error messages for invalid input 233 | 234 | All commands support the `--help` flag for more information: 235 | 236 | ```bash 237 | spreadsheets --help 238 | spreadsheets create --help 239 | spreadsheets convert --help 240 | spreadsheets validate --help 241 | ``` -------------------------------------------------------------------------------- /.cursor/rules/syntax-formatting.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Syntax and Formatting specifics 3 | globs: 4 | --- 5 | ## Syntax and Formatting 6 | 7 | - Use the "function" keyword for pure functions 8 | - Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements 9 | - Make sure everything is properly commented -------------------------------------------------------------------------------- /.cursor/rules/testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Testing specifics 3 | globs: 4 | --- 5 | ## Testing 6 | 7 | - Write tests for all functions, types, interfaces, and ensure examples are accurate 8 | - The `./test` directory is where the tests are stored 9 | - Use `bun test` to run the tests 10 | - Use bun native modules for testing from `import { x, y, z } from 'bun:test'` -------------------------------------------------------------------------------- /.cursor/rules/typescript.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: TypeScript Usage specifics 3 | globs: docs/**/*.md 4 | --- 5 | ## TypeScript Usage 6 | 7 | - Use TypeScript for all code; prefer interfaces over types 8 | - Avoid enums; use `maps` instead, or `as const` 9 | - Use functional components with TypeScript interfaces -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | * Trolling, insulting or derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others' private information, such as a physical or email address, without their explicit permission 25 | * Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at chris@meema.xyz. All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 74 | 75 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 76 | 77 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. 78 | 79 | [homepage]: https://www.contributor-covenant.org 80 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 81 | [Mozilla CoC]: https://github.com/mozilla/diversity 82 | [FAQ]: https://www.contributor-covenant.org/faq 83 | [translations]: https://www.contributor-covenant.org/translations 84 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thank you for taking the time to contribute to the Stacks ecosystem ❤️ 4 | 5 | > **Note** 6 | > The likelihood is high that the repo you are working on is either a Stack or Stacks itself. In both cases, you will be exposed to a meshup of technologies, like [Vue][vue], [Vite][vite], [Tauri][tauri], [Nitro][nitro], and [Bun][bun]. 7 | 8 | ## 💭 Knowledge 9 | 10 | ### TypeScript 11 | 12 | It's important to note early on that these projects are written with [TypeScript][typescript]. If you're unfamiliar with it (or any strongly typed languages such as Java) then this may feel like a slight roadblock. However, there's never a truly perfect time to start learning it, so ... why not today using well-written codebases as your playground? 13 | 14 | _Don't be discouraged. You will get by learning TypeScript on-the-fly as you review some of the examples within the codebase. It's easy to get started—the code is, we hope, very approachable (and readable)._ 15 | 16 | ### Architecture 17 | 18 | An understanding of the framework architecture and design will help if you're looking to contribute long-term, or if you are working on a "more complex" PR. Browse the source and read our documentation to get a better sense of how it is structured. The documentation is very thorough and can be used as your progressive guide as you're learning more about Stacks. 19 | 20 | Feel free to ask any question _(Twitter, Discord, or GitHub Discussions)_, we would love to elaborate & collaborate. 21 | 22 | ### Stacks/Core Setup 23 | 24 | Are you interested in contributing to the Stacks codebase? 25 | 26 | **Working on your first Pull Request?** You can learn how from this free series [How to Contribute to an Open Source Project on GitHub][pr-beginner-series]. 27 | 28 | Head over to the [repository][stacks] on GitHub and click the Fork button in the top right corner. After the project has been forked, run the following commands in your terminal: 29 | 30 | ```bash 31 | # Replace {github-username} with your GitHub username. 32 | git clone https://github.com/{github-username}/stacks --depth=1 33 | 34 | cd stacks 35 | 36 | # Create a branch for your PR, replace {issue-no} with the GitHub issue number. 37 | git checkout -b issue-{issue-no} 38 | ``` 39 | 40 | Now it'll help if we keep our `main` branch pointing at the original repository and make 41 | pull requests from the forked branch. 42 | 43 | ```bash 44 | # Add the original repository as a "remote" called "upstream". 45 | git remote add upstream git@github.com:stacksjs/stacks.git 46 | 47 | # Fetch the git information from the remote. 48 | git fetch upstream 49 | 50 | # Set your local main branch to use the upstream main branch whenever you run `git pull`. 51 | git branch --set-upstream-to=upstream/main main 52 | 53 | # Run this when we want to update our version of main. 54 | git pull 55 | ``` 56 | 57 | _You may also use GitHub Desktop or any other GUI—if that is your preference._ 58 | 59 | ## 🧪 Testing 60 | 61 | Tests are stored within the `./tests` project folder. You can run the tests using the following command: 62 | 63 | ```bash 64 | bun run test 65 | ``` 66 | 67 | Please make sure tests are added for all added & changed functionalties. 68 | 69 | ## ✍️ Commit 70 | 71 | Stacks uses [semantic commit messages][semantic-commit-style] to automate package releases. No worries, you may not be aware what this is or how it works—just let Buddy guide you. Stacks automated the commit process for you, simply run `buddy commit` in your terminal and follow the instructions. 72 | 73 | For example, 74 | 75 | ```bash 76 | # Add all changes to staging to be committed. 77 | git add . 78 | 79 | # Commit changes. 80 | buddy commit 81 | 82 | # Push changes up to GitHub. 83 | git push 84 | ``` 85 | 86 | _By following these minor steps, Stacks is able to automatically release new versions & generate relating local & remote changelogs._ 87 | 88 | ## 🎉 Pull Request 89 | 90 | When you're all done, head over to the [repository][stacks], and click the big green `Compare & Pull Request` button that should appear after you've pushed changes to your fork. 91 | 92 | Don't expect your PR to be accepted immediately or even accepted at all. Give the community time to vet it and see if it should be merged. Please don't be disheartened if it's not accepted. Your contribution is appreciated more than you can imagine, and even a unmerged PR can teach us a lot ❤️ 93 | 94 | [typescript]: https://www.typescriptlang.org 95 | [vue]: https://vuejs.org/ 96 | [vite]: https://vitejs.dev/ 97 | [tauri]: https://tauri.app/ 98 | [nitro]: https://nitro.unjs.io/ 99 | [bun]: https://bun.sh/ 100 | [stacks]: https://github.com/stacksjs/stacks 101 | [semantic-commit-style]: https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716 102 | [pr-beginner-series]: https://app.egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github 103 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [stacksjs, chrisbbreuer] 2 | open_collective: stacksjs 3 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** 4 | 5 | ## Supported Versions 6 | 7 | Only the latest major version receives security fixes. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover a security vulnerability within this package, please send an email to Chris Breuer at chris@meema.xyz. All security vulnerabilities will be promptly addressed. 12 | 13 | ### Public PGP Key 14 | 15 | ``` 16 | -----BEGIN PGP PUBLIC KEY BLOCK----- 17 | Version: OpenPGP v2.0.8 18 | Comment: https://sela.io/pgp/ 19 | 20 | mQINBGEO6uYBEACw8ldEmdK0xR2RjeGnAyNQItT83JG1BQmByttddyikolGHY0w1 21 | MLCSNAwveUT4f5vbDU41sH8QQDda+NBNIWNo+xtFahfWxi3gYpX0xltgPrYkuBIr 22 | P3b6Hz8KsZ5IvGhP4wXI9LA9x8IUjIDMiObx3LyL2MirgF4kHyHwBX444kcsfo3I 23 | 6wk/kfcZ2lY63tIplYrkp3znTxRX3lJyroOkEpCVHyajftw41K+WEKstWVAKvxpc 24 | nHg6TW91AyWCS6TLrsmhdnWYfA9lSULlxbH/NQ0HEYRLb+NoTVGWv5y6WC2OFoJO 25 | SvCae1GOqUIdbW4AC3/lQsqI+i2/nyZvaD5xu+HUrB/qN0d4iw2X+6pj+wsO7XQj 26 | x5qbcIZBmNoUfBkjZH8+ZgH6Kit+0qBMMY8afLjngxCCwrlvfRGmEiC8ehNLP7a5 27 | BjDFbjPBjyjLuZskIerNzHHkJ6XUTQQ8LNfzS32xu8AsF+IknQ/1QuZIfSoRLQdJ 28 | q7s+5hydM0Mtryg8VHL0AN/sXo70EWEl1YgDLEF4iu5cMWWFXlesDmR9wdhDMi8G 29 | S28MRyxx0yitmrEt2WJoGa7D8l9bsVw4ntN5ZP3rd0P67H+lC5FcFqSzFJtxHXLQ 30 | 1JZOv/P7AZ6Ps8mb9gLzgMnwmPXBu07AExJutJQaj4U24hJ4Ko3+D9RQ+QARAQAB 31 | tB1DaHJpcyBCcmV1ZXIgPGNocmlzQG1lZW1hLmlvPokCVAQTAQgAPhYhBHLTi9Xr 32 | 0tFrh0WzUUaA85gSbnQlBQJhDurmAhsDBQkHhh8zBQsJCAcCBhUKCQgLAgQWAgMB 33 | Ah4BAheAAAoJEEaA85gSbnQlhXAQAK+LLp53dQLoYlwOH/L4XQfY+AVtZXhQwg2+ 34 | gSR7tNP8i+XDvw7OA8UeQ9CKSlIarK/jnynzT91WiwWskGr+DeVR0enuG3CFEW/q 35 | X3o0WH8MjSNhJEFQ6Mo2foAMPOO97Fl7R5vyhEhSXIocnGLdAngxP5sYtOuY32c+ 36 | Bu2z72ChIvpGXh2j44ThHs5xsoq+O5OZg5x2xTaMCyndzpgJTSDlAldnzd0wxbtC 37 | OlSvsgmSWdXls/5pZbE7gny6OuxFo5zxpHEcJnWW//e0cZXKgW4Ps3aNzSPmMKDl 38 | va0Mg2toP9H6z+k9c8H0UZm0KKvKBZi9Bvxcvdc5yLcOeR+Rom1YYNcBsxfJc62Q 39 | 6JbaZvDwN3e0RFgitwEyo3Danimp53v1DXbrNfd78FrskES10cX89lBXubSyPpSc 40 | JP1i8IPcooDi8yHw3zAms6qnrEWYFIxCqN8id9gsLxfzwVCRXvUqDhXmzMcZZB2E 41 | wiHP97bq9chlWTQuCkDXrbzHD1SMkaOjbFiVo+w18jNsXdEhHvZKnUQzv0560w2x 42 | DM8NBARGNupmIOc9e4uy5pJIZp4fhKvpGqqih7PpHKFCo8eC+/HgsJh17MpzFTqQ 43 | 5hPaCPCc5fnX/GIGdj3Ax6bATX5fAceEGexvjThpP8tKIPWAWbQFjHnnIay0f/nL 44 | wRmWWqeQuQINBGEO6uYBEADLLrKBJ+4VWmGWlylsUmwRoFmwu/GZokCL60jKWtOu 45 | i2JK9JhptL+MNvqFoGChVs+Okx9CYscEGOxnK38frb+H6FrlOXsABFQmg2DBWjkW 46 | 9VYkXmQ0M9c/ciMj8m55sh4y6E8ITZ4OuNoLF3ElmKWANU29Z2fW+C8Q7OHiawfU 47 | XJ2UwCUVymQntWrtPCSgBLbgh71l/TSTLdwbwGVFWtxQvO7TXeP+nUNNWRG/UMeT 48 | PSHQ7ANMnllkQNsQtuS/Lkcs/BSM+70g0LvZ88loAU80bxV6XCx7vaKKWV19Lxob 49 | 7tu/d7k/kvDq+sGpjPmv0mZCury0F3bk7VHVQ6DKVIt/3R16qUBmGKwECVXDAb2H 50 | zebDcTzMvvICD3fXV5Ye9kCNAeQfMVEXMHf0H14wB1AAr2WAfGyl+g2xwqNRp7DK 51 | Da2JigDvGA14iLwrDFxdpHYIJfMReRngEX6i28WB2GewaIsDGxqsqO0jdwnlJush 52 | 0USUnlN4iwQABM+xqJnfX0wZTVXjpw1Thgh1E/0MSceJF3PgZ0CDX9MIZ/azbqsU 53 | tg06F8KxJcwvKbBdp9jTeN0TRSMPlonyAfZblRVyj0/gPcwlBIB/IajwFPCi4eQ+ 54 | /to/kuVe5dnoDVqrNJ2o7sSNi3xEUc7o02RyJhemCrsnPpYyXFmr0ku7c/J347L1 55 | xQARAQABiQI8BBgBCAAmFiEEctOL1evS0WuHRbNRRoDzmBJudCUFAmEO6uYCGwwF 56 | CQeGHzMACgkQRoDzmBJudCXg/g//VUscqD0h28WYBBffWJb+AAj7T+NNTNcH3I+u 57 | BHcOsvmdH/HSayTHvntqUnV4oVCiAo4U/0xlopJpU45OxPV7vjx66yWAXrwApSJs 58 | BIAa4P/GK2V8q008nP37ha36IHKB11LWZsnKh7/zFOXJ1XlX6FuqvFZkcJNJePCU 59 | sg0RbjlAkRUL7gOFeBktZXGS4cmAzhpUAdDSdZnzVtDpjY4jUswLVn3JZ07CDZx+ 60 | 5RRCZKqbT/+2UgwDDe2f+gmoNCrGmaHfHCrk3S0DYBxR/BBMmWnQe2YiM+eHufB9 61 | MIApvuEgEp0RX68Za/NEdht8vm4LLeZdNxwSG+BgW8vPQRsgT1V+43aNatt5jbHD 62 | hUC5CksIt+i5gy7R9my1xdQ0lqB4jYLcbtBHz0A7E9d9j5kRaGLX3fTr6pOb9KxJ 63 | Ek+KrMLBPp7g4fkn6qUr3xCt1Ss+sDUegHby5PM1ddvs/lbYhZOjq6+7gPvtFkF8 64 | OcFaR3o0xMRuoSk4/zkge4eeND+XR7+2xvA9G9vDBJ7wV8bbxbEnp7PEFWnZVqDR 65 | Lo2polLYC3wvFQl14tyT3OoDH+mkCPcD+GbDwYbWpcb+v6uCkquqAcHTrbYhwhxY 66 | kXSnpSzMVde7LbHMHiVr0Ubl3k4+1uNiKhY7CLW9pLJwJ4mUmG2VX3YPfG4shnYR 67 | HF/6SiI= 68 | =le/X 69 | -----END PGP PUBLIC KEY BLOCK----- 70 | ``` 71 | -------------------------------------------------------------------------------- /.github/art/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacksjs/ts-spreadsheets/296d1546035ea9a9b08596b0c0da2641c55fca06/.github/art/cover.jpg -------------------------------------------------------------------------------- /.github/art/og-image (14).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacksjs/ts-spreadsheets/296d1546035ea9a9b08596b0c0da2641c55fca06/.github/art/og-image (14).png -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>ow3org/renovate-config" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - pinned 5 | - security 6 | - no-stale 7 | - no stale 8 | - pr welcome 9 | staleLabel: stale 10 | markComment: > 11 | This issue has been automatically marked as stale because it has not had 12 | recent activity. It will be closed if no further activity occurs. 13 | Thank you for your contributions. 14 | closeComment: false 15 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions 2 | 3 | This folder contains the following GitHub Actions: 4 | 5 | - [CI][CI] - all CI jobs for the project 6 | - lints the code 7 | - `typecheck`s the code 8 | - runs test suite 9 | - runs on `ubuntu-latest` 10 | - [Release][Release] - automates the release process & changelog generation 11 | 12 | [CI]: ./workflows/ci.yml 13 | [Release]: ./workflows/release.yml 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Install Bun 24 | uses: oven-sh/setup-bun@v2 25 | 26 | - name: Use cached node_modules 27 | uses: actions/cache@v4 28 | with: 29 | path: node_modules 30 | key: node-modules-${{ hashFiles('**/bun.lock') }} 31 | restore-keys: | 32 | node-modules- 33 | 34 | - name: Install Dependencies 35 | run: bun install 36 | 37 | - name: Lint 38 | run: bun run lint 39 | 40 | typecheck: 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - name: Install Bun 47 | uses: oven-sh/setup-bun@v2 48 | 49 | - name: Use cached node_modules 50 | uses: actions/cache@v4 51 | with: 52 | path: node_modules 53 | key: node-modules-${{ hashFiles('**/bun.lock') }} 54 | restore-keys: | 55 | node-modules- 56 | 57 | - name: Install Dependencies 58 | run: bun install 59 | 60 | - name: Typecheck 61 | run: bun --bun run typecheck 62 | 63 | test: 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | 69 | - name: Install Bun 70 | uses: oven-sh/setup-bun@v2 71 | 72 | - name: Use cached node_modules 73 | uses: actions/cache@v4 74 | with: 75 | path: node_modules 76 | key: node-modules-${{ hashFiles('**/bun.lock') }} 77 | restore-keys: | 78 | node-modules- 79 | 80 | - name: Install Dependencies 81 | run: bun install 82 | 83 | - name: Unit Test 84 | run: bun test 85 | 86 | publish-commit: 87 | runs-on: ubuntu-latest 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | 92 | - name: Install Bun 93 | uses: oven-sh/setup-bun@v2 94 | 95 | - name: Use cached node_modules 96 | uses: actions/cache@v4 97 | with: 98 | path: node_modules 99 | key: node-modules-${{ hashFiles('**/bun.lock') }} 100 | restore-keys: | 101 | node-modules- 102 | 103 | - name: Install Dependencies 104 | run: bun install 105 | 106 | - name: Build 107 | run: bun run build 108 | 109 | - name: Publish Commit 110 | run: bunx pkg-pr-new publish 111 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Bun 19 | uses: oven-sh/setup-bun@v2 20 | 21 | - name: Use Cached node_modules 22 | uses: actions/cache@v4 23 | with: 24 | path: node_modules 25 | key: node-modules-${{ hashFiles('**/bun.lock') }} 26 | restore-keys: | 27 | node-modules- 28 | 29 | - name: Install Dependencies 30 | run: bun install 31 | 32 | - name: Publish To npm 33 | run: bun publish --access public 34 | env: 35 | BUN_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 36 | 37 | - name: Create GitHub Release 38 | run: bunx changelogithub 39 | env: 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | 42 | - name: Attach Binaries 43 | uses: softprops/action-gh-release@v2 44 | with: 45 | files: | 46 | bin/spreadsheets-linux-x64 47 | bin/spreadsheets-linux-arm64 48 | bin/spreadsheets-windows-x64.exe 49 | bin/spreadsheets-darwin-x64 50 | bin/spreadsheets-darwin-arm64 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | docs/.vitepress/cache 13 | bin/spreadsheets 14 | invalid.json 15 | -------------------------------------------------------------------------------- /.vscode/dictionary.txt: -------------------------------------------------------------------------------- 1 | antfu 2 | audiox 3 | biomejs 4 | booleanish 5 | bumpp 6 | bunfig 7 | bunx 8 | changelogen 9 | changelogithub 10 | codecov 11 | commitlint 12 | commitlintrc 13 | composables 14 | davidanson 15 | dbaeumer 16 | degit 17 | deps 18 | destructurable 19 | dnsx 20 | dtsx 21 | entrypoints 22 | heroicons 23 | httx 24 | iconify 25 | imgx 26 | lockb 27 | openweb 28 | outdir 29 | pausable 30 | Postcardware 31 | prefetch 32 | preinstall 33 | quickfix 34 | shikijs 35 | shoutout 36 | socio 37 | softprops 38 | Solana 39 | Spatie 40 | sponsorware 41 | stacksjs 42 | tlsx 43 | twoslash 44 | typecheck 45 | unconfig 46 | unocss 47 | unplugin 48 | unref 49 | upath 50 | vidx 51 | vite 52 | vitebook 53 | vitejs 54 | vue-demi 55 | vueus 56 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "davidanson.vscode-markdownlint", 5 | "streetsidesoftware.code-spell-checker" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "biome.enabled": false, 4 | "editor.formatOnSave": false, 5 | 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit", 8 | "source.organizeImports": "never" 9 | }, 10 | 11 | "eslint.rules.customizations": [ 12 | { "rule": "style/*", "severity": "off", "fixable": true }, 13 | { "rule": "format/*", "severity": "off", "fixable": true }, 14 | { "rule": "*-indent", "severity": "off", "fixable": true }, 15 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 16 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 17 | { "rule": "*-order", "severity": "off", "fixable": true }, 18 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 19 | { "rule": "*-newline", "severity": "off", "fixable": true }, 20 | { "rule": "*quotes", "severity": "off", "fixable": true }, 21 | { "rule": "*semi", "severity": "off", "fixable": true } 22 | ], 23 | 24 | "eslint.validate": [ 25 | "javascript", 26 | "typescript", 27 | "vue", 28 | "html", 29 | "markdown", 30 | "json", 31 | "jsonc", 32 | "yaml", 33 | "toml", 34 | "xml", 35 | "css", 36 | "less", 37 | "scss", 38 | "pcss", 39 | "postcss" 40 | ], 41 | 42 | "typescript.tsdk": "${workspaceFolder}/node_modules/typescript/lib", 43 | 44 | "[shellscript]": { 45 | "editor.defaultFormatter": "foxundermoon.shell-format" 46 | }, 47 | 48 | "[markdown]": { 49 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", 50 | "editor.formatOnSave": true 51 | }, 52 | 53 | "[dockerfile]": { 54 | "editor.defaultFormatter": "foxundermoon.shell-format" 55 | }, 56 | 57 | "typescript.preferGoToSourceDefinition": true, 58 | "files.associations": { 59 | "buddy": "typescript", 60 | "*.stx": "vue" 61 | }, 62 | "editor.quickSuggestions": { 63 | "strings": true 64 | }, 65 | "vsicons.associations.files": [ 66 | { 67 | "icon": "${workspaceFolder}/public/favicon.svg", 68 | "extensions": ["stx"], 69 | "format": "svg" 70 | } 71 | ], 72 | "git.enableSmartCommit": true, 73 | "npm.enableRunFromFolder": true, 74 | "npm.packageManager": "bun", 75 | "editor.gotoLocation.multipleDefinitions": "goto", 76 | "search.exclude": { 77 | "**/node_modules": true, 78 | "**/cdk.out": true, 79 | "**/dist": true, 80 | "**/storage/public": true, 81 | "CHANGELOG.md": true 82 | }, 83 | "explorer.confirmDragAndDrop": false, 84 | "todo-tree.highlights.enabled": true, 85 | "cSpell.ignorePaths": [ 86 | "node_modules" 87 | ], 88 | "cSpell.dictionaries": [ 89 | "custom-dictionary" 90 | ], 91 | "cSpell.diagnosticLevel": "Hint", 92 | "cSpell.customDictionaries": { 93 | "stacks": { 94 | "name": "custom-dictionary", 95 | "path": "./.vscode/dictionary.txt", 96 | "scope": "user", 97 | "addWords": true 98 | }, 99 | "custom": true 100 | }, 101 | "terminal.integrated.scrollback": 10000, 102 | "grammarly.files.include": [ 103 | "**/README.md", 104 | "**/readme.md", 105 | "**/*.txt" 106 | ], 107 | "grammarly.files.exclude": [ 108 | "**/dictionary.txt" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // For a full list of overridable settings, and general information on folder-specific settings, 2 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 3 | { 4 | "languages": { 5 | "JavaScript": { 6 | "formatter": { 7 | "code_actions": { 8 | "source.fixAll.eslint": true 9 | } 10 | } 11 | }, 12 | "TypeScript": { 13 | "formatter": { 14 | "code_actions": { 15 | "source.fixAll.eslint": true 16 | } 17 | } 18 | }, 19 | "HTML": { 20 | "formatter": { 21 | "code_actions": { 22 | "source.fixAll.eslint": true 23 | } 24 | } 25 | }, 26 | "CSS": { 27 | "formatter": { 28 | "code_actions": { 29 | "source.fixAll.eslint": true 30 | } 31 | } 32 | }, 33 | "Markdown": { 34 | "formatter": { 35 | "code_actions": { 36 | "source.fixAll.eslint": true 37 | } 38 | } 39 | }, 40 | "JSON": { 41 | "formatter": { 42 | "code_actions": { 43 | "source.fixAll.eslint": true 44 | } 45 | } 46 | }, 47 | "JSONC": { 48 | "formatter": { 49 | "code_actions": { 50 | "source.fixAll.eslint": true 51 | } 52 | } 53 | }, 54 | "YAML": { 55 | "formatter": { 56 | "code_actions": { 57 | "source.fixAll.eslint": true 58 | } 59 | } 60 | }, 61 | "XML": { 62 | "formatter": { 63 | "code_actions": { 64 | "source.fixAll.eslint": true 65 | } 66 | } 67 | }, 68 | "TOML": { 69 | "formatter": { 70 | "code_actions": { 71 | "source.fixAll.eslint": true 72 | } 73 | } 74 | } 75 | }, 76 | "lsp": { 77 | "eslint": { 78 | "settings": { 79 | "rulesCustomizations": [ 80 | { 81 | "rule": "style/*", 82 | "severity": "off", 83 | "fixable": true 84 | }, 85 | { 86 | "rule": "format/*", 87 | "severity": "off", 88 | "fixable": true 89 | }, 90 | { 91 | "rule": "*-indent", 92 | "severity": "off", 93 | "fixable": true 94 | }, 95 | { 96 | "rule": "*-spacing", 97 | "severity": "off", 98 | "fixable": true 99 | }, 100 | { 101 | "rule": "*-spaces", 102 | "severity": "off", 103 | "fixable": true 104 | }, 105 | { 106 | "rule": "*-order", 107 | "severity": "off", 108 | "fixable": true 109 | }, 110 | { 111 | "rule": "*-dangle", 112 | "severity": "off", 113 | "fixable": true 114 | }, 115 | { 116 | "rule": "*-newline", 117 | "severity": "off", 118 | "fixable": true 119 | }, 120 | { 121 | "rule": "*quotes", 122 | "severity": "off", 123 | "fixable": true 124 | }, 125 | { 126 | "rule": "*semi", 127 | "severity": "off", 128 | "fixable": true 129 | } 130 | ] 131 | } 132 | } 133 | }, 134 | "file_types": { 135 | "JavaScript": [ 136 | "buddy" 137 | ] 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## v0.1.2...main 3 | 4 | [compare changes](https://github.com/stacksjs/ts-spreadsheets/compare/v0.1.2...main) 5 | 6 | ### 🚀 Enhancements 7 | 8 | - Create node compatibility ([eca2390](https://github.com/stacksjs/ts-spreadsheets/commit/eca2390)) 9 | - Add cli & binaries ([ddb5577](https://github.com/stacksjs/ts-spreadsheets/commit/ddb5577)) 10 | 11 | ### 📖 Documentation 12 | 13 | - Added documentation ([1bf9f7d](https://github.com/stacksjs/ts-spreadsheets/commit/1bf9f7d)) 14 | - Hard code contributors link ([dfeb93c](https://github.com/stacksjs/ts-spreadsheets/commit/dfeb93c)) 15 | 16 | ### 🏡 Chore 17 | 18 | - Use text lock file ([b3e8ecd](https://github.com/stacksjs/ts-spreadsheets/commit/b3e8ecd)) 19 | - Several adjustments ([0a4f2c2](https://github.com/stacksjs/ts-spreadsheets/commit/0a4f2c2)) 20 | - Re-set unconfig version ([96146a1](https://github.com/stacksjs/ts-spreadsheets/commit/96146a1)) 21 | - Adjust readme ([e7b1bc8](https://github.com/stacksjs/ts-spreadsheets/commit/e7b1bc8)) 22 | 23 | ### ❤️ Contributors 24 | 25 | - Chris ([@chrisbbreuer](http://github.com/chrisbbreuer)) 26 | - Cab-mikee ([@cab-mikee](http://github.com/cab-mikee)) 27 | 28 | ## v0.1.1...main 29 | 30 | [compare changes](https://github.com/stacksjs/bun-spreadsheets/compare/v0.1.1...main) 31 | 32 | ### 🏡 Chore 33 | 34 | - Resolve lint issues ([b834312](https://github.com/stacksjs/bun-spreadsheets/commit/b834312)) 35 | - Resolve release flow ([7394a7e](https://github.com/stacksjs/bun-spreadsheets/commit/7394a7e)) 36 | 37 | ### ❤️ Contributors 38 | 39 | - Chris ([@chrisbbreuer](http://github.com/chrisbbreuer)) 40 | 41 | ## v0.1.0...main 42 | 43 | [compare changes](https://github.com/stacksjs/bun-spreadsheets/compare/v0.1.0...main) 44 | 45 | ### 🏡 Chore 46 | 47 | - Use better readme examples ([bf435e8](https://github.com/stacksjs/bun-spreadsheets/commit/bf435e8)) 48 | - Use eslint ([47e6d6c](https://github.com/stacksjs/bun-spreadsheets/commit/47e6d6c)) 49 | - Housecleaning ([27dcbba](https://github.com/stacksjs/bun-spreadsheets/commit/27dcbba)) 50 | 51 | ### ❤️ Contributors 52 | 53 | - Chris ([@chrisbbreuer](http://github.com/chrisbbreuer)) 54 | 55 | ## ...main 56 | 57 | 58 | ### 🏡 Chore 59 | 60 | - Initial commit ([6a51579](https://github.com/stacksjs/ts-starter/commit/6a51579)) 61 | - Adjust readme links ([363287c](https://github.com/stacksjs/ts-starter/commit/363287c)) 62 | - Several minor adjustments ([c615067](https://github.com/stacksjs/ts-starter/commit/c615067)) 63 | - Update `bun-plugin-dts-auto` ([517f5f7](https://github.com/stacksjs/ts-starter/commit/517f5f7)) 64 | - Use bun publish ([d1d254b](https://github.com/stacksjs/ts-starter/commit/d1d254b)) 65 | - Minify build & include sourcemap ([8080a31](https://github.com/stacksjs/ts-starter/commit/8080a31)) 66 | - Adjust image url for proper remote display ([a71562d](https://github.com/stacksjs/ts-starter/commit/a71562d)) 67 | 68 | ### ❤️ Contributors 69 | 70 | - Chris 71 | 72 | ## v0.3.1...main 73 | 74 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.1...main) 75 | 76 | ### 🚀 Enhancements 77 | 78 | - Add `bun-plugin-dts-auto` ([c0c487b](https://github.com/stacksjs/ts-starter/commit/c0c487b)) 79 | 80 | ### ❤️ Contributors 81 | 82 | - Chris 83 | 84 | ## v0.3.0...main 85 | 86 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.0...main) 87 | 88 | ### 🏡 Chore 89 | 90 | - Fix isolatedDeclarations setting ([b87b6b1](https://github.com/stacksjs/ts-starter/commit/b87b6b1)) 91 | - Adjust urls ([0a40b72](https://github.com/stacksjs/ts-starter/commit/0a40b72)) 92 | 93 | ### ❤️ Contributors 94 | 95 | - Chris 96 | 97 | ## v0.2.1...main 98 | 99 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.1...main) 100 | 101 | ### 🚀 Enhancements 102 | 103 | - Add `noFallthroughCasesInSwitch` ([b9cfa30](https://github.com/stacksjs/ts-starter/commit/b9cfa30)) 104 | - Add `verbatimModuleSyntax` ([c495d17](https://github.com/stacksjs/ts-starter/commit/c495d17)) 105 | - Several updates ([f703179](https://github.com/stacksjs/ts-starter/commit/f703179)) 106 | 107 | ### 🩹 Fixes 108 | 109 | - Properly use bun types ([7144221](https://github.com/stacksjs/ts-starter/commit/7144221)) 110 | 111 | ### 🏡 Chore 112 | 113 | - Adjust badge links ([432aff7](https://github.com/stacksjs/ts-starter/commit/432aff7)) 114 | - Add `runs-on` options ([9a5b97f](https://github.com/stacksjs/ts-starter/commit/9a5b97f)) 115 | - Cache node_modules ([ba2f6ce](https://github.com/stacksjs/ts-starter/commit/ba2f6ce)) 116 | - Use `ubuntu-latest` for now ([1add684](https://github.com/stacksjs/ts-starter/commit/1add684)) 117 | - Minor updates ([1007cff](https://github.com/stacksjs/ts-starter/commit/1007cff)) 118 | - Lint ([d531bdc](https://github.com/stacksjs/ts-starter/commit/d531bdc)) 119 | - Remove bunx usage ([e1a5575](https://github.com/stacksjs/ts-starter/commit/e1a5575)) 120 | - Pass bun flag ([960976f](https://github.com/stacksjs/ts-starter/commit/960976f)) 121 | - Use defaults ([157455b](https://github.com/stacksjs/ts-starter/commit/157455b)) 122 | - Run typecheck using bun flag ([f22f3b1](https://github.com/stacksjs/ts-starter/commit/f22f3b1)) 123 | - Test ([0b3c3a1](https://github.com/stacksjs/ts-starter/commit/0b3c3a1)) 124 | - Use modern js for commitlint ([4bd6978](https://github.com/stacksjs/ts-starter/commit/4bd6978)) 125 | - Update worklows readme ([f54aae9](https://github.com/stacksjs/ts-starter/commit/f54aae9)) 126 | - Adjust readme ([92d7ff1](https://github.com/stacksjs/ts-starter/commit/92d7ff1)) 127 | - More updates ([0225587](https://github.com/stacksjs/ts-starter/commit/0225587)) 128 | - Add .zed settings for biome ([1688024](https://github.com/stacksjs/ts-starter/commit/1688024)) 129 | - Extend via alias ([b108d30](https://github.com/stacksjs/ts-starter/commit/b108d30)) 130 | - Lint ([d961b2a](https://github.com/stacksjs/ts-starter/commit/d961b2a)) 131 | - Minor updates ([e66d44a](https://github.com/stacksjs/ts-starter/commit/e66d44a)) 132 | 133 | ### ❤️ Contributors 134 | 135 | - Chris 136 | 137 | ## v0.2.0...main 138 | 139 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.0...main) 140 | 141 | ### 🏡 Chore 142 | 143 | - Remove unused action ([066f85a](https://github.com/stacksjs/ts-starter/commit/066f85a)) 144 | - Housekeeping ([fc4e24d](https://github.com/stacksjs/ts-starter/commit/fc4e24d)) 145 | 146 | ### ❤️ Contributors 147 | 148 | - Chris 149 | 150 | ## v0.1.1...main 151 | 152 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.1...main) 153 | 154 | ### 🏡 Chore 155 | 156 | - Adjust eslint config name ([53c2aa6](https://github.com/stacksjs/ts-starter/commit/53c2aa6)) 157 | - Set type module ([22dde14](https://github.com/stacksjs/ts-starter/commit/22dde14)) 158 | 159 | ### ❤️ Contributors 160 | 161 | - Chris 162 | 163 | ## v0.1.0...main 164 | 165 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.0...main) 166 | 167 | ### 🏡 Chore 168 | 169 | - Use correct cover image ([75bd3ae](https://github.com/stacksjs/ts-starter/commit/75bd3ae)) 170 | 171 | ### ❤️ Contributors 172 | 173 | - Chris 174 | 175 | ## v0.0.5...main 176 | 177 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main) 178 | 179 | ### 🚀 Enhancements 180 | 181 | - Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066)) 182 | - Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093)) 183 | 184 | ### 🏡 Chore 185 | 186 | - Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000)) 187 | - Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522)) 188 | - Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b)) 189 | - Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625)) 190 | - Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c)) 191 | - Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42)) 192 | - Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3)) 193 | - Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2)) 194 | 195 | ### ❤️ Contributors 196 | 197 | - Chris 198 | 199 | ## v0.0.5...main 200 | 201 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main) 202 | 203 | ### 🚀 Enhancements 204 | 205 | - Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066)) 206 | - Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093)) 207 | 208 | ### 🏡 Chore 209 | 210 | - Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000)) 211 | - Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522)) 212 | - Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b)) 213 | - Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625)) 214 | - Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c)) 215 | - Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42)) 216 | - Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3)) 217 | - Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2)) 218 | 219 | ### ❤️ Contributors 220 | 221 | - Chris 222 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Open Web Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Social Card of this Bun Spreadsheets repo

2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![GitHub Actions][github-actions-src]][github-actions-href] 5 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 6 | 7 | 8 | 9 | # ts-spreadsheets 10 | 11 | Easily generate spreadsheets, like CSVs and Excel files. 12 | 13 | ## Features 14 | 15 | - Generate CSV & Excel files 16 | - Store spreadsheets to disk 17 | - Download spreadsheets as a Response object 18 | - Simple API for creating and manipulating spreadsheets 19 | - Performant & dependency-free 20 | - Library & CLI support 21 | - Fully typed 22 | 23 | ## Usage 24 | 25 | There are two ways to interact with `ts-spreadsheets`: via the CLI or as a library. 26 | 27 | ### Library 28 | 29 | As a library, you will want to make sure to install it in your project: 30 | 31 | ```bash 32 | bun install ts-spreadsheets 33 | ``` 34 | 35 | _Now, you can use the library in your project:_ 36 | 37 | ```ts 38 | import { createSpreadsheet, spreadsheet } from 'ts-spreadsheets' 39 | 40 | // Create a spreadsheet 41 | const data = { 42 | headings: ['Name', 'Age', 'City'], 43 | data: [ 44 | ['Chris Breuer', 30, 'Playa Vista'], 45 | ['Avery Hill', 25, 'Santa Monica'], 46 | ['Danny Johnson', 35, 'San Francisco'] 47 | ] 48 | } 49 | 50 | // Generate and manipulate spreadsheets 51 | 52 | // 1. Using createSpreadsheet function 53 | const spreadsheet = createSpreadsheet(data) // defaults to csv 54 | const csvSpreadsheet = createSpreadsheet(data, { type: 'csv' }) 55 | const excelSpreadsheet = createSpreadsheet(data, { type: 'excel' }) 56 | 57 | // Store the spreadsheet to disk 58 | await spreadsheet.store('output.csv') 59 | 60 | // Create a download response 61 | const response1 = excelSpreadsheet.download('data.xlsx') // downloads and stores as data.xlsx on your filesystem 62 | 63 | // 2. Using spreadsheet object directly, and chain if desired 64 | const csvContent = spreadsheet(data).generateCSV().store('output2.csv') 65 | const csvContent2 = spreadsheet(data).csv().store('output3.csv') // same as above 66 | 67 | const excelContent = spreadsheet(data).generateExcel() 68 | await excelContent.store('output3.xlsx') 69 | const response2 = await excelContent.download('output3.xlsx') // downloads and stores as output3.xlsx 70 | 71 | // 3. Accessing raw content 72 | const rawCsvContent = spreadsheet(data).csv().getContent() 73 | const rawCsvContent2 = spreadsheet(data).generateCSV().getContent() 74 | const rawExcelContent = spreadsheet(data).excel().getContent() 75 | const rawExcelContent2 = spreadsheet(data).generateExcel().getContent() 76 | 77 | console.log('CSV Content:', rawCsvContent) 78 | console.log('Excel Content:', rawExcelContent) 79 | ``` 80 | 81 | ### Main Functions 82 | 83 | #### spreadsheet(data: Content) 84 | 85 | Creates a spreadsheet object with various methods. 86 | 87 | - `data`: An object containing `headings` and `data` for the spreadsheet. 88 | 89 | Returns an object with the following methods: 90 | 91 | - `csv()`: Generates a CSV SpreadsheetWrapper 92 | - `excel()`: Generates an Excel SpreadsheetWrapper 93 | - `store(path: string)`: Stores the spreadsheet to a file 94 | - `generateCSV()`: Generates a CSV SpreadsheetWrapper 95 | - `generateExcel()`: Generates an Excel SpreadsheetWrapper 96 | 97 | _Example:_ 98 | 99 | ```typescript 100 | const csvWrapper = await spreadsheet(data).csv() // equivalent to spreadsheet(data).generateCSV() 101 | ``` 102 | 103 | #### createSpreadsheet(data: Content, options?: SpreadsheetOptions) 104 | 105 | Creates a SpreadsheetWrapper with the given data and options. 106 | 107 | - `data`: An object containing `headings` and `data` for the spreadsheet. 108 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 109 | 110 | Returns a SpreadsheetWrapper. 111 | 112 | _Example:_ 113 | 114 | ```typescript 115 | const spreadsheet = createSpreadsheet(data, { type: 'csv' }) 116 | ``` 117 | 118 | ### SpreadsheetWrapper Methods 119 | 120 | #### getContent() 121 | 122 | Returns the content of the spreadsheet as a string or Uint8Array. 123 | 124 | #### download(filename: string) 125 | 126 | Creates a download Response for the spreadsheet. 127 | 128 | - `filename`: The name of the file to be downloaded. 129 | 130 | Returns a Response object. 131 | 132 | #### store(path: string) 133 | 134 | Stores the spreadsheet to a file. 135 | 136 | - `path`: The file path where the spreadsheet will be stored. 137 | 138 | Returns a Promise that resolves when the file is written. 139 | 140 | ### Utility Functions 141 | 142 | #### spreadsheet.create(data: Content, options?: SpreadsheetOptions) 143 | 144 | Creates a SpreadsheetContent object. 145 | 146 | - `data`: An object containing `headings` and `data` for the spreadsheet. 147 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 148 | 149 | Returns a SpreadsheetContent object. 150 | 151 | #### spreadsheet.generate(data: Content, options?: SpreadsheetOptions) 152 | 153 | Generates spreadsheet content based on the given data and options. 154 | 155 | - `data`: An object containing `headings` and `data` for the spreadsheet. 156 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 157 | 158 | Returns a string or Uint8Array representing the spreadsheet content. 159 | 160 | #### spreadsheet.generateCSV(content: Content) 161 | 162 | Generates a CSV SpreadsheetWrapper. 163 | 164 | - `content`: An object containing `headings` and `data` for the spreadsheet. 165 | 166 | Returns a SpreadsheetWrapper for CSV, which can be used to chain other methods like `store()` or `download()`. 167 | 168 | _Example:_ 169 | 170 | ```typescript 171 | await spreadsheet(data).generateCSV().store('output.csv') 172 | 173 | // if one can rely on the file extension to determine the type, you may do this: 174 | await spreadsheet(data).store('output.csv') 175 | ``` 176 | 177 | #### spreadsheet.generateExcel(content: Content) 178 | 179 | Generates an Excel SpreadsheetWrapper. 180 | 181 | - `content`: An object containing `headings` and `data` for the spreadsheet. 182 | 183 | Returns a SpreadsheetWrapper for Excel, which can be used to chain other methods like `store()` or `download()`. 184 | 185 | _Example:_ 186 | 187 | ```ts 188 | await spreadsheet(data).store('output.xlsx') 189 | // or 190 | await spreadsheet(data).generateExcel().store('output.xlsx') 191 | ``` 192 | 193 | To view the full documentation, please visit [https://ts-spreadsheets.netlify.app](https://ts-spreadsheets.netlify.app). 194 | 195 | ### CLI 196 | 197 | You can also use the CLI to generate spreadsheets from JSON input: 198 | 199 | ```bash 200 | # Create a spreadsheet from JSON input 201 | spreadsheets create data.json -o output.csv 202 | spreadsheets create data.json --type excel -o output.xlsx 203 | 204 | # Convert between formats 205 | spreadsheets convert input.csv output.xlsx 206 | spreadsheets convert input.xlsx output.csv 207 | 208 | # Validate JSON input format 209 | spreadsheets validate input.json 210 | ``` 211 | 212 | The input json should follow this format: 213 | 214 | ```json 215 | { 216 | "headings": ["Name", "Age", "City"], 217 | "data": [ 218 | ["Chris Breuer", 30, "Playa Vista"], 219 | ["Avery Hill", 25, "Santa Monica"], 220 | ["Danny Johnson", 35, "San Francisco"] 221 | ] 222 | } 223 | ``` 224 | 225 | #### CLI Commands 226 | 227 | - `create`: Generate a spreadsheet from JSON input 228 | - Options: 229 | - `-t, --type `: Output type ('csv' or 'excel'), defaults to 'csv' 230 | - `-o, --output `: Output file path 231 | - `convert`: Convert between spreadsheet formats 232 | - Automatically detects format from file extensions 233 | - Supports conversion between CSV and Excel formats 234 | - `validate`: Check if JSON input meets the required format 235 | - Validates structure and data types 236 | - Provides helpful error messages for invalid input 237 | 238 | All commands support the `--help` flag for more information: 239 | 240 | ```bash 241 | spreadsheets --help 242 | spreadsheets create --help 243 | spreadsheets convert --help 244 | spreadsheets validate --help 245 | ``` 246 | 247 | ## Testing 248 | 249 | ```bash 250 | bun test 251 | ``` 252 | 253 | ## Changelog 254 | 255 | Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently. 256 | 257 | ## Contributing 258 | 259 | Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details. 260 | 261 | ## Community 262 | 263 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 264 | 265 | [Discussions on GitHub](https://github.com/stacksjs/stacks/discussions) 266 | 267 | For casual chit-chat with others using this package: 268 | 269 | [Join the Stacks Discord Server](https://discord.gg/stacksjs) 270 | 271 | ## Postcardware 272 | 273 | “Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where `ts-spreadsheets` is being used! We showcase them on our website too. 274 | 275 | Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎 276 | 277 | ## Sponsors 278 | 279 | We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us. 280 | 281 | - [JetBrains](https://www.jetbrains.com/) 282 | - [The Solana Foundation](https://solana.com/) 283 | 284 | ## Credits 285 | 286 | - [Chris Breuer](https://github.com/chrisbbreuer) 287 | - [All Contributors](https://github.com/stacksjs/ts-spreadsheets/contributors) 288 | 289 | ## License 290 | 291 | The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/ts-spreadsheets/tree/main/LICENSE.md) for more information. 292 | 293 | Made with 💙 294 | 295 | 296 | [npm-version-src]: https://img.shields.io/npm/v/ts-spreadsheets?style=flat-square 297 | [npm-version-href]: https://npmjs.com/package/ts-spreadsheets 298 | [github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/ts-spreadsheets/ci.yml?style=flat-square&branch=main 299 | [github-actions-href]: https://github.com/stacksjs/ts-spreadsheets/actions?query=workflow%3Aci 300 | 301 | 303 | -------------------------------------------------------------------------------- /bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | import { readFileSync } from 'node:fs' 3 | import { readFile } from 'node:fs/promises' 4 | import { resolve } from 'node:path' 5 | import process from 'node:process' 6 | import { CAC } from 'cac' 7 | import { type Content, createSpreadsheet, csvToContent } from '../src/index' 8 | 9 | // Use sync version for version to avoid race conditions 10 | const version = process.env.NODE_ENV === 'test' 11 | ? '0.0.0' 12 | : JSON.parse( 13 | readFileSync(resolve(__dirname, '../package.json'), 'utf-8'), 14 | ).version 15 | 16 | interface CLIOptions { 17 | type?: 'csv' | 'excel' 18 | output?: string 19 | pretty?: boolean 20 | } 21 | 22 | const cli = new CAC('spreadsheets') 23 | 24 | function logMessage(message: string) { 25 | if (process.env.NODE_ENV === 'test') { 26 | process.stderr.write(`${message}\n`) 27 | } 28 | else { 29 | console.log(message) 30 | } 31 | } 32 | 33 | cli 34 | .command('create ', 'Create a spreadsheet from JSON input file') 35 | .option('-t, --type ', 'Output type (csv or excel)', { default: 'csv' }) 36 | .option('-o, --output ', 'Output file path') 37 | .action(async (input: string, options: CLIOptions) => { 38 | try { 39 | const inputPath = resolve(process.cwd(), input) 40 | const content = JSON.parse(await readFile(inputPath, 'utf-8')) as Content 41 | 42 | const result = createSpreadsheet(content, { type: options.type }) 43 | 44 | if (options.output) { 45 | await result.store(options.output) 46 | logMessage(`Spreadsheet saved to ${options.output}`) 47 | } 48 | else { 49 | const output = result.getContent() 50 | if (typeof output === 'string') { 51 | process.stdout.write(output) 52 | } 53 | else { 54 | process.stdout.write(output) 55 | } 56 | } 57 | } 58 | catch (error) { 59 | logMessage(`Failed to create spreadsheet: ${(error as Error).message}`) 60 | process.exit(1) 61 | } 62 | }) 63 | 64 | cli 65 | .command('convert ', 'Convert between spreadsheet formats') 66 | .action(async (input: string, output: string) => { 67 | try { 68 | const inputExt = input.slice(input.lastIndexOf('.')) as '.csv' | '.xlsx' 69 | const outputExt = output.slice(output.lastIndexOf('.')) as '.csv' | '.xlsx' 70 | 71 | if (inputExt === outputExt) { 72 | logMessage('Input and output formats are the same') 73 | return 74 | } 75 | 76 | // Handle CSV input 77 | let content: Content 78 | if (inputExt === '.csv') { 79 | content = await csvToContent(input) 80 | } 81 | else { 82 | // Handle JSON input 83 | content = JSON.parse(await readFile(input, 'utf-8')) as Content 84 | } 85 | 86 | const outputType = outputExt === '.csv' ? 'csv' : 'excel' 87 | const result = createSpreadsheet(content, { type: outputType }) 88 | await result.store(output) 89 | 90 | logMessage(`Converted ${input} to ${output}`) 91 | } 92 | catch (error) { 93 | logMessage(`Failed to convert spreadsheet: ${(error as Error).message}`) 94 | process.exit(1) 95 | } 96 | }) 97 | 98 | cli 99 | .command('validate ', 'Validate JSON input format') 100 | .action(async (input: string) => { 101 | try { 102 | const content = JSON.parse(await readFile(input, 'utf-8')) 103 | 104 | if (!content.headings || !Array.isArray(content.headings)) { 105 | throw new Error('Missing or invalid headings array') 106 | } 107 | if (!content.data || !Array.isArray(content.data)) { 108 | throw new Error('Missing or invalid data array') 109 | } 110 | 111 | const invalidHeadings = content.headings.some((h: unknown) => typeof h !== 'string') 112 | if (invalidHeadings) { 113 | throw new Error('Headings must be strings') 114 | } 115 | 116 | const invalidData = content.data.some((row: unknown[]) => 117 | !Array.isArray(row) 118 | || row.some((cell: unknown) => typeof cell !== 'string' && typeof cell !== 'number'), 119 | ) 120 | if (invalidData) { 121 | throw new Error('Data must be an array of arrays containing only strings and numbers') 122 | } 123 | 124 | logMessage('Input JSON is valid') 125 | } 126 | catch (error) { 127 | logMessage(`Invalid input: ${(error as Error).message}`) 128 | process.exit(1) 129 | } 130 | }) 131 | 132 | cli.version(version) 133 | cli.help() 134 | cli.parse() 135 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { dts } from 'bun-plugin-dtsx' 3 | 4 | console.log('Building...') 5 | 6 | const result = await Bun.build({ 7 | entrypoints: ['./src/index.ts', './bin/cli.ts'], 8 | outdir: './dist', 9 | format: 'esm', 10 | target: 'node', 11 | minify: true, 12 | splitting: true, 13 | plugins: [ 14 | dts(), 15 | ], 16 | }) 17 | 18 | if (!result.success) { 19 | console.error('Build failed') 20 | 21 | for (const message of result.logs) { 22 | // Bun will pretty print the message object 23 | console.error(message) 24 | } 25 | 26 | process.exit(1) 27 | } 28 | 29 | console.log('Build complete') 30 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | registry = { url = "https://registry.npmjs.org/", token = "$BUN_AUTH_TOKEN" } 3 | peer = true 4 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Home: typeof import('./theme/components/Home.vue')['default'] 11 | HomeContributors: typeof import('./theme/components/HomeContributors.vue')['default'] 12 | HomeSponsors: typeof import('./theme/components/HomeSponsors.vue')['default'] 13 | HomeTeam: typeof import('./theme/components/HomeTeam.vue')['default'] 14 | TeamMember: typeof import('./theme/components/TeamMember.vue')['default'] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import type { HeadConfig } from 'vitepress' 2 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 3 | import { withPwa } from '@vite-pwa/vitepress' 4 | import { defineConfig } from 'vitepress' 5 | 6 | import vite from './vite.config' 7 | 8 | // https://vitepress.dev/reference/site-config 9 | 10 | const analyticsHead: HeadConfig[] = [ 11 | [ 12 | 'script', 13 | { 14 | 'src': 'https://cdn.usefathom.com/script.js', 15 | 'data-site': 'EGVEPJYT', 16 | 'defer': '', 17 | }, 18 | ], 19 | ] 20 | 21 | const nav = [ 22 | { text: 'News', link: 'https://stacksjs.org/news' }, 23 | { 24 | text: 'Changelog', 25 | link: 'https://github.com/stacksjs/ts-spreadsheets/blob/main/CHANGELOG.md', 26 | }, 27 | { 28 | text: 'Resources', 29 | items: [ 30 | { text: 'Team', link: '/team' }, 31 | { text: 'Sponsors', link: '/sponsors' }, 32 | { text: 'Partners', link: '/partners' }, 33 | { text: 'Postcardware', link: '/postcardware' }, 34 | { text: 'Stargazers', link: '/stargazers' }, 35 | { text: 'License', link: '/license' }, 36 | { 37 | items: [ 38 | { 39 | text: 'Awesome Stacks', 40 | link: 'https://github.com/stacksjs/awesome-stacks', 41 | }, 42 | { 43 | text: 'Contributing', 44 | link: 'https://github.com/stacksjs/stacks/blob/main/.github/CONTRIBUTING.md', 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | ] 51 | 52 | const sidebar = [ 53 | { 54 | text: 'Get Started', 55 | items: [ 56 | { text: 'Intro', link: '/intro' }, 57 | { text: 'Install', link: '/install' }, 58 | { text: 'Usage', link: '/usage' }, 59 | ], 60 | }, 61 | { text: 'Showcase', link: '/Showcase' }, 62 | ] 63 | 64 | const description = 'Easily manage spreadsheets. CSV & Excel.' 65 | const title = 'ts-spreadsheets | Easily generate spreadsheets, like CSVs and Excel files.' 66 | 67 | export default withPwa( 68 | defineConfig({ 69 | lang: 'en-US', 70 | title: 'ts-spreadsheets', 71 | description, 72 | metaChunk: true, 73 | cleanUrls: true, 74 | lastUpdated: true, 75 | 76 | head: [ 77 | ['link', { rel: 'icon', type: 'image/svg+xml', href: './images/logo-mini.svg' }], 78 | ['link', { rel: 'icon', type: 'image/png', href: './images/logo.png' }], 79 | ['meta', { name: 'theme-color', content: '#0A0ABC' }], 80 | ['meta', { name: 'title', content: title }], 81 | ['meta', { name: 'description', content: description }], 82 | ['meta', { name: 'author', content: 'Stacks.js, Inc.' }], 83 | ['meta', { 84 | name: 'tags', 85 | content: 'ts-spreadsheets, ts, typescript, stacksjs, csv, export, import, lightweight, open-source, lightweight, excel, spreadsheet', 86 | }], 87 | ['meta', { property: 'og:type', content: 'website' }], 88 | ['meta', { property: 'og:locale', content: 'en' }], 89 | ['meta', { property: 'og:title', content: title }], 90 | ['meta', { property: 'og:description', content: description }], 91 | 92 | ['meta', { property: 'og:site_name', content: 'ts-spreadsheets' }], 93 | ['meta', { property: 'og:image', content: './images/og-image.jpg' }], 94 | ['meta', { property: 'og:url', content: 'https://ts-spreadsheets.netlify.app/' }], 95 | // ['script', { 'src': 'https://cdn.usefathom.com/script.js', 'data-site': '', 'data-spa': 'auto', 'defer': '' }], 96 | ...analyticsHead, 97 | ], 98 | 99 | themeConfig: { 100 | logo: { 101 | light: './images/logo-transparent.svg', 102 | dark: './images/logo-white-transparent.svg', 103 | }, 104 | 105 | nav, 106 | sidebar, 107 | 108 | editLink: { 109 | pattern: 'https://github.com/stacksjs/stacks/edit/main/docs/docs/:path', 110 | text: 'Edit this page on GitHub', 111 | }, 112 | 113 | footer: { 114 | message: 'Released under the MIT License.', 115 | copyright: 'Copyright © 2025-present Stacks.js, Inc.', 116 | }, 117 | 118 | socialLinks: [ 119 | { icon: 'twitter', link: 'https://twitter.com/stacksjs' }, 120 | { icon: 'bluesky', link: 'https://bsky.app/profile/chrisbreuer.dev' }, 121 | { icon: 'github', link: 'https://github.com/stacksjs/ts-spreadsheets' }, 122 | { icon: 'discord', link: 'https://discord.gg/stacksjs' }, 123 | ], 124 | 125 | // algolia: services.algolia, 126 | 127 | // carbonAds: { 128 | // code: '', 129 | // placement: '', 130 | // }, 131 | }, 132 | 133 | pwa: { 134 | manifest: { 135 | theme_color: '#0A0ABC', 136 | }, 137 | }, 138 | 139 | markdown: { 140 | theme: { 141 | light: 'github-light', 142 | dark: 'github-dark', 143 | }, 144 | 145 | codeTransformers: [ 146 | transformerTwoslash(), 147 | ], 148 | }, 149 | 150 | vite, 151 | }), 152 | ) 153 | -------------------------------------------------------------------------------- /docs/.vitepress/sw.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /// 4 | 5 | import { CacheableResponsePlugin } from 'workbox-cacheable-response' 6 | import { ExpirationPlugin } from 'workbox-expiration' 7 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 8 | import { NavigationRoute, registerRoute } from 'workbox-routing' 9 | import { NetworkFirst, NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies' 10 | 11 | declare let self: ServiceWorkerGlobalScope 12 | 13 | const entries = self.__WB_MANIFEST 14 | 15 | // self.__WB_MANIFEST is the default injection point 16 | precacheAndRoute(entries) 17 | 18 | // clean old assets 19 | cleanupOutdatedCaches() 20 | 21 | let allowlist: undefined | RegExp[] 22 | if (import.meta.env.DEV) 23 | allowlist = [/^\/$/] 24 | 25 | if (import.meta.env.PROD) { 26 | const swPath = self.location.pathname.lastIndexOf('/') 27 | const base = swPath === 0 ? '/' : self.location.pathname.slice(0, swPath + 1) 28 | function escapeStringRegexp(value: string) { 29 | // Escape characters with special meaning either inside or outside character sets. 30 | // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar. 31 | return value 32 | .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') 33 | .replace(/-/g, '\\x2d') 34 | } 35 | allowlist = entries.filter((page) => { 36 | return typeof page === 'string' 37 | ? page.endsWith('.html') 38 | : page.url.endsWith('.html') 39 | }).map((page) => { 40 | const url = typeof page === 'string' ? page : page.url 41 | const regex = url === 'index.html' 42 | ? escapeStringRegexp(base) 43 | : escapeStringRegexp(`${base}${url.replace(/\.html$/, '')}`) 44 | return new RegExp(`^${regex}(\\.html)?$`) 45 | }) 46 | registerRoute( 47 | ({ request, sameOrigin }) => { 48 | return sameOrigin && request.mode === 'navigate' 49 | }, 50 | new NetworkOnly({ 51 | plugins: [{ 52 | /* this callback will be called when the fetch call fails */ 53 | handlerDidError: async () => Response.redirect('404', 302), 54 | /* this callback will prevent caching the response */ 55 | cacheWillUpdate: async () => null, 56 | }], 57 | }), 58 | 'GET', 59 | ) 60 | // googleapis 61 | registerRoute( 62 | /^https:\/\/fonts\.googleapis\.com\/.*/i, 63 | new NetworkFirst({ 64 | cacheName: 'google-fonts-cache', 65 | plugins: [ 66 | new CacheableResponsePlugin({ statuses: [0, 200] }), 67 | // we only need a few entries 68 | new ExpirationPlugin({ 69 | maxEntries: 10, 70 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days 71 | }), 72 | ], 73 | }), 74 | ) 75 | // gstatic 76 | registerRoute( 77 | /^https:\/\/fonts\.gstatic\.com\/.*/i, 78 | new StaleWhileRevalidate({ 79 | cacheName: 'google-fonts-cache', 80 | plugins: [ 81 | new CacheableResponsePlugin({ statuses: [0, 200] }), 82 | // we only need a few entries 83 | new ExpirationPlugin({ 84 | maxEntries: 10, 85 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days 86 | }), 87 | ], 88 | }), 89 | ) 90 | // antfu sponsors 91 | registerRoute( 92 | /^https:\/\/cdn\.jsdelivr\.net\/.*/i, 93 | new NetworkFirst({ 94 | cacheName: 'jsdelivr-images-cache', 95 | plugins: [ 96 | new CacheableResponsePlugin({ statuses: [0, 200] }), 97 | // we only need a few entries 98 | new ExpirationPlugin({ 99 | maxEntries: 10, 100 | maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days 101 | }), 102 | ], 103 | }), 104 | ) 105 | } 106 | 107 | // to allow work offline 108 | registerRoute(new NavigationRoute( 109 | createHandlerBoundToURL('index.html'), 110 | { allowlist }, 111 | )) 112 | 113 | // Skip-Waiting Service Worker-based solution 114 | self.addEventListener('activate', async () => { 115 | // after we've taken over, iterate over all the current clients (windows) 116 | const clients = await self.clients.matchAll({ type: 'window' }) 117 | clients.forEach((client) => { 118 | // ...and refresh each one of them 119 | client.navigate(client.url) 120 | }) 121 | }) 122 | 123 | self.skipWaiting() 124 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeContributors.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeSponsors.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeTeam.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/TeamMember.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/contributors.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrisbbreuer": true, 3 | "glennmichael123": false, 4 | "cab-mikee": true, 5 | "konkonam": false 6 | } 7 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/contributors.ts: -------------------------------------------------------------------------------- 1 | import contributors from './contributors.json' 2 | 3 | export interface Contributor { 4 | name: string 5 | avatar: string 6 | } 7 | 8 | export interface CoreTeam { 9 | avatar: string 10 | name: string 11 | github: string 12 | twitter?: string 13 | bluesky?: string 14 | sponsors?: boolean 15 | description: string 16 | packages?: string[] 17 | functions?: string[] 18 | } 19 | 20 | const contributorsAvatars: Record = {} 21 | 22 | function getAvatarUrl(name: string) { 23 | return `https://avatars.githubusercontent.com/${name}?v=4` 24 | } 25 | 26 | const contributorList = (Object.keys(contributors) as string[]).reduce((acc, name) => { 27 | contributorsAvatars[name] = getAvatarUrl(name) 28 | 29 | if (contributors[name]) { 30 | acc.push({ name, avatar: contributorsAvatars[name] }) 31 | } 32 | 33 | return acc 34 | }, [] as Contributor[]) 35 | 36 | const coreTeamMembers: CoreTeam[] = [ 37 | { 38 | avatar: contributorsAvatars.chrisbbreuer || 'default-avatar.png', 39 | name: 'Chris Breuer', 40 | github: 'chrisbbreuer', 41 | twitter: 'chrisbbreuer', 42 | bluesky: 'chrisbreuer.dev', 43 | sponsors: true, 44 | description: 'Open sourceror.
Core Stacks team.
Working at Stacks.js', 45 | packages: ['core'], 46 | functions: ['cloud', 'backend', 'frontend', 'ci/cd'], 47 | }, 48 | { 49 | avatar: contributorsAvatars.glennmichael123 || 'default-avatar.png', 50 | name: 'Glenn', 51 | github: 'glennmichael123', 52 | twitter: 'glennmichael123', 53 | sponsors: false, 54 | packages: ['core'], 55 | functions: ['backend', 'frontend', 'desktop'], 56 | description: 'Open sourceror.
Core Stacks team.
Working at Stacks.js', 57 | }, 58 | 59 | { 60 | avatar: contributorsAvatars['cab-mikee'] || 'default-avatar.png', 61 | name: 'Mike', 62 | github: 'cab-mikee', 63 | twitter: 'cab-mikee', 64 | sponsors: false, 65 | description: 'Open sourceror.
Core Stacks team.
Working at Stacks.js', 66 | packages: ['core'], 67 | functions: ['backend', 'frontend'], 68 | }, 69 | 70 | { 71 | avatar: contributorsAvatars.konkonam || 'default-avatar.png', 72 | name: 'Zoltan', 73 | github: 'konkonam', 74 | sponsors: true, 75 | description: 'Open sourceror.
Core Stacks team.', 76 | packages: ['core'], 77 | functions: ['backend', 'frontend', 'desktop'], 78 | }, 79 | ] 80 | .sort((pre, cur) => { 81 | const contribute = Object.keys(contributors) 82 | return contribute.findIndex(name => name === pre.github) - contribute.findIndex(name => name === cur.github) 83 | }) 84 | 85 | export { contributorList as contributors, coreTeamMembers } 86 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from 'vitepress' 2 | import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client' 3 | import DefaultTheme from 'vitepress/theme' 4 | import { h } from 'vue' 5 | 6 | import 'uno.css' 7 | 8 | import './styles/main.css' 9 | import './styles/vars.css' 10 | import './styles/overrides.css' 11 | 12 | export default { 13 | ...DefaultTheme, 14 | 15 | enhanceApp(ctx: any) { 16 | ctx.app.use(TwoSlashFloatingVue) 17 | }, 18 | 19 | Layout: () => { 20 | return h(DefaultTheme.Layout, null, { 21 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 22 | }) 23 | }, 24 | } satisfies Theme 25 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/main.css: -------------------------------------------------------------------------------- 1 | html.dark { 2 | color-scheme: dark; 3 | } 4 | 5 | .vp-doc h2 { 6 | border-top: 0; 7 | margin-top: 10px; 8 | } 9 | 10 | .VPMenuLink .link { 11 | line-height: 28px !important; 12 | } 13 | 14 | .VPSidebarGroup .link { 15 | padding: 3px 0 !important; 16 | } 17 | 18 | .vp-doc a:has(> code) { 19 | text-decoration: none; 20 | color: var(--vp-c-brand-1); 21 | } 22 | 23 | .vp-doc a:has(> code):hover { 24 | text-decoration: underline; 25 | } 26 | 27 | #app a:focus-visible, 28 | #app button:focus-visible, 29 | #app input[type='checkbox']:focus-visible { 30 | --at-apply: outline-1 outline-primary ring-2 ring-primary; 31 | } 32 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/overrides.css: -------------------------------------------------------------------------------- 1 | .custom-block-title { 2 | opacity: 0.5; 3 | font-size: 12px; 4 | margin-top: -5px !important; 5 | margin-bottom: -10px !important; 6 | filter: saturate(0.6); 7 | letter-spacing: 0.5px; 8 | } 9 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-brand-1: #2563eb; 7 | --vp-c-brand-2: #1e40af; 8 | --vp-c-brand-3: #172554; 9 | --vp-c-brand-soft: #2563eb50; 10 | 11 | --vp-c-text-dark-1: #ffffff; /* Adding this to ensure light text */ 12 | 13 | --vp-c-text-code: #4a72bf; 14 | 15 | --vp-code-block-bg: rgba(125, 125, 125, 0.04); 16 | --vp-code-copy-code-bg: rgba(125, 125, 125, 0.1); 17 | --vp-code-copy-code-hover-bg: rgba(125, 125, 125, 0.2); 18 | 19 | --vp-c-disabled-bg: rgba(125, 125, 125, 0.2); 20 | --vp-c-text-light-2: rgba(56 56 56 / 70%); 21 | --vp-c-text-dark-2: rgba(56 56 56 / 70%); 22 | 23 | --vp-custom-block-info-bg: transparent; 24 | --vp-custom-block-tip-bg: transparent; 25 | 26 | --vp-custom-block-warning-bg: #d9a40605; 27 | --vp-custom-block-warning-text: #d9a406; 28 | --vp-custom-block-warning-border: #d9a40630; 29 | 30 | --vp-custom-block-tip-bg: #2563eb05; 31 | --vp-custom-block-tip-text: #2563eb; 32 | --vp-custom-block-tip-border: #2563eb30; 33 | 34 | --vp-code-color: #4a72bf; 35 | } 36 | 37 | .dark { 38 | --vp-code-block-bg: var(--vp-c-bg-alt); 39 | --vp-c-text-code: #93c5fd; 40 | --vp-c-text-dark-2: rgba(235, 235, 235, 0.6); 41 | } 42 | 43 | /** 44 | * Component: Code 45 | * -------------------------------------------------------------------------- */ 46 | 47 | :root { 48 | --vp-code-line-highlight-color: rgba(125, 125, 125, 0.2); 49 | } 50 | 51 | .dark { 52 | --vp-code-line-highlight-color: rgba(0, 0, 0, 0.5); 53 | } 54 | 55 | /** 56 | * Component: Button 57 | * -------------------------------------------------------------------------- */ 58 | 59 | :root { 60 | --vp-button-brand-border: var(--vp-c-brand-1); 61 | --vp-button-brand-text: var(--vp-c-text-dark-1); 62 | --vp-button-brand-bg: var(--vp-c-brand-1); 63 | --vp-button-brand-hover-border: var(--vp-c-brand-2); 64 | --vp-button-brand-hover-text: var(--vp-c-text-dark-1); 65 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); 66 | --vp-button-brand-active-border: var(--vp-c-brand-2); 67 | --vp-button-brand-active-text: var(--vp-c-text-dark-1); 68 | --vp-button-brand-active-bg: var(--vp-c-brand-2); 69 | } 70 | 71 | /** 72 | * Component: Home 73 | * -------------------------------------------------------------------------- */ 74 | 75 | :root { 76 | --vp-home-hero-name-color: transparent; 77 | --vp-home-hero-name-background: -webkit-linear-gradient( 78 | 120deg, 79 | #1e40af, 80 | #2563eb 81 | ); 82 | --vp-home-hero-image-background-image: linear-gradient(-45deg, #1e40af 50%, #2563eb 50%); 83 | --vp-home-hero-image-filter: blur(30px); 84 | } 85 | 86 | @media (min-width: 640px) { 87 | :root { 88 | --vp-home-hero-image-filter: blur(56px); 89 | } 90 | } 91 | 92 | @media (min-width: 960px) { 93 | :root { 94 | --vp-home-hero-image-filter: blur(60px); 95 | } 96 | } 97 | 98 | /** 99 | * Component: Algolia 100 | * -------------------------------------------------------------------------- */ 101 | 102 | /* .DocSearch { 103 | --docsearch-primary-color: var(--vp-c-brand) !important; 104 | } */ 105 | -------------------------------------------------------------------------------- /docs/.vitepress/unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetUno, 6 | transformerDirectives, 7 | transformerVariantGroup, 8 | } from 'unocss' 9 | 10 | export default defineConfig({ 11 | shortcuts: { 12 | 'border-main': 'border-$vp-c-divider', 13 | 'bg-main': 'bg-gray-400', 14 | 'bg-base': 'bg-white dark:bg-hex-1a1a1a', 15 | }, 16 | 17 | presets: [ 18 | presetUno(), 19 | presetAttributify(), 20 | presetIcons({ 21 | scale: 1.2, 22 | warn: true, 23 | }), 24 | ], 25 | 26 | theme: { 27 | colors: { 28 | primary: '#3eaf7c', 29 | }, 30 | 31 | fontFamily: { 32 | mono: 'var(--vp-font-family-mono)', 33 | }, 34 | }, 35 | 36 | transformers: [ 37 | transformerDirectives(), 38 | transformerVariantGroup(), 39 | ], 40 | }) 41 | -------------------------------------------------------------------------------- /docs/.vitepress/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | // import Inspect from 'vite-plugin-inspect' 3 | import UnoCSS from 'unocss/vite' 4 | import IconsResolver from 'unplugin-icons/resolver' 5 | import Icons from 'unplugin-icons/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { defineConfig } from 'vite' 8 | 9 | export default defineConfig({ 10 | build: { 11 | assetsDir: 'assets', 12 | rollupOptions: { 13 | output: { 14 | assetFileNames: 'assets/[name].[hash][extname]', 15 | }, 16 | }, 17 | }, 18 | 19 | resolve: { 20 | dedupe: [ 21 | 'vue', 22 | '@vue/runtime-core', 23 | ], 24 | }, 25 | plugins: [ 26 | // custom 27 | // MarkdownTransform(), 28 | // Contributors(contributions), 29 | 30 | // plugins 31 | Components({ 32 | dirs: resolve(__dirname, 'theme/components'), 33 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 34 | resolvers: [ 35 | IconsResolver({ 36 | componentPrefix: '', 37 | }), 38 | ], 39 | dts: resolve(__dirname, 'components.d.ts'), 40 | transformer: 'vue3', 41 | }), 42 | 43 | Icons({ 44 | compiler: 'vue3', 45 | defaultStyle: 'display: inline-block', 46 | }), 47 | 48 | UnoCSS(resolve(__dirname, 'unocss.config.ts')), 49 | 50 | // Inspect(), 51 | ], 52 | 53 | optimizeDeps: { 54 | exclude: [ 55 | // 'vue', 56 | 'body-scroll-lock', 57 | ], 58 | 59 | include: [ 60 | 'nprogress', 61 | ], 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /docs/_data/team.js: -------------------------------------------------------------------------------- 1 | export const core = [ 2 | { 3 | avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UAFFN6YSE-fb28a6b5d278-512', 4 | name: 'Chris Breuer', 5 | title: 'Creator', 6 | org: 'Stacks', 7 | orgLink: 'https://stacksjs.org/', 8 | desc: 'Independent open source developer, builder in the Stacks ecosystem.', 9 | links: [ 10 | { icon: 'github', link: 'https://github.com/chrisbbreuer' }, 11 | { icon: 'bluesky', link: 'https://bsky.app/profile/chrisbreuer.dev' }, 12 | { icon: 'twitter', link: 'https://twitter.com/chrisbbreuer' }, 13 | ], 14 | sponsor: 'https://github.com/sponsors/chrisbbreuer', 15 | }, 16 | { 17 | avatar: 'https://avatars.githubusercontent.com/u/72235211', 18 | name: 'Blake Ayer', 19 | title: 'Cloud Genius', 20 | org: 'Stacks', 21 | orgLink: 'https://stacksjs.org/', 22 | desc: 'Core team member of Stacks.', 23 | links: [{ icon: 'github', link: 'https://github.com/blakeayer' }], 24 | sponsor: 'https://github.com/sponsors/blakeayer', 25 | }, 26 | { 27 | avatar: 'https://avatars.githubusercontent.com/u/19656966', 28 | name: 'Zoltan', 29 | title: 'Desktop Wizard', 30 | org: 'Stacks', 31 | orgLink: 'https://stacksjs.org/', 32 | desc: 'Core team member of Stacks.', 33 | links: [{ icon: 'github', link: 'https://github.com/konkonam' }], 34 | sponsor: 'https://github.com/sponsors/konkonam', 35 | }, 36 | { 37 | avatar: 'https://www.averyahill.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Favatar.626e0c07.jpg&w=128&q=75', 38 | name: 'Avery Hill', 39 | title: 'Merchandise & Event Planning', 40 | org: 'Stacks', 41 | orgLink: 'https://stacksjs.org/', 42 | desc: 'Core team member of Stacks.', 43 | links: [{ icon: 'github', link: 'https://www.averyahill.com/' }], 44 | }, 45 | { 46 | avatar: 'https://avatars.githubusercontent.com/u/10015302', 47 | name: 'Harlequin Doyon', 48 | title: 'A collaborative being', 49 | org: 'Stacks', 50 | orgLink: 'https://stacksjs.org/', 51 | desc: 'Core team member of Stacks.', 52 | links: [{ icon: 'github', link: 'https://github.com/harlekoy' }], 53 | sponsor: 'https://github.com/sponsors/harlekoy', 54 | }, 55 | { 56 | avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UCX5LQ2NP-g16e383ecf66-512', 57 | name: 'Germaine Abellanosa', 58 | title: 'Social Tech Genius', 59 | org: 'Stacks', 60 | orgLink: 'https://stacksjs.org/', 61 | desc: 'Core team member of Stacks.', 62 | links: [{ icon: 'github', link: 'https://github.com/germikee' }], 63 | sponsor: 'https://github.com/sponsors/germikee', 64 | }, 65 | { 66 | avatar: 'https://avatars.githubusercontent.com/u/58994540', 67 | name: 'Frederik Bußmann', 68 | title: 'A collaborative being', 69 | org: 'Stacks', 70 | orgLink: 'https://stacksjs.org/', 71 | desc: 'Core team member of Stacks.', 72 | links: [{ icon: 'github', link: 'https://github.com/freb97' }], 73 | sponsor: 'https://github.com/sponsors/freb97', 74 | }, 75 | { 76 | avatar: 'https://avatars.githubusercontent.com/u/977413', 77 | name: 'Dorell James', 78 | title: 'A collaborative being', 79 | org: 'Stacks', 80 | orgLink: 'https://stacksjs.org/', 81 | desc: 'Core team member of Stacks.', 82 | links: [ 83 | { icon: 'github', link: 'https://github.com/dorelljames' }, 84 | { icon: 'twitter', link: 'https://twitter.com/dorelljames' }, 85 | ], 86 | sponsor: 'https://github.com/sponsors/dorelljames', 87 | }, 88 | { 89 | avatar: 'https://avatars.githubusercontent.com/u/29087513', 90 | name: 'Glenn Michael', 91 | title: 'Desktop, Mobile, Web', 92 | org: 'Stacks', 93 | orgLink: 'https://stacksjs.org/', 94 | desc: 'Core team member of Stacks.', 95 | links: [{ icon: 'github', link: 'https://github.com/glennmichael123' }], 96 | sponsor: 'https://github.com/sponsors/glennmichael123', 97 | }, 98 | ] 99 | 100 | export const emeriti = [ 101 | // 102 | ] 103 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: "ts-spreadsheets" 6 | text: "A TypeScript Spreadsheet Library" 7 | tagline: "Easily manage spreadsheets. CSVs & Excel." 8 | image: /images/logo-white.png 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /intro 13 | - theme: alt 14 | text: View on GitHub 15 | link: https://github.com/stacksjs/ts-spreadsheets 16 | 17 | features: 18 | - title: Developer-Friendly API 19 | icon: 🛠️ 20 | details: Designed for TypeScript with strong type support. 21 | - title: Lightweight 22 | icon: 🚀 23 | details: "Small footprint and performance optimized." 24 | - title: Import/Export 25 | icon: 📊 26 | details: "Easily read and write from CSV, JSON, or XLSX formats." 27 | - title: Fully Typed 28 | icon: 🏗️ 29 | details: "Written in and for TypeScript, with full type support." 30 | --- 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | Installing `ts-spreadsheets` is easy. Simply pull it in via your package manager of choice. 4 | 5 | ::: code-group 6 | 7 | ```sh [npm] 8 | npm install ts-spreadsheets 9 | # npm i ts-spreadsheets 10 | ``` 11 | 12 | ```sh [bun] 13 | bun install ts-spreadsheets 14 | # bun add ts-spreadsheets 15 | ``` 16 | 17 | ```sh [pnpm] 18 | pnpm add ts-spreadsheets 19 | ``` 20 | 21 | ```sh [yarn] 22 | yarn add ts-spreadsheets 23 | ``` 24 | 25 | ::: 26 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 |

Social Card of this typescript spreadsheets repo

2 | 3 | # ts-spreadsheets 4 | 5 | Easily generate spreadsheets, like CSVs and Excel files. 6 | 7 | ## Features 8 | 9 | - Generate CSV & Excel files 10 | - Store spreadsheets to disk 11 | - Download spreadsheets as a Response object 12 | - Simple API for creating and manipulating spreadsheets 13 | - Performant & dependency-free 14 | - Library & CLI support 15 | - Fully typed 16 | 17 | ## Usage 18 | 19 | There are two ways to interact with `ts-spreadsheets`: via the CLI or as a library. 20 | 21 | ### Library 22 | 23 | As a library, you will want to make sure to install it in your project: 24 | 25 | ```bash 26 | bun install ts-spreadsheets 27 | ``` 28 | 29 | _Now, you can use the library in your project:_ 30 | 31 | ```ts 32 | import { createSpreadsheet, spreadsheet } from 'ts-spreadsheets' 33 | 34 | // Create a spreadsheet 35 | const data = { 36 | headings: ['Name', 'Age', 'City'], 37 | data: [ 38 | ['Chris Breuer', 30, 'Playa Vista'], 39 | ['Avery Hill', 25, 'Santa Monica'], 40 | ['Danny Johnson', 35, 'San Francisco'] 41 | ] 42 | } 43 | 44 | // Generate and manipulate spreadsheets 45 | 46 | // 1. Using createSpreadsheet function 47 | const spreadsheet = createSpreadsheet(data) // defaults to csv 48 | const csvSpreadsheet = createSpreadsheet(data, { type: 'csv' }) 49 | const excelSpreadsheet = createSpreadsheet(data, { type: 'excel' }) 50 | 51 | // Store the spreadsheet to disk 52 | await spreadsheet.store('output.csv') 53 | 54 | // Create a download response 55 | const response1 = excelSpreadsheet.download('data.xlsx') // downloads and stores as data.xlsx on your filesystem 56 | 57 | // 2. Using spreadsheet object directly, and chain if desired 58 | const csvContent = spreadsheet(data).generateCSV().store('output2.csv') 59 | const csvContent2 = spreadsheet(data).csv().store('output3.csv') // same as above 60 | 61 | const excelContent = spreadsheet(data).generateExcel() 62 | await excelContent.store('output3.xlsx') 63 | const response2 = await excelContent.download('output3.xlsx') // downloads and stores as output3.xlsx 64 | 65 | // 3. Accessing raw content 66 | const rawCsvContent = spreadsheet(data).csv().getContent() 67 | const rawCsvContent2 = spreadsheet(data).generateCSV().getContent() 68 | const rawExcelContent = spreadsheet(data).excel().getContent() 69 | const rawExcelContent2 = spreadsheet(data).generateExcel().getContent() 70 | 71 | console.log('CSV Content:', rawCsvContent) 72 | console.log('Excel Content:', rawExcelContent) 73 | ``` 74 | 75 | ### Main Functions 76 | 77 | #### spreadsheet(data: Content) 78 | 79 | Creates a spreadsheet object with various methods. 80 | 81 | - `data`: An object containing `headings` and `data` for the spreadsheet. 82 | 83 | Returns an object with the following methods: 84 | 85 | - `csv()`: Generates a CSV SpreadsheetWrapper 86 | - `excel()`: Generates an Excel SpreadsheetWrapper 87 | - `store(path: string)`: Stores the spreadsheet to a file 88 | - `generateCSV()`: Generates a CSV SpreadsheetWrapper 89 | - `generateExcel()`: Generates an Excel SpreadsheetWrapper 90 | 91 | _Example:_ 92 | 93 | ```typescript 94 | const csvWrapper = await spreadsheet(data).csv() // equivalent to spreadsheet(data).generateCSV() 95 | ``` 96 | 97 | #### createSpreadsheet(data: Content, options?: SpreadsheetOptions) 98 | 99 | Creates a SpreadsheetWrapper with the given data and options. 100 | 101 | - `data`: An object containing `headings` and `data` for the spreadsheet. 102 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 103 | 104 | Returns a SpreadsheetWrapper. 105 | 106 | _Example:_ 107 | 108 | ```typescript 109 | const spreadsheet = createSpreadsheet(data, { type: 'csv' }) 110 | ``` 111 | 112 | ### SpreadsheetWrapper Methods 113 | 114 | #### getContent() 115 | 116 | Returns the content of the spreadsheet as a string or Uint8Array. 117 | 118 | #### download(filename: string) 119 | 120 | Creates a download Response for the spreadsheet. 121 | 122 | - `filename`: The name of the file to be downloaded. 123 | 124 | Returns a Response object. 125 | 126 | #### store(path: string) 127 | 128 | Stores the spreadsheet to a file. 129 | 130 | - `path`: The file path where the spreadsheet will be stored. 131 | 132 | Returns a Promise that resolves when the file is written. 133 | 134 | ### Utility Functions 135 | 136 | #### spreadsheet.create(data: Content, options?: SpreadsheetOptions) 137 | 138 | Creates a SpreadsheetContent object. 139 | 140 | - `data`: An object containing `headings` and `data` for the spreadsheet. 141 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 142 | 143 | Returns a SpreadsheetContent object. 144 | 145 | #### spreadsheet.generate(data: Content, options?: SpreadsheetOptions) 146 | 147 | Generates spreadsheet content based on the given data and options. 148 | 149 | - `data`: An object containing `headings` and `data` for the spreadsheet. 150 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 151 | 152 | Returns a string or Uint8Array representing the spreadsheet content. 153 | 154 | #### spreadsheet.generateCSV(content: Content) 155 | 156 | Generates a CSV SpreadsheetWrapper. 157 | 158 | - `content`: An object containing `headings` and `data` for the spreadsheet. 159 | 160 | Returns a SpreadsheetWrapper for CSV, which can be used to chain other methods like `store()` or `download()`. 161 | 162 | _Example:_ 163 | 164 | ```typescript 165 | await spreadsheet(data).generateCSV().store('output.csv') 166 | 167 | // if one can rely on the file extension to determine the type, you may do this: 168 | await spreadsheet(data).store('output.csv') 169 | ``` 170 | 171 | #### spreadsheet.generateExcel(content: Content) 172 | 173 | Generates an Excel SpreadsheetWrapper. 174 | 175 | - `content`: An object containing `headings` and `data` for the spreadsheet. 176 | 177 | Returns a SpreadsheetWrapper for Excel, which can be used to chain other methods like `store()` or `download()`. 178 | 179 | _Example:_ 180 | 181 | ```ts 182 | await spreadsheet(data).store('output.xlsx') 183 | // or 184 | await spreadsheet(data).generateExcel().store('output.xlsx') 185 | ``` 186 | 187 | To view the full documentation, please visit [https://ts-spreadsheets.netlify.app](https://ts-spreadsheets.netlify.app). 188 | 189 | ### CLI 190 | 191 | You can also use the CLI to generate spreadsheets from JSON input: 192 | 193 | ```bash 194 | # Create a spreadsheet from JSON input 195 | spreadsheets create data.json -o output.csv 196 | spreadsheets create data.json --type excel -o output.xlsx 197 | 198 | # Convert between formats 199 | spreadsheets convert input.csv output.xlsx 200 | spreadsheets convert input.xlsx output.csv 201 | 202 | # Validate JSON input format 203 | spreadsheets validate input.json 204 | ``` 205 | 206 | The input json should follow this format: 207 | 208 | ```json 209 | { 210 | "headings": ["Name", "Age", "City"], 211 | "data": [ 212 | ["Chris Breuer", 30, "Playa Vista"], 213 | ["Avery Hill", 25, "Santa Monica"], 214 | ["Danny Johnson", 35, "San Francisco"] 215 | ] 216 | } 217 | ``` 218 | 219 | #### CLI Commands 220 | 221 | - `create`: Generate a spreadsheet from JSON input 222 | - Options: 223 | - `-t, --type `: Output type ('csv' or 'excel'), defaults to 'csv' 224 | - `-o, --output `: Output file path 225 | - `convert`: Convert between spreadsheet formats 226 | - Automatically detects format from file extensions 227 | - Supports conversion between CSV and Excel formats 228 | - `validate`: Check if JSON input meets the required format 229 | - Validates structure and data types 230 | - Provides helpful error messages for invalid input 231 | 232 | All commands support the `--help` flag for more information: 233 | 234 | ```bash 235 | spreadsheets --help 236 | spreadsheets create --help 237 | spreadsheets convert --help 238 | spreadsheets validate --help 239 | ``` 240 | 241 | ## Testing 242 | 243 | ```bash 244 | bun test 245 | ``` 246 | 247 | ## Changelog 248 | 249 | Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently. 250 | 251 | ## Contributing 252 | 253 | Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details. 254 | 255 | ## Stargazers 256 | 257 | [![Stargazers](https://starchart.cc/stacksjs/ts-spreadsheets.svg?variant=adaptive)](https://starchart.cc/stacksjs/ts-spreadsheets) 258 | 259 | ## Community 260 | 261 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 262 | 263 | [Discussions on GitHub](https://github.com/stacksjs/stacks/discussions) 264 | 265 | For casual chit-chat with others using this package: 266 | 267 | [Join the Stacks Discord Server](https://discord.gg/stacksjs) 268 | 269 | ## Postcardware 270 | 271 | Two things are true: Stacks OSS will always stay open-source, and we do love to receive postcards from wherever Stacks is used! _We also publish them on our website._ 272 | 273 | Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094 🌎 274 | 275 | ## Sponsors 276 | 277 | We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us. 278 | 279 | - [JetBrains](https://www.jetbrains.com/) 280 | - [The Solana Foundation](https://solana.com/) 281 | 282 | ## Credits 283 | 284 | - [Chris Breuer](https://github.com/chrisbbreuer) 285 | - [All Contributors](https://github.com/stacksjs/ts-spreadsheets/contributors) 286 | 287 | ## License 288 | 289 | The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/ts-spreadsheets/tree/main/LICENSE.md) for more information. 290 | 291 | Made with 💙 292 | 293 | 294 | 295 | 297 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Stacks.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/partners.md: -------------------------------------------------------------------------------- 1 | # Partners 2 | 3 | The following companies and organizations are supporting Stacks development through partnerships: 4 | 5 | - [JetBrains](https://www.jetbrains.com/) 6 | - [The Solana Foundation](https://solana.com/) 7 | 8 | If you are interested in becoming a partner, please reach out to us. 9 | -------------------------------------------------------------------------------- /docs/postcardware.md: -------------------------------------------------------------------------------- 1 | ## Postcardware 2 | 3 | You will always be free to use any of the Stacks OSS software. We would also love to see which parts of the world Stacks ends up in. _Receiving postcards makes us happy—and we will publish them on our website._ 4 | 5 | Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎 6 | -------------------------------------------------------------------------------- /docs/public/images/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/public/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/public/images/logo-mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/public/images/logo-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/public/images/logo-white-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/public/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacksjs/ts-spreadsheets/296d1546035ea9a9b08596b0c0da2641c55fca06/docs/public/images/logo-white.png -------------------------------------------------------------------------------- /docs/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacksjs/ts-spreadsheets/296d1546035ea9a9b08596b0c0da2641c55fca06/docs/public/images/logo.png -------------------------------------------------------------------------------- /docs/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/public/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacksjs/ts-spreadsheets/296d1546035ea9a9b08596b0c0da2641c55fca06/docs/public/images/og-image.png -------------------------------------------------------------------------------- /docs/showcase.md: -------------------------------------------------------------------------------- 1 | # Showcase 2 | 3 | Here are all the cool projects that people have built using `ts-spreadsheets`. 4 | 5 | ## Community Projects 6 | 7 | - _Yours could be here!_ 8 | 9 | If you authored a project you’d like to showcase, please share it with us in any way _(on Discord, Social Media, or via a PR, etc.)_, and we’ll add it here. 10 | 11 | ## First-Party 12 | 13 | The whole Stacks ecosystem is built using `ts-spreadsheets`. Here are some of the projects utilizing it: 14 | 15 | - [`stacks`](https://github.com/stacksjs/stacks) _(Progressive full-stack framework for Web Artisans. Develop modern apps, clouds & framework-agnostic libraries—faster.)_ 16 | 17 | ### Stacks Framework 18 | 19 | As mentioned before, the whole core of the Stacks Framework is built using `dtsx`. Here are some of those Stacks core packages: 20 | 21 | - [`@stacksjs/actions`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/actions) 22 | - [`@stacksjs/ai`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ai) 23 | - [`@stacksjs/alias`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/alias) 24 | - [`@stacksjs/analytics`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/analytics) 25 | - [`@stacksjs/api`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/api) 26 | - [`@stacksjs/arrays`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/arrays) 27 | - [`@stacksjs/auth`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/auth) 28 | - [`@stacksjs/browser`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/browser) 29 | - [`@stacksjs/buddy`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/buddy) 30 | - [`@stacksjs/build`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/build) 31 | - [`@stacksjs/cache`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cache) 32 | - [`@stacksjs/calendar`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/calendar) 33 | - [`@stacksjs/chat`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/chat) 34 | - [`@stacksjs/cli`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cli) 35 | - [`@stacksjs/cloud`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cloud) 36 | - [`@stacksjs/collections`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/collections) 37 | - [`@stacksjs/commerce`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/commerce) 38 | - [`@stacksjs/components`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/components) 39 | - [`@stacksjs/config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/config) 40 | - [`@stacksjs/cron`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cron) 41 | - [`@stacksjs/database`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/database) 42 | - [`@stacksjs/datetime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/datetime) 43 | - [`@stacksjs/desktop`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/desktop) 44 | - [`@stacksjs/development`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/development) 45 | - [`@stacksjs/dns`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/dns) 46 | - [`@stacksjs/docs`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/docs) 47 | - [`@stacksjs/email`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/email) 48 | - [`@stacksjs/enums`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/enums) 49 | - [`@stacksjs/env`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/env) 50 | - [`@stacksjs/error-handling`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/error-handling) 51 | - [`@stacksjs/events`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/events) 52 | - [`@stacksjs/faker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/faker) 53 | - [`@stacksjs/git`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/git) 54 | - [`@stacksjs/health`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/health) 55 | - [`@stacksjs/http`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/http) 56 | - [`@stacksjs/lint`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/lint) 57 | - [`@stacksjs/logging`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/logging) 58 | - [`@stacksjs/notifications`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/notifications) 59 | - [`@stacksjs/objects`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/objects) 60 | - [`@stacksjs/orm`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/orm) 61 | - [`@stacksjs/path`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/path) 62 | - [`@stacksjs/payments`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/payments) 63 | - [`@stacksjs/plugins`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/plugins) 64 | - [`@stacksjs/push`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/push) 65 | - [`@stacksjs/query-builder`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/query-builder) 66 | - [`@stacksjs/queue`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/queue) 67 | - [`@stacksjs/raycast`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/raycast) 68 | - [`@stacksjs/realtime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/realtime) 69 | - [`@stacksjs/registry`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/registry) 70 | - [`@stacksjs/repl`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/repl) 71 | - [`@stacksjs/router`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/router) 72 | - [`@stacksjs/scheduler`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/scheduler) 73 | - [`@stacksjs/search-engine`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/search-engine) 74 | - [`@stacksjs/security`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/security) 75 | - [`@stacksjs/server`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/server) 76 | - [`@stacksjs/shell`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/shell) 77 | - [`@stacksjs/slug`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/slug) 78 | - [`@stacksjs/sms`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/sms) 79 | - [`@stacksjs/storage`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/storage) 80 | - [`@stacksjs/strings`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/strings) 81 | - [`@stacksjs/testing`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/testing) 82 | - [`@stacksjs/tinker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tinker) 83 | - [`@stacksjs/tunnel`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tunnel) 84 | - [`@stacksjs/types`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/types) 85 | - [`@stacksjs/ui`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ui) 86 | - [`@stacksjs/utils`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/utils) 87 | - [`@stacksjs/validation`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/validation) 88 | - [`@stacksjs/vite-config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-config) 89 | - [`@stacksjs/vite-plugin`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-plugin) 90 | - [`@stacksjs/whois`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/whois) 91 | - [`@stacksjs/x-ray`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/x-ray) 92 | 93 | We try to keep the list up-to-date, but it's possible that some packages are missing. If you find any, please let us know! 94 | -------------------------------------------------------------------------------- /docs/sponsors.md: -------------------------------------------------------------------------------- 1 | # Become a Stacks.js Sponsor 2 | 3 | Stacks.js is an MIT licensed open source project and completely free to use. The tremendous amount of effort needed to maintain such a large ecosystem and develop new features for the project is only made sustainable thanks to the generous financial backing of our sponsors. 4 | 5 | ## How to Sponsor 6 | 7 | Sponsorships can be done via [GitHub Sponsors](https://github.com/sponsors/chrisbbreuer) or [OpenCollective](https://opencollective.com/stacksjs). Invoices can be obtained via GitHub's payment system. Both monthly-recurring sponsorships and one-time donations are accepted. Recurring sponsorships are entitled to logo placements as specified in Sponsorship Tiers. 8 | 9 | If you have questions regarding tiers, payment logistics, or sponsor exposure data, please reach out to . 10 | 11 | The following companies and organizations are supporting Stacks development through sponsorships: 12 | 13 | ## Sponsoring Stacks as a Business 14 | 15 | Sponsoring Stacks gives you developer exposure around the world through our website and GitHub project READMEs. This not only directly generates leads, but also improves your brand recognition as a business that cares about Open Source. This is an intangible but extremely important asset for companies building products for developers, as it improves your conversion rate. 16 | 17 | If you are using Stacks to build a revenue-generating product, it makes business sense to sponsor Stacks's development: it ensures the project that your product relies on stays healthy and actively maintained. The exposure and positive brand image in the Stacks community also makes it easier to attract and recruit Stacks developers. 18 | 19 | If you are building a product where your target customers are developers, you will gain high quality traffic through the sponsorship exposure, since all our visitors are developers. The sponsorship also builds brand recognition and improves conversion. 20 | 21 | ## Sponsoring Stacks as an Individual 22 | 23 | If you are an individual user and have enjoyed the productivity of using Stacks, consider donating as a sign of appreciation - like buying us coffee once in a while. Many of our team members accept sponsorships and donations via GitHub Sponsors. Look for the "Sponsor" button on each team member's profile on our [team page](https://stacksjs-docs.netlify.app/team). 24 | 25 | You can also try to convince your employer to sponsor Stacks as a business. This may not be easy, but business sponsorships typically make a much larger impact on the sustainability of OSS projects than individual donations, so you will help us much more if you succeed. 26 | 27 | ## Tier Benefits 28 | 29 | - ***Global Special Sponsor*** 30 | - Limited to one sponsor globally. Currently vacant. [Get in touch!](mailto:sponsors@stacksjs.org) 31 | - *(Exclusive)* Above the fold logo placement on the front page of [stacksjs.org](https://stacksjs.org). 32 | - *(Exclusive)* Special shoutout and regular retweets of major product launches via Stacks's official social accounts. 33 | - Most prominent logo placement in all locations from tiers below. 34 | 35 | ___ 36 | 37 | - **Diamond (USD$2,500/mo):** 38 | - Prominent logo placement on the front page of [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 39 | - Prominent logo placement in sidebar of all content pages. 40 | - Prominent logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 41 | 42 | ___ 43 | 44 | - **Platinum (USD$1,000/mo):** 45 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 46 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 47 | 48 | ___ 49 | 50 | - **Double Gold (USD$500/mo):** 51 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 52 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 53 | 54 | ___ 55 | 56 | - **Gold (USD$250/mo):** 57 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 58 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 59 | 60 | ___ 61 | 62 | - **Silver (USD$100/mo):** 63 | - Medium logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 64 | - Medium logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 65 | - Medium logo placement in the `BACKERS.md` file of>70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 66 | 67 | ___ 68 | 69 | - **Bronze (50/mo):** 70 | - Small logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). 71 | - Small logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 72 | - Small logo placement in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 73 | 74 | ___ 75 | 76 | - **Generous Backer (USD$25/mo):** 77 | - Private Discord channel invite 78 | - Free Personal Account for Stacks Dashboard 79 | - Social media follow 80 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 81 | 82 | ___ 83 | 84 | - **Sponsorware Tier (USD$10/mo):** 85 | - Get access to all current sponsorware projects *(i.e. the Stacks DynamoDB ORM driver)* 86 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 87 | 88 | ___ 89 | 90 | - **Individual Backer (USD$5/mo):** 91 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` 92 | 93 | ## Current Sponsors 94 | 95 | ### Global Special Sponsor 96 | 97 | - Vacant 98 | 99 | ### Diamond Sponsors 100 | 101 | - Vacant 102 | 103 | ### Platinum Sponsors 104 | 105 | - Vacant 106 | 107 | ### Gold Sponsors 108 | 109 | - Vacant 110 | 111 | ### Silver Sponsors 112 | 113 | - Vacant 114 | 115 | ### Bronze Sponsors 116 | 117 | - Vacant 118 | 119 | ### Generous Backers 120 | 121 | - Vacant 122 | 123 | ### Individual Backers 124 | 125 | - Vacant 126 | 127 | ___ 128 | 129 | ##### Thanks to Vue.js for the inspiration of this sponsorship page 130 | 131 | *You can find their sponsorship page [here](https://vuejs.org/sponsor/).* 132 | -------------------------------------------------------------------------------- /docs/stargazers.md: -------------------------------------------------------------------------------- 1 | ## Stargazers over time 2 | 3 | [![Stargazers over time](https://starchart.cc/stacksjs/ts-spreadsheets.svg?variant=adaptive)](https://starchart.cc/stacksjs/ts-spreadsheets) 4 | -------------------------------------------------------------------------------- /docs/team.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Meet the Team 4 | description: A team of incredible people. 5 | sidebar: false 6 | --- 7 | 8 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 36 | 37 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # 🤖 Usage 2 | 3 | Now, you can use it in your project: 4 | 5 | ```ts 6 | import { createSpreadsheet, spreadsheet } from 'ts-spreadsheets' 7 | 8 | // Create a spreadsheet 9 | const data = { 10 | headings: ['Name', 'Age', 'City'], 11 | data: [ 12 | ['John Doe', 30, 'Los Angeles'], 13 | ['Jana Schmidt', 25, 'Berlin'], 14 | ['Bob Johnson', 35, 'London'] 15 | ] 16 | } 17 | 18 | // Generate and manipulate spreadsheets 19 | 20 | // 1. Using createSpreadsheet function 21 | const spreadsheet = createSpreadsheet(data) // defaults to csv 22 | const csvSpreadsheet = createSpreadsheet(data, { type: 'csv' }) 23 | const excelSpreadsheet = createSpreadsheet(data, { type: 'excel' }) 24 | 25 | // Store the spreadsheet to disk 26 | await spreadsheet.store('output.csv') 27 | 28 | // Create a download response 29 | const response1 = excelSpreadsheet.download('data.xlsx') // downloads and stores as data.xlsx on your filesystem 30 | 31 | // 2. Using spreadsheet object directly, and chain if desired 32 | const csvContent = spreadsheet(data).generateCSV().store('output2.csv') 33 | const csvContent2 = spreadsheet(data).csv().store('output3.csv') // same as above 34 | 35 | const excelContent = spreadsheet(data).generateExcel() 36 | await excelContent.store('output3.xlsx') 37 | const response2 = await excelContent.download('output3.xlsx') // downloads and stores as output3.xlsx 38 | 39 | // 3. Accessing raw content 40 | const rawCsvContent = spreadsheet(data).csv().getContent() 41 | const rawCsvContent2 = spreadsheet(data).generateCSV().getContent() 42 | const rawExcelContent = spreadsheet(data).excel().getContent() 43 | const rawExcelContent2 = spreadsheet(data).generateExcel().getContent() 44 | 45 | console.log('CSV Content:', rawCsvContent) 46 | console.log('Excel Content:', rawExcelContent) 47 | ``` 48 | 49 | # 📚 API Documentation 50 | 51 | ## Main Functions 52 | 53 | ### spreadsheet(data: Content) 54 | 55 | Creates a spreadsheet object with various methods. 56 | 57 | - `data`: An object containing `headings` and `data` for the spreadsheet. 58 | 59 | Returns an object with the following methods: 60 | 61 | - `csv()`: Generates a CSV SpreadsheetWrapper 62 | - `excel()`: Generates an Excel SpreadsheetWrapper 63 | - `store(path: string)`: Stores the spreadsheet to a file 64 | - `generateCSV()`: Generates a CSV SpreadsheetWrapper 65 | - `generateExcel()`: Generates an Excel SpreadsheetWrapper 66 | 67 | Example: 68 | 69 | ```typescript 70 | const csvWrapper = await spreadsheet(data).csv() // equivalent to spreadsheet(data).generateCSV() 71 | ``` 72 | 73 | ### createSpreadsheet(data: Content, options?: SpreadsheetOptions) 74 | 75 | Creates a SpreadsheetWrapper with the given data and options. 76 | 77 | - `data`: An object containing `headings` and `data` for the spreadsheet. 78 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 79 | 80 | Returns a SpreadsheetWrapper. 81 | 82 | Example: 83 | 84 | ```typescript 85 | const spreadsheet = createSpreadsheet(data, { type: 'csv' }) 86 | ``` 87 | 88 | ## SpreadsheetWrapper Methods 89 | 90 | ### getContent() 91 | 92 | Returns the content of the spreadsheet as a string or Uint8Array. 93 | 94 | ### download(filename: string) 95 | 96 | Creates a download Response for the spreadsheet. 97 | 98 | - `filename`: The name of the file to be downloaded. 99 | 100 | Returns a Response object. 101 | 102 | ### store(path: string) 103 | 104 | Stores the spreadsheet to a file. 105 | 106 | - `path`: The file path where the spreadsheet will be stored. 107 | 108 | Returns a Promise that resolves when the file is written. 109 | 110 | ## Utility Functions 111 | 112 | ### spreadsheet.create(data: Content, options?: SpreadsheetOptions) 113 | 114 | Creates a SpreadsheetContent object. 115 | 116 | - `data`: An object containing `headings` and `data` for the spreadsheet. 117 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 118 | 119 | Returns a SpreadsheetContent object. 120 | 121 | ### spreadsheet.generate(data: Content, options?: SpreadsheetOptions) 122 | 123 | Generates spreadsheet content based on the given data and options. 124 | 125 | - `data`: An object containing `headings` and `data` for the spreadsheet. 126 | - `options`: Optional. An object specifying the spreadsheet type ('csv' or 'excel'). 127 | 128 | Returns a string or Uint8Array representing the spreadsheet content. 129 | 130 | ### spreadsheet.generateCSV(content: Content) 131 | 132 | Generates a CSV SpreadsheetWrapper. 133 | 134 | - `content`: An object containing `headings` and `data` for the spreadsheet. 135 | 136 | Returns a SpreadsheetWrapper for CSV, which can be used to chain other methods like `store()` or `download()`. 137 | 138 | Example: 139 | 140 | ```typescript 141 | await spreadsheet(data).generateCSV().store('output.csv') 142 | 143 | // if one can rely on the file extension to determine the type, you may do this: 144 | await spreadsheet(data).store('output.csv') 145 | ``` 146 | 147 | ### spreadsheet.generateExcel(content: Content) 148 | 149 | Generates an Excel SpreadsheetWrapper. 150 | 151 | - `content`: An object containing `headings` and `data` for the spreadsheet. 152 | 153 | Returns a SpreadsheetWrapper for Excel, which can be used to chain other methods like `store()` or `download()`. 154 | 155 | Example: 156 | 157 | ```ts 158 | await spreadsheet(data).store('output.xlsx') 159 | // or 160 | await spreadsheet(data).generateExcel().store('output.xlsx') 161 | ``` 162 | 163 | To view the full documentation, please visit [https://stacksjs.org/docs/ts-spreadsheets](https://stacksjs.org/docs/ts-spreadsheets). 164 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import type { ESLintConfig } from '@stacksjs/eslint-config' 2 | import stacks from '@stacksjs/eslint-config' 3 | 4 | const config: ESLintConfig = stacks({ 5 | stylistic: { 6 | indent: 2, 7 | quotes: 'single', 8 | }, 9 | 10 | typescript: true, 11 | jsonc: true, 12 | yaml: true, 13 | }) 14 | 15 | export default config 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-spreadsheets", 3 | "type": "module", 4 | "version": "0.2.0", 5 | "description": "Easily generate spreadsheets, like CSVs and Excel files.", 6 | "author": "Chris Breuer ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/stacksjs/ts-spreadsheets#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/stacksjs/ts-spreadsheets.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/stacksjs/ts-spreadsheets/issues" 15 | }, 16 | "keywords": [ 17 | "spreadsheet", 18 | "csv", 19 | "excel", 20 | "export", 21 | "functional", 22 | "functions", 23 | "bun", 24 | "node" 25 | ], 26 | "exports": { 27 | ".": { 28 | "bun": "./src/index.ts", 29 | "types": "./dist/index.d.ts", 30 | "import": "./dist/index.js" 31 | }, 32 | "./*": { 33 | "bun": "./src/*", 34 | "import": "./dist/*" 35 | } 36 | }, 37 | "module": "./dist/index.js", 38 | "types": "./dist/index.d.ts", 39 | "bin": { 40 | "spreadsheets": "./dist/bin/cli.js" 41 | }, 42 | "files": [ 43 | "dist", 44 | "src" 45 | ], 46 | "scripts": { 47 | "build": "bun build.ts && bun run compile", 48 | "compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/spreadsheets", 49 | "compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64", 50 | "compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/spreadsheets-linux-x64", 51 | "compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/spreadsheets-linux-arm64", 52 | "compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/spreadsheets-windows-x64.exe", 53 | "compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/spreadsheets-darwin-x64", 54 | "compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/spreadsheets-darwin-arm64", 55 | "lint": "bunx --bun eslint .", 56 | "lint:fix": "bunx --bun eslint . --fix", 57 | "fresh": "bunx rimraf node_modules/ bun.lock && bun i", 58 | "changelog": "bunx changelogen --output CHANGELOG.md", 59 | "prepublishOnly": "bun --bun run build && bun run compile:all", 60 | "release": "bun run changelog && bunx bumpp package.json --all", 61 | "test": "bun test", 62 | "dev:docs": "bun --bun vitepress dev docs", 63 | "build:docs": "bun --bun vitepress build docs", 64 | "preview:docs": "bun --bun vitepress preview docs", 65 | "typecheck": "bun --bun tsc --noEmit" 66 | }, 67 | "devDependencies": { 68 | "@stacksjs/docs": "^0.70.23", 69 | "@stacksjs/eslint-config": "^4.2.1-beta.1", 70 | "@types/bun": "^1.2.13", 71 | "bumpp": "^10.1.1", 72 | "bun-plugin-dtsx": "^0.21.12", 73 | "changelogen": "^0.6.1", 74 | "lint-staged": "^15.5.2", 75 | "simple-git-hooks": "^2.13.0", 76 | "typescript": "^5.8.3", 77 | "unocss": "66.1.2" 78 | }, 79 | "overrides": { 80 | "unconfig": "0.3.10" 81 | }, 82 | "simple-git-hooks": { 83 | "pre-commit": "bun lint-staged" 84 | }, 85 | "lint-staged": { 86 | "*.{js,ts}": "bunx eslint . --fix" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkgx.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | bun.sh: ^1.2.2 3 | -------------------------------------------------------------------------------- /spreadsheets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | import('./bin/cli') 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Content, 3 | FileExtension, 4 | Spreadsheet, 5 | SpreadsheetContent, 6 | SpreadsheetOptions, 7 | SpreadsheetType, 8 | } from './types' 9 | import { Buffer } from 'node:buffer' 10 | import { readFile, writeFile } from 'node:fs/promises' 11 | import { gzipSync } from 'node:zlib' 12 | 13 | export const spreadsheet: Spreadsheet = Object.assign( 14 | (data: Content) => ({ 15 | csv: () => spreadsheet.generateCSV(data), 16 | excel: () => spreadsheet.generateExcel(data), 17 | store: async (path: string) => { 18 | const extension = path.slice(path.lastIndexOf('.')) as FileExtension 19 | const type = extension === '.csv' ? 'csv' : 'excel' 20 | const content = spreadsheet.generate(data, { type }) 21 | await spreadsheet.store({ content, type }, path) 22 | }, 23 | generateCSV: () => spreadsheet.generateCSV(data), 24 | generateExcel: () => spreadsheet.generateExcel(data), 25 | }), 26 | { 27 | generate: (data: Content, options: SpreadsheetOptions = { type: 'csv' }): string | Uint8Array => { 28 | const generators: Record string | Uint8Array | SpreadsheetWrapper> = { 29 | csv: spreadsheet.generateCSV, 30 | excel: spreadsheet.generateExcel, 31 | } 32 | 33 | const generator = generators[options.type || 'csv'] 34 | 35 | if (!generator) { 36 | throw new Error(`Unsupported spreadsheet type: ${options.type}`) 37 | } 38 | 39 | const result = generator(data) 40 | if (result instanceof SpreadsheetWrapper) { 41 | return result.getContent() 42 | } 43 | 44 | return result 45 | }, 46 | 47 | create: (data: Content, options: SpreadsheetOptions = { type: 'csv' }): SpreadsheetContent => ({ 48 | content: spreadsheet.generate(data, options), 49 | type: options.type || 'csv', 50 | }), 51 | 52 | generateCSV: (content: Content): SpreadsheetWrapper => { 53 | const csvContent = generateCSVContent(content) 54 | return new SpreadsheetWrapper(csvContent, 'csv') 55 | }, 56 | 57 | generateExcel: (content: Content): SpreadsheetWrapper => { 58 | const excelContent = generateExcelContent(content) 59 | return new SpreadsheetWrapper(excelContent, 'excel') 60 | }, 61 | 62 | store: async ({ content }: SpreadsheetContent, path: string): Promise => { 63 | try { 64 | await writeFile(path, content) 65 | } 66 | catch (error) { 67 | throw new Error(`Failed to store spreadsheet: ${(error as Error).message}`) 68 | } 69 | }, 70 | 71 | download: ({ content, type }: SpreadsheetContent, filename: string): Response => { 72 | const mimeType = type === 'csv' ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 73 | const blob = new Blob([content], { type: mimeType }) 74 | 75 | return new Response(blob, { 76 | headers: { 77 | 'Content-Type': mimeType, 78 | 'Content-Disposition': `attachment; filename="${filename}"`, 79 | }, 80 | }) 81 | }, 82 | }, 83 | ) 84 | 85 | export class SpreadsheetWrapper { 86 | constructor( 87 | private content: string | Uint8Array, 88 | private type: SpreadsheetType, 89 | ) {} 90 | 91 | getContent(): string | Uint8Array { 92 | return this.content 93 | } 94 | 95 | download(filename: string): Response { 96 | return spreadsheet.download({ content: this.content, type: this.type }, filename) 97 | } 98 | 99 | store(path: string): Promise { 100 | return spreadsheet.store({ content: this.content, type: this.type }, path) 101 | } 102 | } 103 | 104 | export function createSpreadsheet(data: Content, options: SpreadsheetOptions = { type: 'csv' }): SpreadsheetWrapper { 105 | const content = spreadsheet.generate(data, options) 106 | 107 | return new SpreadsheetWrapper(content, options.type || 'csv') 108 | } 109 | 110 | export function generateCSVContent(content: Content): string { 111 | const rows = [content.headings, ...content.data] 112 | 113 | return rows 114 | .map(row => 115 | row 116 | .map((cell) => { 117 | const cellString = String(cell) 118 | if (cellString.includes(',') || cellString.includes('"') || cellString.includes('\n')) { 119 | return `"${cellString.replace(/"/g, '""')}"` 120 | } 121 | return cellString 122 | }) 123 | .join(','), 124 | ) 125 | .join('\n') 126 | } 127 | 128 | export function generateExcelContent(content: Content): Uint8Array { 129 | const workbook = new Uint8Array( 130 | Buffer.from(` 131 | 132 | 133 | 134 | 135 | `), 136 | ) 137 | 138 | const worksheet = ` 139 | 140 | 141 | ${[content.headings, ...content.data] 142 | .map( 143 | (row, rowIndex) => ` 144 | 145 | ${row 146 | .map( 147 | (cell, cellIndex) => ` 148 | 149 | ${typeof cell === 'string' ? cell.replace(/&/g, '&').replace(//g, '>') : cell} 150 | `, 151 | ) 152 | .join('')} 153 | `, 154 | ) 155 | .join('')} 156 | 157 | ` 158 | 159 | const contentTypes = ` 160 | 161 | 162 | 163 | 164 | 165 | ` 166 | 167 | const rels = ` 168 | 169 | 170 | ` 171 | 172 | const files: Array<{ name: string, content: Uint8Array }> = [ 173 | { name: '[Content_Types].xml', content: new Uint8Array(Buffer.from(contentTypes)) }, 174 | { name: '_rels/.rels', content: new Uint8Array(Buffer.from(rels)) }, 175 | { name: 'xl/workbook.xml', content: workbook }, 176 | { name: 'xl/worksheets/sheet1.xml', content: new Uint8Array(Buffer.from(worksheet)) }, 177 | ] 178 | 179 | const zipData = files.map((file) => { 180 | const compressedContent = new Uint8Array(gzipSync(file.content)) 181 | const header = new Uint8Array(30 + file.name.length) 182 | const headerView = new DataView(header.buffer) 183 | 184 | headerView.setUint32(0, 0x04034B50, true) // 'PK\x03\x04' 185 | headerView.setUint32(4, 0x0008, true) 186 | headerView.setUint32(18, compressedContent.length, true) 187 | headerView.setUint32(22, file.content.length, true) 188 | headerView.setUint16(26, file.name.length, true) 189 | 190 | const encoder = new TextEncoder() 191 | header.set(encoder.encode(file.name), 30) 192 | 193 | return { header, compressedContent } 194 | }) 195 | 196 | const centralDirectory = files.map((file, index) => { 197 | const header = new Uint8Array(46 + file.name.length) 198 | const headerView = new DataView(header.buffer) 199 | 200 | headerView.setUint32(0, 0x02014B50, true) // 'PK\x01\x02' 201 | headerView.setUint16(4, 0x0014, true) 202 | headerView.setUint16(6, 0x0008, true) 203 | headerView.setUint32(8, 0x0008, true) 204 | headerView.setUint32(20, zipData[index].compressedContent.length - 30 - file.name.length, true) 205 | headerView.setUint32( 206 | 42, 207 | zipData.slice(0, index).reduce((acc, curr) => acc + curr.header.length + curr.compressedContent.length, 0), 208 | true, 209 | ) 210 | headerView.setUint16(28, file.name.length, true) 211 | 212 | const encoder = new TextEncoder() 213 | header.set(encoder.encode(file.name), 46) 214 | 215 | return header 216 | }) 217 | 218 | const endOfCentralDirectory = new Uint8Array(22) 219 | 220 | const totalSize 221 | = zipData.reduce((acc, { header, compressedContent }) => acc + header.length + compressedContent.length, 0) 222 | + centralDirectory.reduce((acc, header) => acc + header.length, 0) 223 | + endOfCentralDirectory.length 224 | 225 | // Create a single Uint8Array with the total size 226 | const result = new Uint8Array(totalSize) 227 | 228 | // Copy data into the result array 229 | let offset = 0 230 | for (const { header, compressedContent } of zipData) { 231 | result.set(header, offset) 232 | offset += header.length 233 | result.set(compressedContent, offset) 234 | offset += compressedContent.length 235 | } 236 | for (const header of centralDirectory) { 237 | result.set(header, offset) 238 | offset += header.length 239 | } 240 | result.set(endOfCentralDirectory, offset) 241 | 242 | return result 243 | } 244 | 245 | export async function csvToContent(csvPath: string): Promise { 246 | const csvContent = await readFile(csvPath, 'utf-8') 247 | 248 | // Split into lines and parse CSV 249 | const lines = csvContent.split('\n').map(line => 250 | line.split(',').map((cell) => { 251 | const trimmed = cell.trim() 252 | // Remove quotes if present 253 | if (trimmed.startsWith('"') && trimmed.endsWith('"')) { 254 | // Handle escaped quotes 255 | return trimmed.slice(1, -1).replace(/""/g, '"') 256 | } 257 | return trimmed 258 | }), 259 | ) 260 | 261 | // First line is headers 262 | const [headings, ...data] = lines 263 | 264 | // Convert numeric strings to numbers 265 | const typedData = data.map(row => 266 | row.map((cell) => { 267 | const num = Number(cell) 268 | return !Number.isNaN(num) && cell.trim() !== '' ? num : cell 269 | }), 270 | ) 271 | 272 | return { 273 | headings, 274 | data: typedData, 275 | } 276 | } 277 | 278 | export * from './types' 279 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { SpreadsheetWrapper } from './' 2 | 3 | /** 4 | * Content is the data structure that represents the spreadsheet content. 5 | * 6 | * @example 7 | * const content: Content = { 8 | * headings: ['Name', 'Age', 'City'], 9 | * data: [ 10 | * ['John Doe', 30, 'New York'], 11 | * ['Jane Smith', 25, 'London'], 12 | * ['Bob Johnson', 35, 'Paris'] 13 | * ] 14 | * } 15 | */ 16 | export interface Content { 17 | headings: string[] 18 | data: (string | number)[][] 19 | } 20 | 21 | export type SpreadsheetType = 'csv' | 'excel' 22 | 23 | export interface SpreadsheetContent { 24 | content: string | Uint8Array 25 | type: SpreadsheetType 26 | } 27 | 28 | export type SpreadsheetOptions = Partial<{ 29 | type: SpreadsheetType 30 | }> 31 | 32 | export type FileExtension = '.csv' | '.xlsx' 33 | 34 | export interface Spreadsheet { 35 | ( 36 | data: Content, 37 | ): { 38 | csv: () => SpreadsheetWrapper 39 | excel: () => SpreadsheetWrapper 40 | store: (path: string) => Promise 41 | generateCSV: () => SpreadsheetWrapper 42 | generateExcel: () => SpreadsheetWrapper 43 | } 44 | create: (data: Content, options: SpreadsheetOptions) => SpreadsheetContent 45 | generate: (data: Content, options: SpreadsheetOptions) => string | Uint8Array 46 | generateCSV: (content: Content) => SpreadsheetWrapper 47 | generateExcel: (content: Content) => SpreadsheetWrapper 48 | store: (spreadsheet: SpreadsheetContent, path: string) => Promise 49 | download: (spreadsheet: SpreadsheetContent, filename: string) => Response 50 | } 51 | -------------------------------------------------------------------------------- /test/spreadsheets.test.ts: -------------------------------------------------------------------------------- 1 | import type { Content } from '../src/types' 2 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test' 3 | import { spawnSync } from 'node:child_process' 4 | import { existsSync, mkdirSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs' 5 | import { resolve } from 'node:path' 6 | import { createSpreadsheet, spreadsheet } from '../src/index' 7 | 8 | function runCLI(command: string): string { 9 | try { 10 | // Use proper path handling 11 | const cliPath = resolve(__dirname, '../bin/cli.ts') 12 | 13 | // Don't split the command by spaces, pass it as a single argument 14 | const result = spawnSync('bun', [cliPath, ...command.match(/(?:[^\s"]|"[^"]*")+/g)?.map(arg => 15 | arg.startsWith('"') && arg.endsWith('"') ? arg.slice(1, -1) : arg, 16 | ) || []], { 17 | encoding: 'utf8', 18 | env: { 19 | ...process.env, 20 | NODE_ENV: 'test', 21 | NO_COLOR: '1', 22 | }, 23 | cwd: process.cwd(), // Ensure we're in the right directory 24 | }) 25 | 26 | // Combine stdout and stderr 27 | const output = [ 28 | result.stdout?.trim(), 29 | result.stderr?.trim(), 30 | ].filter(Boolean).join('\n') 31 | 32 | if (result.error) { 33 | console.error('CLI execution error:', result.error) 34 | } 35 | 36 | return output || '' 37 | } 38 | catch (error) { 39 | console.error('Unexpected error in runCLI:', error) 40 | return error instanceof Error ? error.message : String(error) 41 | } 42 | } 43 | 44 | // Helper function to wait for file to exist 45 | async function waitForFile(filepath: string, timeoutMs = 2000): Promise { 46 | const startTime = Date.now() 47 | while (Date.now() - startTime < timeoutMs) { 48 | if (existsSync(filepath)) 49 | return true 50 | await new Promise(resolve => setTimeout(resolve, 100)) 51 | } 52 | return false 53 | } 54 | 55 | describe('ts-spreadsheets', () => { 56 | let testData: Content 57 | 58 | beforeEach(() => { 59 | testData = { 60 | headings: ['Name', 'Age', 'City'], 61 | data: [ 62 | ['John Doe', 30, 'New York'], 63 | ['Jane Smith', 25, 'London'], 64 | ['Bob Johnson', 35, 'Paris'], 65 | ], 66 | } 67 | }) 68 | 69 | afterEach(() => { 70 | // Clean up any files created during tests 71 | const filesToDelete = ['output.csv', 'output.xlsx'] 72 | filesToDelete.forEach((file) => { 73 | if (existsSync(file)) 74 | unlinkSync(file) 75 | }) 76 | }) 77 | 78 | describe('Content Creation', () => { 79 | it('should create valid Content object', () => { 80 | expect(testData.headings.length).toBe(3) 81 | expect(testData.data.length).toBe(3) 82 | expect(testData.data[0].length).toBe(3) 83 | }) 84 | 85 | it('should handle empty data', () => { 86 | const emptyData: Content = { headings: [], data: [] } 87 | expect(() => createSpreadsheet(emptyData)).not.toThrow() 88 | }) 89 | 90 | it('should handle single row data', () => { 91 | const singleRowData: Content = { 92 | headings: ['Test'], 93 | data: [['Value']], 94 | } 95 | expect(() => createSpreadsheet(singleRowData)).not.toThrow() 96 | }) 97 | }) 98 | 99 | describe('CSV Generation', () => { 100 | it('should generate valid CSV content', () => { 101 | const csvContent = spreadsheet(testData).csv().getContent() as string 102 | const lines = csvContent.split('\n') 103 | expect(lines[0]).toBe('Name,Age,City') 104 | expect(lines[1]).toBe('John Doe,30,New York') 105 | }) 106 | 107 | it('should handle special characters in CSV', () => { 108 | const specialData: Content = { 109 | headings: ['Name', 'Description'], 110 | data: [['John, Doe', 'Likes "quotes"']], 111 | } 112 | const csvContent = spreadsheet(specialData).csv().getContent() as string 113 | expect(csvContent).toBe('Name,Description\n"John, Doe","Likes ""quotes"""') 114 | }) 115 | 116 | it('should correctly store numbers in CSV', () => { 117 | const numericData: Content = { 118 | headings: ['Name', 'Age', 'Score'], 119 | data: [ 120 | ['Alice', 28, 95.5], 121 | ['Bob', 32, 88], 122 | ['Charlie', 45, 72.75], 123 | ], 124 | } 125 | const csvContent = spreadsheet(numericData).csv().getContent() as string 126 | const lines = csvContent.split('\n') 127 | expect(lines[0]).toBe('Name,Age,Score') 128 | expect(lines[1]).toBe('Alice,28,95.5') 129 | expect(lines[2]).toBe('Bob,32,88') 130 | expect(lines[3]).toBe('Charlie,45,72.75') 131 | }) 132 | 133 | it('should handle empty cells', () => { 134 | const dataWithEmpty: Content = { 135 | headings: ['Col1', 'Col2'], 136 | data: [['', 'value'], ['value', '']], 137 | } 138 | const csvContent = spreadsheet(dataWithEmpty).csv().getContent() as string 139 | expect(csvContent).toBe('Col1,Col2\n,value\nvalue,') 140 | }) 141 | }) 142 | 143 | describe('Excel Generation', () => { 144 | it('should generate valid Excel content', () => { 145 | const excelContent = spreadsheet(testData).excel().getContent() as Uint8Array 146 | expect(excelContent).toBeInstanceOf(Uint8Array) 147 | expect(excelContent.length).toBeGreaterThan(0) 148 | }) 149 | 150 | it('should generate larger Excel files correctly', () => { 151 | const largeData: Content = { 152 | headings: ['ID', 'Value'], 153 | data: Array.from({ length: 1000 }, (_, i) => [i, `Value ${i}`]), 154 | } 155 | const excelContent = spreadsheet(largeData).excel().getContent() as Uint8Array 156 | expect(excelContent).toBeInstanceOf(Uint8Array) 157 | expect(excelContent.length).toBeGreaterThan(0) 158 | }) 159 | }) 160 | 161 | describe('File Storage', () => { 162 | it('should store CSV file', async () => { 163 | await spreadsheet(testData).store('output.csv') 164 | expect(existsSync('output.csv')).toBe(true) 165 | }) 166 | 167 | it('should store Excel file', async () => { 168 | await spreadsheet(testData).store('output.xlsx') 169 | expect(existsSync('output.xlsx')).toBe(true) 170 | }) 171 | 172 | it('should handle file overwrite', async () => { 173 | await spreadsheet(testData).store('output.csv') 174 | await spreadsheet(testData).store('output.csv') 175 | expect(existsSync('output.csv')).toBe(true) 176 | }) 177 | }) 178 | 179 | describe('Download Response', () => { 180 | it('should create valid download response for CSV', () => { 181 | const response = spreadsheet(testData).csv().download('test.csv') 182 | expect(response).toBeInstanceOf(Response) 183 | expect(response.headers.get('Content-Type')).toBe('text/csv') 184 | expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="test.csv"') 185 | }) 186 | 187 | it('should create valid download response for Excel', () => { 188 | const response = spreadsheet(testData).excel().download('test.xlsx') 189 | expect(response).toBeInstanceOf(Response) 190 | expect(response.headers.get('Content-Type')).toBe( 191 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 192 | ) 193 | expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="test.xlsx"') 194 | }) 195 | }) 196 | 197 | describe('Method Chaining', () => { 198 | it('should support method chaining', async () => { 199 | await spreadsheet(testData).csv().store('output.csv') 200 | expect(existsSync('output.csv')).toBe(true) 201 | }) 202 | 203 | it('should support multiple operations', async () => { 204 | const result = spreadsheet(testData) 205 | await result.csv().store('output.csv') 206 | await result.excel().store('output.xlsx') 207 | expect(existsSync('output.csv')).toBe(true) 208 | expect(existsSync('output.xlsx')).toBe(true) 209 | }) 210 | }) 211 | 212 | describe('Error Handling', () => { 213 | it('should throw error for unsupported spreadsheet type', () => { 214 | // @ts-expect-error: Testing invalid type 215 | expect(() => createSpreadsheet(testData, { type: 'pdf' })).toThrow() 216 | }) 217 | 218 | it('should handle invalid file paths', async () => { 219 | await expect( 220 | spreadsheet(testData).store('/invalid/path/file.csv'), 221 | ).rejects.toThrow() 222 | }) 223 | }) 224 | 225 | describe('CLI Integration', () => { 226 | const testDir = resolve(process.cwd(), '.test-output') 227 | let testData: Content 228 | let testFilePath: string 229 | 230 | beforeEach(() => { 231 | // Ensure test directory exists 232 | mkdirSync(testDir, { recursive: true }) 233 | 234 | testData = { 235 | headings: ['Name', 'Age', 'City'], 236 | data: [ 237 | ['John Doe', 30, 'New York'], 238 | ['Jane Smith', 25, 'London'], 239 | ['Bob Johnson', 35, 'Paris'], 240 | ], 241 | } 242 | testFilePath = resolve(testDir, 'test-input.json') 243 | writeFileSync(testFilePath, JSON.stringify(testData, null, 2)) 244 | }) 245 | 246 | afterEach(() => { 247 | if (existsSync(testDir)) { 248 | readdirSync(testDir).forEach((file) => { 249 | unlinkSync(resolve(testDir, file)) 250 | }) 251 | rmdirSync(testDir) 252 | } 253 | }) 254 | 255 | describe('create command', () => { 256 | it('should create CSV file from JSON input', async () => { 257 | const outputPath = resolve(testDir, 'output.csv') 258 | const output = runCLI(`create "${testFilePath}" -o "${outputPath}"`) 259 | 260 | await waitForFile(outputPath) 261 | expect(existsSync(outputPath)).toBe(true) 262 | expect(output).toContain('Spreadsheet saved') 263 | }) 264 | 265 | it('should create Excel file from JSON input', async () => { 266 | const outputPath = resolve(testDir, 'output.xlsx') 267 | const output = runCLI(`create "${testFilePath}" --type excel -o "${outputPath}"`) 268 | 269 | await waitForFile(outputPath) 270 | expect(existsSync(outputPath)).toBe(true) 271 | expect(output).toContain('Spreadsheet saved') 272 | }) 273 | 274 | it('should output to stdout when no output file specified', () => { 275 | const output = runCLI(`create "${testFilePath}"`) 276 | expect(output).toContain('Name,Age,City') 277 | }) 278 | 279 | it('should handle invalid JSON input', () => { 280 | const invalidPath = resolve(testDir, 'invalid.json') 281 | writeFileSync(invalidPath, '{ invalid json }') 282 | const output = runCLI(`create "${invalidPath}"`) 283 | expect(output).toContain('Failed to create spreadsheet') 284 | }) 285 | }) 286 | 287 | describe('convert command', () => { 288 | it('should convert CSV to Excel', async () => { 289 | const csvPath = resolve(testDir, 'test-output.csv') 290 | const xlsxPath = resolve(testDir, 'test-output.xlsx') 291 | 292 | // First create CSV 293 | runCLI(`create "${testFilePath}" -o "${csvPath}"`) 294 | await waitForFile(csvPath) 295 | 296 | // Then convert 297 | const output = runCLI(`convert "${csvPath}" "${xlsxPath}"`) 298 | await waitForFile(xlsxPath) 299 | 300 | expect(existsSync(xlsxPath)).toBe(true) 301 | expect(output).toContain('Converted') 302 | }) 303 | 304 | it('should warn when input and output formats are the same', async () => { 305 | const csvPath = resolve(testDir, 'test-output.csv') 306 | const samePath = resolve(testDir, 'same-output.csv') 307 | 308 | runCLI(`create "${testFilePath}" -o "${csvPath}"`) 309 | await waitForFile(csvPath) 310 | 311 | const output = runCLI(`convert "${csvPath}" "${samePath}"`) 312 | expect(output).toContain('same') 313 | }) 314 | 315 | it('should handle invalid input file', () => { 316 | const nonexistentPath = resolve(testDir, 'nonexistent.csv') 317 | const outputPath = resolve(testDir, 'output.xlsx') 318 | const output = runCLI(`convert "${nonexistentPath}" "${outputPath}"`) 319 | expect(output).toContain('Failed to convert spreadsheet') 320 | }) 321 | }) 322 | 323 | describe('validate command', () => { 324 | it('should validate correct JSON format', () => { 325 | const output = runCLI(`validate "${testFilePath}"`) 326 | expect(output).toContain('Input JSON is valid') 327 | }) 328 | 329 | it('should catch missing headings', () => { 330 | const invalidPath = resolve(testDir, 'invalid.json') 331 | const invalidData = { data: [['test']] } 332 | writeFileSync(invalidPath, JSON.stringify(invalidData)) 333 | const output = runCLI(`validate "${invalidPath}"`) 334 | expect(output).toContain('Missing or invalid headings array') 335 | }) 336 | 337 | it('should catch invalid data types', () => { 338 | const invalidPath = resolve(testDir, 'invalid.json') 339 | const invalidData = { 340 | headings: ['Test'], 341 | data: [[{ invalid: 'object' }]], 342 | } 343 | writeFileSync(invalidPath, JSON.stringify(invalidData)) 344 | const output = runCLI(`validate "${invalidPath}"`) 345 | expect(output).toContain('Data must be an array of arrays') 346 | }) 347 | 348 | it('should catch non-string headings', () => { 349 | const invalidPath = resolve(testDir, 'invalid.json') 350 | const invalidData = { 351 | headings: [42], 352 | data: [['test']], 353 | } 354 | writeFileSync(invalidPath, JSON.stringify(invalidData)) 355 | const output = runCLI(`validate "${invalidPath}"`) 356 | expect(output).toContain('Headings must be strings') 357 | }) 358 | }) 359 | }) 360 | }) 361 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "moduleDetection": "force", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "resolveJsonModule": true, 9 | "types": ["bun"], 10 | "allowImportingTsExtensions": true, 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "declaration": true, 15 | "noEmit": true, 16 | "esModuleInterop": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "isolatedDeclarations": true, 19 | "isolatedModules": true, 20 | "verbatimModuleSyntax": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true 23 | } 24 | } 25 | --------------------------------------------------------------------------------