web-csv-toolbox 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  <div align="center">
2
2
 
3
- [![Node.js CI](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml/badge.svg)](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml)
4
3
  [![npm version](https://badge.fury.io/js/web-csv-toolbox.svg)](https://badge.fury.io/js/web-csv-toolbox)
5
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
5
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
@@ -38,9 +37,8 @@ A CSV Toolbox utilizing Web Standard APIs.
38
37
  - Using only Web Standards APIs.
39
38
  - 💪 **Property-based testing.**
40
39
  - Using [fast-check](https://fast-check.dev/) and [vitest](https://vitest.dev).
41
- - ✅ **_To Be Tested_** **Cross-platform.**
40
+ - ✅ **Cross-platform.**
42
41
  - Works on browsers, Node.js, and Deno.
43
- - Only web standard APIs are used, so it should work with these runtimes.
44
42
 
45
43
  ## Key Features 📗
46
44
 
@@ -58,6 +56,10 @@ A CSV Toolbox utilizing Web Standard APIs.
58
56
 
59
57
  ## Installation 📥
60
58
 
59
+ ### With Package manager 📦
60
+
61
+ This package can then be installed using a package manager.
62
+
61
63
  ```sh
62
64
  # Install with npm
63
65
  $ npm install web-csv-toolbox
@@ -67,6 +69,50 @@ $ yarn add web-csv-toolbox
67
69
  $ pnpm add web-csv-toolbox
68
70
  ```
69
71
 
72
+ ### From CDN (unpkg.com) 🌐
73
+
74
+ #### UMD Style 🔄
75
+
76
+ ```html
77
+ <script src="https://unpkg.com/web-csv-toolbox"></script>
78
+ <script>
79
+ const csv = `name,age
80
+ Alice,42
81
+ Bob,69`;
82
+
83
+ (async function () {
84
+ for await (const record of CSV.parse(csv)) {
85
+ console.log(record);
86
+ }
87
+ })();
88
+ </script>
89
+ ```
90
+
91
+
92
+ #### ESModule Style 📦
93
+
94
+ ```html
95
+ <script type="module">
96
+ import { parse } from 'https://unpkg.com/web-csv-toolbox/lib/index.js';
97
+
98
+ const csv = `name,age
99
+ Alice,42
100
+ Bob,69`;
101
+
102
+ for await (const record of parse(csv)) {
103
+ console.log(record);
104
+ }
105
+ </script>
106
+ ```
107
+
108
+ #### Deno 🦕
109
+
110
+ You can install and use the package by specifying the following:
111
+
112
+ ```js
113
+ import { parse } from "npm:web-csv-toolbox";
114
+ ```
115
+
70
116
  ## Usage 📘
71
117
 
72
118
  ### Parsing CSV files from strings
@@ -158,6 +204,30 @@ for await (const record of parse(csv, { headers: ['name', 'age'] })) {
158
204
  // { name: 'Bob', age: '69' }
159
205
  ```
160
206
 
207
+ ## Supported Runtimes 💻
208
+
209
+ ### Works on Node.js [![Node.js CI](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml/badge.svg)](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml)
210
+
211
+ | Versions | Status |
212
+ | -------- | ------ |
213
+ | 20.x | ✅ |
214
+ | 18.x | ✅ |
215
+
216
+
217
+ ### Works on Browser [![Browsers CI](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/browsers.yaml/badge.svg)](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/browsers.yaml)
218
+
219
+ | OS | Chrome | FireFox | Default |
220
+ | ------- | ------ | ------- | ------------- |
221
+ | Windows | ✅ | ✅ | ✅ (Edge) |
222
+ | macos | ✅ | ✅ | ⬜ (Safari *) |
223
+ | Linux | ✅ | ✅ | - |
224
+
225
+ > **\* To Be Tested**: [I couldn't launch Safari in headless mode](https://github.com/vitest-dev/vitest/blob/main/packages/browser/src/node/providers/webdriver.ts#L39-L41) on GitHub Actions, so I couldn't verify it, but it probably works.
226
+
227
+ ### Others
228
+
229
+ - Verify that JavaScript is executable on the Deno. [![Deno CI](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/deno.yaml/badge.svg)](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/deno.yaml)
230
+
161
231
  ## APIs 🧑‍💻
162
232
 
163
233
  ### High-level APIs 🚀
@@ -204,20 +274,20 @@ ideal for developers looking for in-depth control and flexibility.
204
274
 
205
275
  ### Common Options ⚙️
206
276
 
207
- | Option | Description | Default | Notes |
208
- | -------------- | ------------------------------------- | ----------- | ----- |
209
- | `delimiter` | Character to separate fields | `,` | |
210
- | `quotation` | Character used for quoting fields | `"` | |
211
- | `headers` | Custom headers for the parsed records | First row | If not provided, the first row is used as headers |
277
+ | Option | Description | Default | Notes |
278
+ | ----------- | ------------------------------------- | --------- | ------------------------------------------------- |
279
+ | `delimiter` | Character to separate fields | `,` | |
280
+ | `quotation` | Character used for quoting fields | `"` | |
281
+ | `headers` | Custom headers for the parsed records | First row | If not provided, the first row is used as headers |
212
282
 
213
283
  ### Advanced Options (Binary-Specific) 🧬
214
284
 
215
- | Option | Description | Default | Notes |
216
- | --------------- | ------------------------------------------------- | --------- | ---------------------- |
217
- | `charset` | Character encoding for binary CSV inputs | `utf-8` | See [Encoding API Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings) for the encoding formats that can be specified. |
218
- | `decompression` | Decompression algorithm for compressed CSV inputs | | See [DecompressionStream Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream#browser_compatibilit). |
219
- | `ignoreBOM` | Whether to ignore Byte Order Mark (BOM) | `false` | See [TextDecoderOptions.ignoreBOM](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/ignoreBOM) for more information about the BOM. |
220
- | `fatal` | Throw an error on invalid characters | `false` | See [TextDecoderOptions.fatal](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/fatal) for more information. |
285
+ | Option | Description | Default | Notes |
286
+ | --------------- | ------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | `charset` | Character encoding for binary CSV inputs | `utf-8` | See [Encoding API Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings) for the encoding formats that can be specified. |
288
+ | `decompression` | Decompression algorithm for compressed CSV inputs | | See [DecompressionStream Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream#browser_compatibilit). |
289
+ | `ignoreBOM` | Whether to ignore Byte Order Mark (BOM) | `false` | See [TextDecoderOptions.ignoreBOM](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/ignoreBOM) for more information about the BOM. |
290
+ | `fatal` | Throw an error on invalid characters | `false` | See [TextDecoderOptions.fatal](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/fatal) for more information. |
221
291
 
222
292
  ## How to Contribute 💪
223
293
 
package/lib/index.js CHANGED
@@ -122,7 +122,7 @@ class LexerTransformer extends TransformStream {
122
122
  if (flush === false && this.#buffer.endsWith(this.#quotation)) {
123
123
  return null;
124
124
  }
125
- return this.extractQuotedString();
125
+ return this.extractQuotedString(flush);
126
126
  }
127
127
  const match = this.#matcher.exec(this.#buffer);
128
128
  if (match) {
@@ -134,7 +134,7 @@ class LexerTransformer extends TransformStream {
134
134
  }
135
135
  return null;
136
136
  }
137
- extractQuotedString() {
137
+ extractQuotedString(flush) {
138
138
  let end = this.#quotationLength;
139
139
  let value = "";
140
140
  while (end < this.#buffer.length) {
@@ -153,6 +153,24 @@ class LexerTransformer extends TransformStream {
153
153
  if (
154
154
  this.#buffer.slice(end, end + this.#quotationLength) === this.quotation
155
155
  ) {
156
+ if (
157
+ flush === false &&
158
+ end + this.#quotationLength < this.#buffer.length &&
159
+ this.#buffer.slice(
160
+ end + this.#quotationLength,
161
+ this.#demiliterLength,
162
+ ) !== this.demiliter &&
163
+ this.#buffer.slice(
164
+ end + this.#quotationLength,
165
+ end + this.#quotationLength + 2,
166
+ ) !== CRLF &&
167
+ this.#buffer.slice(
168
+ end + this.#quotationLength,
169
+ end + this.#quotationLength + 1,
170
+ ) !== LF
171
+ ) {
172
+ return null;
173
+ }
156
174
  this.#buffer = this.#buffer.slice(end + this.#quotationLength);
157
175
  return { type: Field, value };
158
176
  }
package/lib/index.umd.js CHANGED
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).CSV={})}(this,(function(e){"use strict";const t=Symbol.for("web-streams-csv.FieldDelimiter"),i=Symbol.for("web-streams-csv.RecordDelimiter"),r=Symbol.for("web-streams-csv.Field"),n="\n";function s(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}class o extends TransformStream{#e;#t;#i;#r;#n;#s="";get demiliter(){return this.#e}get quotation(){return this.#i}constructor({demiliter:e=",",quotation:t='"'}={}){!function(e){if("string"==typeof e.quotation&&0===e.quotation.length)throw new Error("quotation must not be empty");if("string"==typeof e.demiliter&&0===e.demiliter.length)throw new Error("demiliter must not be empty");if(e.quotation.includes(n)||e.quotation.includes("\r"))throw new Error("quotation must not include CR or LF");if(e.demiliter.includes(n)||e.demiliter.includes("\r"))throw new Error("demiliter must not include CR or LF");if(e.demiliter.includes(e.quotation)||e.quotation.includes(e.demiliter))throw new Error("demiliter and quotation must not include each other as a substring")}({demiliter:e,quotation:t}),super({transform:(e,t)=>{if(0!==e.length){this.#s+=e;for(const e of this.#o({flush:!1}))t.enqueue(e)}},flush:e=>{for(const t of this.#o({flush:!0}))e.enqueue(t)}}),this.#e=e,this.#t=e.length,this.#i=t,this.#r=t.length;const i=s(e),r=s(t);this.#n=new RegExp(`^(?:(?!${r})(?!${i})(?![\\r\\n]))([\\S\\s\\uFEFF\\xA0]+?)(?=${r}|${i}|\\r|\\n|$)`)}*#o({flush:e}){let n=null;for(let s;s=this.#a({flush:e});)switch(s.type){case r:n?n.value+=s.value:n=s;break;case t:case i:n&&(yield n,n=null),yield s}n&&(yield n)}#a({flush:e=!1}={}){if(0===this.#s.length)return null;if(this.#s.startsWith("\r\n"))return this.#s=this.#s.slice(2),{type:i,value:"\r\n"};if(this.#s.startsWith(n))return this.#s=this.#s.slice(1),{type:i,value:n};if(this.#s.startsWith(this.#e))return this.#s=this.#s.slice(this.#t),{type:t,value:this.#e};if(this.#s.startsWith(this.#i))return!1===e&&this.#s.endsWith(this.#i)?null:this.extractQuotedString();const s=this.#n.exec(this.#s);return s?!1===e&&s[0].length===this.#s.length?null:(this.#s=this.#s.slice(s[0].length),{type:r,value:s[0]}):null}extractQuotedString(){let e=this.#r,t="";for(;e<this.#s.length;)if(this.#s.slice(e,e+this.#r)!==this.quotation||this.#s.slice(e+this.#r,e+2*this.#r)!==this.quotation){if(this.#s.slice(e,e+this.#r)===this.quotation)return this.#s=this.#s.slice(e+this.#r),{type:r,value:t};t+=this.#s[e],e++}else t+=this.quotation,e+=2*this.#r;return null}}class a extends TransformStream{#u=0;#h=[];#l;#f=!1;constructor(e={}){super({transform:(e,n)=>{switch(e.type){case r:this.#f=!0,this.#h[this.#u]=e.value;break;case t:this.#u++;break;case i:if(void 0===this.#l)this.#d(this.#h);else if(this.#f){const e=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#h.at(t)])));n.enqueue(e)}this.#u=0,this.#h=new Array(this.#l?.length),this.#f=!1}},flush:e=>{if(0!==this.#u&&void 0!==this.#l&&this.#f){const t=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#h.at(t)])));e.enqueue(t)}}}),void 0!==e.header&&Array.isArray(e.header)&&this.#d(e.header)}#d(e){if(this.#l=e,0===this.#l.length)throw new Error("The header must not be empty.");if(new Set(this.#l).size!==this.#l.length)throw new Error("The header must not contain duplicate fields.")}}class u extends ReadableStream{constructor(e){super({start(t){t.enqueue(e),t.close()}})}}async function h(...e){const t=[];for await(const i of this(...e))t.push(i);return t}async function*l(e,t){let i;const r=new ReadableStream({start:e=>i=e});await e.pipeThrough(new o(t)).pipeThrough(new a(t)).pipeTo(new WritableStream({write:e=>i.enqueue(e),close:()=>i.close()}));const n=r.getReader();try{for(;;){const{value:e,done:t}=await n.read();if(t)break;yield e}}finally{n.releaseLock()}}async function*f(e,t){yield*l(new u(e),t)}async function*d(e,t){const{charset:i,fatal:r,ignoreBOM:n,decomposition:s}=t??{};yield*l([...s?[new DecompressionStream(s)]:[],new TextDecoderStream(i,{fatal:r,ignoreBOM:n})].reduce(((e,t)=>e.pipeThrough(t)),e),t)}function c(e,t){const{headers:i}=e,r=i.get("content-type")??"text/csv",n=function(e){const[t,...i]=e.split(";"),r={type:t.trim(),parameters:{}};for(const e of i){const[t,i]=e.split("=");r.parameters[t.trim()]=i.trim()}return r}(r);if("text/csv"!==n.type)throw new Error(`Invalid mime type: ${r}`);const s=i.get("content-encoding")??void 0,o=n.parameters.charset??"utf-8";if(null===e.body)throw new Error("Response body is null");return d(e.body,{decomposition:s,charset:o,...t})}async function*m(e,t){const[i,r]=e.tee(),n=i.getReader(),{value:s}=await n.read();n.releaseLock(),"string"==typeof s?yield*l(r,t):s instanceof Uint8Array&&(yield*d(r,t))}async function*y(e,t){"string"==typeof e?yield*f(e,t):e instanceof ReadableStream?yield*m(e,t):e instanceof Response&&(yield*c(e,t))}!function(e){e.toArray=h}(l||(l={})),function(e){e.toArray=h}(f||(f={})),function(e){e.toArray=h}(d||(d={})),function(e){e.toArray=h}(c||(c={})),function(e){e.toArray=h}(m||(m={})),function(e){e.toArray=h}(y||(y={})),e.Field=r,e.FieldDelimiter=t,e.LexerTransformer=o,e.RecordAssemblerTransformar=a,e.RecordDelimiter=i,e.parse=y,e.parseBinaryStream=d,e.parseResponse=c,e.parseStream=m,e.parseString=f,e.parseStringStream=l}));
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).CSV={})}(this,(function(e){"use strict";const t=Symbol.for("web-streams-csv.FieldDelimiter"),i=Symbol.for("web-streams-csv.RecordDelimiter"),r=Symbol.for("web-streams-csv.Field"),n="\r\n",s="\n";function o(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}class a extends TransformStream{#e;#t;#i;#r;#n;#s="";get demiliter(){return this.#e}get quotation(){return this.#i}constructor({demiliter:e=",",quotation:t='"'}={}){!function(e){if("string"==typeof e.quotation&&0===e.quotation.length)throw new Error("quotation must not be empty");if("string"==typeof e.demiliter&&0===e.demiliter.length)throw new Error("demiliter must not be empty");if(e.quotation.includes(s)||e.quotation.includes("\r"))throw new Error("quotation must not include CR or LF");if(e.demiliter.includes(s)||e.demiliter.includes("\r"))throw new Error("demiliter must not include CR or LF");if(e.demiliter.includes(e.quotation)||e.quotation.includes(e.demiliter))throw new Error("demiliter and quotation must not include each other as a substring")}({demiliter:e,quotation:t}),super({transform:(e,t)=>{if(0!==e.length){this.#s+=e;for(const e of this.#o({flush:!1}))t.enqueue(e)}},flush:e=>{for(const t of this.#o({flush:!0}))e.enqueue(t)}}),this.#e=e,this.#t=e.length,this.#i=t,this.#r=t.length;const i=o(e),r=o(t);this.#n=new RegExp(`^(?:(?!${r})(?!${i})(?![\\r\\n]))([\\S\\s\\uFEFF\\xA0]+?)(?=${r}|${i}|\\r|\\n|$)`)}*#o({flush:e}){let n=null;for(let s;s=this.#a({flush:e});)switch(s.type){case r:n?n.value+=s.value:n=s;break;case t:case i:n&&(yield n,n=null),yield s}n&&(yield n)}#a({flush:e=!1}={}){if(0===this.#s.length)return null;if(this.#s.startsWith(n))return this.#s=this.#s.slice(2),{type:i,value:n};if(this.#s.startsWith(s))return this.#s=this.#s.slice(1),{type:i,value:s};if(this.#s.startsWith(this.#e))return this.#s=this.#s.slice(this.#t),{type:t,value:this.#e};if(this.#s.startsWith(this.#i))return!1===e&&this.#s.endsWith(this.#i)?null:this.extractQuotedString(e);const o=this.#n.exec(this.#s);return o?!1===e&&o[0].length===this.#s.length?null:(this.#s=this.#s.slice(o[0].length),{type:r,value:o[0]}):null}extractQuotedString(e){let t=this.#r,i="";for(;t<this.#s.length;)if(this.#s.slice(t,t+this.#r)!==this.quotation||this.#s.slice(t+this.#r,t+2*this.#r)!==this.quotation){if(this.#s.slice(t,t+this.#r)===this.quotation)return!1===e&&t+this.#r<this.#s.length&&this.#s.slice(t+this.#r,this.#t)!==this.demiliter&&this.#s.slice(t+this.#r,t+this.#r+2)!==n&&this.#s.slice(t+this.#r,t+this.#r+1)!==s?null:(this.#s=this.#s.slice(t+this.#r),{type:r,value:i});i+=this.#s[t],t++}else i+=this.quotation,t+=2*this.#r;return null}}class h extends TransformStream{#h=0;#u=[];#l;#f=!1;constructor(e={}){super({transform:(e,n)=>{switch(e.type){case r:this.#f=!0,this.#u[this.#h]=e.value;break;case t:this.#h++;break;case i:if(void 0===this.#l)this.#c(this.#u);else if(this.#f){const e=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#u.at(t)])));n.enqueue(e)}this.#h=0,this.#u=new Array(this.#l?.length),this.#f=!1}},flush:e=>{if(0!==this.#h&&void 0!==this.#l&&this.#f){const t=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#u.at(t)])));e.enqueue(t)}}}),void 0!==e.header&&Array.isArray(e.header)&&this.#c(e.header)}#c(e){if(this.#l=e,0===this.#l.length)throw new Error("The header must not be empty.");if(new Set(this.#l).size!==this.#l.length)throw new Error("The header must not contain duplicate fields.")}}class u extends ReadableStream{constructor(e){super({start(t){t.enqueue(e),t.close()}})}}async function l(...e){const t=[];for await(const i of this(...e))t.push(i);return t}async function*f(e,t){let i;const r=new ReadableStream({start:e=>i=e});await e.pipeThrough(new a(t)).pipeThrough(new h(t)).pipeTo(new WritableStream({write:e=>i.enqueue(e),close:()=>i.close()}));const n=r.getReader();try{for(;;){const{value:e,done:t}=await n.read();if(t)break;yield e}}finally{n.releaseLock()}}async function*c(e,t){yield*f(new u(e),t)}async function*d(e,t){const{charset:i,fatal:r,ignoreBOM:n,decomposition:s}=t??{};yield*f([...s?[new DecompressionStream(s)]:[],new TextDecoderStream(i,{fatal:r,ignoreBOM:n})].reduce(((e,t)=>e.pipeThrough(t)),e),t)}function m(e,t){const{headers:i}=e,r=i.get("content-type")??"text/csv",n=function(e){const[t,...i]=e.split(";"),r={type:t.trim(),parameters:{}};for(const e of i){const[t,i]=e.split("=");r.parameters[t.trim()]=i.trim()}return r}(r);if("text/csv"!==n.type)throw new Error(`Invalid mime type: ${r}`);const s=i.get("content-encoding")??void 0,o=n.parameters.charset??"utf-8";if(null===e.body)throw new Error("Response body is null");return d(e.body,{decomposition:s,charset:o,...t})}async function*y(e,t){const[i,r]=e.tee(),n=i.getReader(),{value:s}=await n.read();n.releaseLock(),"string"==typeof s?yield*f(r,t):s instanceof Uint8Array&&(yield*d(r,t))}async function*p(e,t){"string"==typeof e?yield*c(e,t):e instanceof ReadableStream?yield*y(e,t):e instanceof Response&&(yield*m(e,t))}!function(e){e.toArray=l}(f||(f={})),function(e){e.toArray=l}(c||(c={})),function(e){e.toArray=l}(d||(d={})),function(e){e.toArray=l}(m||(m={})),function(e){e.toArray=l}(y||(y={})),function(e){e.toArray=l}(p||(p={})),e.Field=r,e.FieldDelimiter=t,e.LexerTransformer=a,e.RecordAssemblerTransformar=h,e.RecordDelimiter=i,e.parse=p,e.parseBinaryStream=d,e.parseResponse=m,e.parseStream=y,e.parseString=c,e.parseStringStream=f}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-csv-toolbox",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A CSV Toolbox utilizing Web Standard APIs.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -24,6 +24,7 @@
24
24
  "scripts": {
25
25
  "doc": "typedoc",
26
26
  "test": "vitest",
27
+ "test:browser": "vitest --browser",
27
28
  "format": "biome format . --write",
28
29
  "lint": "biome lint .",
29
30
  "check": "biome check src --apply",
@@ -53,9 +54,9 @@
53
54
  "@changesets/cli": "^2.27.1",
54
55
  "@fast-check/vitest": "^0.0.9",
55
56
  "@rollup/plugin-terser": "^0.4.4",
57
+ "@vitest/browser": "^1.1.0",
56
58
  "changesets-github-release": "^0.1.0",
57
59
  "husky": "^8.0.0",
58
- "jsdom": "^23.0.1",
59
60
  "lint-staged": "^15.2.0",
60
61
  "rollup": "^4.9.1",
61
62
  "rollup-plugin-delete": "^2.0.0",
@@ -64,6 +65,7 @@
64
65
  "typedoc": "^0.25.4",
65
66
  "typedoc-plugin-mdn-links": "^3.1.9",
66
67
  "typescript": "^5.3.2",
67
- "vitest": "^1.1.0"
68
+ "vitest": "^1.1.0",
69
+ "webdriverio": "^8.27.0"
68
70
  }
69
71
  }