react-csv-autopilot 0.0.1-beta.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 ADDED
@@ -0,0 +1,275 @@
1
+ # react-csv-autopilot
2
+
3
+ [![npm version](https://img.shields.io/npm/v/react-csv-autopilot?color=blue&logo=npm)](https://www.npmjs.com/package/react-csv-autopilot)
4
+ [![npm downloads](https://img.shields.io/npm/dw/react-csv-autopilot?logo=npm)](https://www.npmjs.com/package/react-csv-autopilot)
5
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/react-csv-autopilot?color=green&logo=webpack)](https://bundlephobia.com/package/react-csv-autopilot)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?logo=typescript)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ **Drop the function, we handle the rest.**
10
+
11
+ A React library for exporting large datasets to CSV with automatic pagination, streaming, and progress tracking. Built with Web Workers for non-blocking performance and File System Access API for efficient file writing.
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - **Automatic Pagination** - Just provide a `getNextPage` function, we handle the rest
18
+ - **Streaming Export** - Uses ReadableStream for memory-efficient large file exports
19
+ - **Non-blocking** - Web Workers for CSV conversion without freezing the UI
20
+ - **Progress Tracking** - Real-time progress updates via BroadcastChannel
21
+ - **TypeScript** - Full type safety with TypeScript definitions
22
+ - **Zero Dependencies** - Only React as peer dependency
23
+ - **Framework Agnostic Core** - Use the core logic in any JavaScript environment
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install react-csv-autopilot
31
+ ```
32
+
33
+ ```bash
34
+ yarn add react-csv-autopilot
35
+ ```
36
+
37
+ ```bash
38
+ pnpm add react-csv-autopilot
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Quick Start
44
+
45
+ ```typescript
46
+ import { useExportCSV, useMessageExportCSV } from 'react-csv-autopilot';
47
+
48
+ function ExportButton() {
49
+ const { handler } = useExportCSV();
50
+
51
+ // Track export progress
52
+ useMessageExportCSV((progress) => {
53
+ if (progress.type === 'progress') {
54
+ console.log(`${progress.loadedItemsCount}/${progress.total}`);
55
+ }
56
+ });
57
+
58
+ const handleExport = async () => {
59
+ await handler.execute({
60
+ fileName: 'users-export',
61
+ columns: [
62
+ { key: 'id', label: 'ID' },
63
+ { key: 'name', label: 'Full Name' },
64
+ { key: 'email', label: 'Email Address' },
65
+ ],
66
+ getNextPage: async (offset) => {
67
+ // Fetch your data - this will be called automatically
68
+ const response = await fetch(`/api/users?page=${offset}&limit=100`);
69
+ const data = await response.json();
70
+
71
+ return {
72
+ rows: data.users,
73
+ total: data.totalCount,
74
+ };
75
+ },
76
+ });
77
+ };
78
+
79
+ return <button onClick={handleExport}>Export to CSV</button>;
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 📖 API Reference
86
+
87
+ ### `useExportCSV()`
88
+
89
+ Hook that provides access to the CSV export controller.
90
+
91
+ **Returns:**
92
+ ```typescript
93
+ {
94
+ handler: ExportController // Controller instance for executing exports
95
+ }
96
+ ```
97
+
98
+ **Example:**
99
+ ```typescript
100
+ const { handler } = useExportCSV();
101
+
102
+ await handler.execute({
103
+ fileName: 'data-export',
104
+ columns: [...],
105
+ getNextPage: async (offset) => {...}
106
+ });
107
+ ```
108
+
109
+ ---
110
+
111
+ ### `useMessageExportCSV(callback, channelName?)`
112
+
113
+ Hook for tracking export progress in real-time.
114
+
115
+ **Parameters:**
116
+ - `callback: (payload: ExportProgressPayload) => void` - Called on each progress update
117
+ - `channelName?: string` - Optional custom channel name (default: `'EXPORT_CSV_CHANNEL'`)
118
+
119
+ **Payload Type:**
120
+ ```typescript
121
+ type ExportProgressPayload = {
122
+ total: number;
123
+ loadedItemsCount: number;
124
+ type: 'progress' | 'done' | 'failed';
125
+ };
126
+ ```
127
+
128
+ **Example:**
129
+ ```typescript
130
+ const [progress, setProgress] = useState({ loaded: 0, total: 0 });
131
+
132
+ useMessageExportCSV((payload) => {
133
+ setProgress({
134
+ loaded: payload.loadedItemsCount,
135
+ total: payload.total,
136
+ });
137
+
138
+ if (payload.type === 'done') {
139
+ console.log('Export completed!');
140
+ }
141
+ });
142
+ ```
143
+
144
+ ---
145
+
146
+ ### Types
147
+
148
+ #### `ExportParams`
149
+
150
+ ```typescript
151
+ type ExportParams = {
152
+ fileName: string;
153
+ columns: Column[];
154
+ getNextPage: (offset: number) => Promise<{ rows: any[]; total: number }>;
155
+ };
156
+ ```
157
+
158
+ #### `Column`
159
+
160
+ ```typescript
161
+ type Column = {
162
+ key: string;
163
+ label: string;
164
+ timezone?: 'UTC' | string;
165
+ formatType?: 'dateFull' | 'dateMediumTime' | 'timeShort' | 'numDecimal' | 'numCompact' | 'numCurrency' | 'numPercent';
166
+ };
167
+ ```
168
+
169
+
170
+ ### Pagination Strategies
171
+
172
+ **API with page numbers:**
173
+ ```typescript
174
+ getNextPage: async (offset) => {
175
+ const page = offset + 1; // Convert to 1-based pagination
176
+ const response = await fetch(`/api/data?page=${page}&size=100`);
177
+ return await response.json();
178
+ }
179
+ ```
180
+
181
+ **API with cursor-based pagination:**
182
+ ```typescript
183
+ let nextCursor = null;
184
+
185
+ getNextPage: async (offset) => {
186
+ const url = offset === 0
187
+ ? '/api/data?limit=100'
188
+ : `/api/data?cursor=${nextCursor}&limit=100`;
189
+
190
+ const response = await fetch(url);
191
+ const data = await response.json();
192
+
193
+ nextCursor = data.nextCursor;
194
+
195
+ return {
196
+ rows: data.items,
197
+ total: data.totalCount,
198
+ };
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Architecture
205
+
206
+ ### How It Works
207
+
208
+ 1. **Stream-based Export** - Uses `ReadableStream` and File System Access API for efficient file writing
209
+ 2. **Web Workers** - CSV conversion happens in a separate thread to keep UI responsive
210
+ 3. **Automatic Pagination** - Calls your `getNextPage` function repeatedly until all data is fetched
211
+ 4. **Progress Tracking** - Uses `BroadcastChannel` to communicate progress across components
212
+
213
+ ```
214
+ ┌─────────────────┐
215
+ │ React Hook │
216
+ │ useExportCSV() │
217
+ └────────┬────────┘
218
+
219
+
220
+ ┌─────────────────┐ ┌──────────────┐
221
+ │ Export │─────▶│ Web Worker │
222
+ │ Controller │ │ (CSV Convert)│
223
+ └────────┬────────┘ └──────────────┘
224
+
225
+
226
+ ┌─────────────────┐ ┌──────────────┐
227
+ │ ReadableStream │─────▶│ File System │
228
+ │ (Pagination) │ │ Access API │
229
+ └─────────────────┘ └──────────────┘
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Browser Compatibility
235
+
236
+ Requires browsers with support for:
237
+ - **File System Access API** (Chrome 86+, Edge 86+)
238
+ - **Web Workers** (All modern browsers)
239
+ - **ReadableStream** (All modern browsers)
240
+
241
+ > **Note:** For browsers without File System Access API, the library will fall back to Blob-based download.
242
+
243
+ ---
244
+
245
+ ## Contributing
246
+
247
+ Contributions are welcome! Please feel free to submit a Pull Request.
248
+
249
+ ---
250
+
251
+ ## 📄 License
252
+
253
+ MIT © [Pavlo Kuzina](../../LICENSE)
254
+
255
+ ---
256
+
257
+ ## 🔗 Links
258
+
259
+ - **npm Package**: [react-csv-autopilot](https://www.npmjs.com/package/react-csv-autopilot)
260
+ - **Repository**: [GitHub - utils-kit](https://github.com/PashaSchool/utils-kit)
261
+ - **Issues**: [GitHub Issues](https://github.com/PashaSchool/utils-kit/issues)
262
+ - **Monorepo**: Part of [utils-kit](../../README.md) collection
263
+
264
+ ---
265
+
266
+ ## Related Packages
267
+
268
+ - [**react-url-query-params**](../react-url-query-params) - Type-safe URL query parameter management
269
+
270
+ ---
271
+
272
+ <div align="center">
273
+ <sub>Built with ❤️ by <a href="https://github.com/PashaSchool">Pavlo Kuzina</a></sub>
274
+ </div>
275
+
package/dist/index.cjs ADDED
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __typeError = (msg) => {
7
+ throw TypeError(msg);
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
23
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
24
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
25
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
26
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
27
+
28
+ // src/index.ts
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ useExportCSV: () => useExportCSV_default,
32
+ useMessageExportCSV: () => useMessageExportCSV_default
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/hooks/useExportCSV.ts
37
+ var import_react = require("react");
38
+
39
+ // src/core/controllers/ExportController.ts
40
+ var _ExportController_instances, canUseFSAccess_fn, resolveStrategy_fn;
41
+ var ExportController = class {
42
+ constructor(deps) {
43
+ this.deps = deps;
44
+ __privateAdd(this, _ExportController_instances);
45
+ }
46
+ async start(params) {
47
+ const strategy = __privateMethod(this, _ExportController_instances, resolveStrategy_fn).call(this);
48
+ return strategy.export(params);
49
+ }
50
+ };
51
+ _ExportController_instances = new WeakSet();
52
+ canUseFSAccess_fn = function() {
53
+ return typeof window.showSaveFilePicker === "function";
54
+ };
55
+ resolveStrategy_fn = function() {
56
+ if (__privateMethod(this, _ExportController_instances, canUseFSAccess_fn).call(this)) {
57
+ return this.deps.fsAccessStrategy;
58
+ }
59
+ return this.deps.blobExportStrategy;
60
+ };
61
+
62
+ // src/core/strategy/BolbExportStrategy.ts
63
+ var BolbExportStrategy = class {
64
+ export(params) {
65
+ return Promise.resolve({});
66
+ }
67
+ };
68
+ var BolbExportStrategy_default = BolbExportStrategy;
69
+
70
+ // src/core/contants/index.ts
71
+ var WEB_WORKER_NAME = "scv-worker";
72
+ var BROADCAST_CHANNEL_NAME = "react-csv-exporter";
73
+
74
+ // src/core/WorkerManager.ts
75
+ var import_meta = {};
76
+ var pending = /* @__PURE__ */ new Map();
77
+ var _worker, _WorkerManager_instances, listenerRegistry_fn;
78
+ var _WorkerManager = class _WorkerManager {
79
+ constructor() {
80
+ __privateAdd(this, _WorkerManager_instances);
81
+ __privateAdd(this, _worker);
82
+ let workerUrl;
83
+ try {
84
+ workerUrl = new URL("./worker.js", import_meta.url);
85
+ } catch {
86
+ workerUrl = "/worker.js";
87
+ }
88
+ __privateSet(this, _worker, new Worker(workerUrl, {
89
+ type: "module",
90
+ name: WEB_WORKER_NAME
91
+ }));
92
+ __privateMethod(this, _WorkerManager_instances, listenerRegistry_fn).call(this);
93
+ }
94
+ static initialise() {
95
+ return new _WorkerManager();
96
+ }
97
+ async triggerWorker(payload) {
98
+ const id = payload.id ?? Math.random().toString(36).substr(2);
99
+ const p = new Promise((resolve, reject) => {
100
+ pending.set(id, { resolve, reject });
101
+ });
102
+ __privateGet(this, _worker).postMessage(payload);
103
+ return p;
104
+ }
105
+ terminate() {
106
+ if (__privateGet(this, _worker)) {
107
+ __privateGet(this, _worker).terminate();
108
+ __privateSet(this, _worker, null);
109
+ }
110
+ }
111
+ };
112
+ _worker = new WeakMap();
113
+ _WorkerManager_instances = new WeakSet();
114
+ listenerRegistry_fn = function() {
115
+ __privateGet(this, _worker).addEventListener("message", (event) => {
116
+ const { id, result, error } = event.data;
117
+ const entity = pending.get(id);
118
+ if (!entity) {
119
+ return;
120
+ }
121
+ pending.delete(id);
122
+ if (error) {
123
+ entity.reject(error);
124
+ } else {
125
+ entity.resolve(result);
126
+ }
127
+ });
128
+ __privateGet(this, _worker).addEventListener("error", (event) => {
129
+ for (const [, { reject }] of pending) {
130
+ reject(event);
131
+ }
132
+ pending.clear();
133
+ });
134
+ };
135
+ var WorkerManager = _WorkerManager;
136
+ var WorkerManager_default = WorkerManager;
137
+
138
+ // src/core/strategy/FsAccessExportStrategy.ts
139
+ var FsAccessExportStrategy = class {
140
+ constructor() {
141
+ this.workerManager = WorkerManager_default.initialise();
142
+ }
143
+ async export(params) {
144
+ const _suggestedName = params?.fileName || "export";
145
+ const fileHandle = await window.showSaveFilePicker({
146
+ suggestedName: _suggestedName,
147
+ types: [{ description: "CSV file", accept: { "text/csv": [".csv"] } }]
148
+ });
149
+ const writableFileStream = await fileHandle.createWritable();
150
+ let iterator = 0;
151
+ let totalRowsLoaded = 0;
152
+ const encoder = new TextEncoder();
153
+ const messaging = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
154
+ const readable = new ReadableStream({
155
+ pull: async (controller) => {
156
+ try {
157
+ const response = await params.getNextPage(iterator++);
158
+ const safeRows = Array.isArray(response.rows) ? response?.rows : [];
159
+ const safeTotal = response.total ?? 0;
160
+ const isRowsEmpty = !safeRows || !safeRows.length;
161
+ totalRowsLoaded = isRowsEmpty ? safeTotal : totalRowsLoaded += safeRows.length;
162
+ const isFinished = totalRowsLoaded >= safeTotal;
163
+ if (isRowsEmpty) {
164
+ messaging.postMessage(
165
+ JSON.stringify({
166
+ type: "done",
167
+ total: safeTotal,
168
+ loadedItemsCount: totalRowsLoaded
169
+ })
170
+ );
171
+ await this.workerManager.triggerWorker({ id: iterator, type: "completed" });
172
+ messaging.close();
173
+ controller.close();
174
+ return;
175
+ }
176
+ const csvChunks = await this.workerManager.triggerWorker({
177
+ id: iterator,
178
+ type: "to_csv_chunk",
179
+ data: safeRows,
180
+ columns: params.columns
181
+ });
182
+ messaging.postMessage(
183
+ JSON.stringify({
184
+ type: "progress",
185
+ total: safeTotal,
186
+ loadedItemsCount: totalRowsLoaded
187
+ })
188
+ );
189
+ controller.enqueue(encoder.encode(csvChunks));
190
+ if (isFinished) {
191
+ messaging.postMessage(
192
+ JSON.stringify({
193
+ type: "done",
194
+ total: safeTotal,
195
+ loadedItemsCount: totalRowsLoaded
196
+ })
197
+ );
198
+ await this.workerManager.triggerWorker({ id: iterator, type: "completed" });
199
+ messaging.close();
200
+ controller.close();
201
+ return;
202
+ }
203
+ } catch (error) {
204
+ controller.error(error);
205
+ messaging.postMessage(
206
+ JSON.stringify({
207
+ type: "failed",
208
+ total: 0,
209
+ loadedItemsCount: totalRowsLoaded
210
+ })
211
+ );
212
+ }
213
+ }
214
+ });
215
+ try {
216
+ await readable.pipeTo(writableFileStream);
217
+ } catch (err) {
218
+ console.error("Export failed:", err);
219
+ throw err;
220
+ } finally {
221
+ this.workerManager.terminate();
222
+ }
223
+ return {
224
+ finished: true,
225
+ totalRowsLoaded,
226
+ logs: {
227
+ warnings: []
228
+ }
229
+ };
230
+ }
231
+ };
232
+ var FsAccessExportStrategy_default = FsAccessExportStrategy;
233
+
234
+ // src/core/fabric/ExportControlFabric.ts
235
+ var ExportControlFabric = class {
236
+ static create() {
237
+ const controller = new ExportController({
238
+ fsAccessStrategy: new FsAccessExportStrategy_default(),
239
+ blobExportStrategy: new BolbExportStrategy_default()
240
+ });
241
+ return controller;
242
+ }
243
+ };
244
+
245
+ // src/core/ExportControllerSingleton.ts
246
+ var _ExportControllerSingleton = class _ExportControllerSingleton {
247
+ static init() {
248
+ if (_ExportControllerSingleton.instance) {
249
+ return _ExportControllerSingleton.instance;
250
+ }
251
+ _ExportControllerSingleton.instance = ExportControlFabric.create();
252
+ _ExportControllerSingleton.initialized = true;
253
+ return _ExportControllerSingleton.instance;
254
+ }
255
+ static getInstance() {
256
+ if (!_ExportControllerSingleton.instance) {
257
+ return _ExportControllerSingleton.init();
258
+ }
259
+ return _ExportControllerSingleton.instance;
260
+ }
261
+ };
262
+ _ExportControllerSingleton.instance = null;
263
+ _ExportControllerSingleton.initialized = false;
264
+ var ExportControllerSingleton = _ExportControllerSingleton;
265
+ var ExportControllerSingleton_default = ExportControllerSingleton;
266
+
267
+ // src/hooks/useExportCSV.ts
268
+ function useExportCSV() {
269
+ const exportCallbackRef = (0, import_react.useRef)(ExportControllerSingleton_default.init());
270
+ return {
271
+ handler: exportCallbackRef.current
272
+ };
273
+ }
274
+ var useExportCSV_default = useExportCSV;
275
+
276
+ // src/hooks/useMessageExportCSV.ts
277
+ var import_react2 = require("react");
278
+ function useMessageExportCSV(cb) {
279
+ (0, import_react2.useEffect)(() => {
280
+ const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
281
+ channel.addEventListener("message", (params) => {
282
+ try {
283
+ const json = JSON.parse(params.data);
284
+ cb(json);
285
+ } catch (error) {
286
+ console.error({ error });
287
+ }
288
+ });
289
+ return () => {
290
+ channel.close();
291
+ };
292
+ }, []);
293
+ }
294
+ var useMessageExportCSV_default = useMessageExportCSV;
@@ -0,0 +1,52 @@
1
+ type formatterTypes = "dateFull" | "dateMediumTime" | "timeShort" | "numDecimal" | "numCompact" | "numCurrency" | "numPercent";
2
+ type Column = {
3
+ key: string;
4
+ label: string;
5
+ timezone?: "UTC" | string;
6
+ formatType?: formatterTypes;
7
+ };
8
+ type ExportParams = {
9
+ fileName: string;
10
+ columns: Column[];
11
+ getNextPage: (offset: number) => Promise<{
12
+ rows: any[];
13
+ total: number;
14
+ }>;
15
+ };
16
+ interface ExportStrategy {
17
+ export(params: ExportParams): Promise<any>;
18
+ }
19
+
20
+ declare class BolbExportStrategy implements ExportStrategy {
21
+ export(params: ExportParams): Promise<any>;
22
+ }
23
+
24
+ declare class FsAccessExportStrategy implements ExportStrategy {
25
+ private workerManager;
26
+ constructor();
27
+ export(params: ExportParams): Promise<any>;
28
+ }
29
+
30
+ type ExportControllerDeps = {
31
+ fsAccessStrategy: FsAccessExportStrategy;
32
+ blobExportStrategy: BolbExportStrategy;
33
+ };
34
+ declare class ExportController {
35
+ #private;
36
+ private readonly deps;
37
+ constructor(deps: ExportControllerDeps);
38
+ start(params: ExportParams): Promise<any>;
39
+ }
40
+
41
+ declare function useExportCSV(): {
42
+ handler: ExportController;
43
+ };
44
+
45
+ type Payload = {
46
+ total: number;
47
+ loadedItemsCount: number;
48
+ state: "progress" | "failed" | "done";
49
+ };
50
+ declare function useMessageExportCSV(cb: (payload: Payload) => void): void;
51
+
52
+ export { type Column, ExportController, type ExportParams, useExportCSV, useMessageExportCSV };
@@ -0,0 +1,52 @@
1
+ type formatterTypes = "dateFull" | "dateMediumTime" | "timeShort" | "numDecimal" | "numCompact" | "numCurrency" | "numPercent";
2
+ type Column = {
3
+ key: string;
4
+ label: string;
5
+ timezone?: "UTC" | string;
6
+ formatType?: formatterTypes;
7
+ };
8
+ type ExportParams = {
9
+ fileName: string;
10
+ columns: Column[];
11
+ getNextPage: (offset: number) => Promise<{
12
+ rows: any[];
13
+ total: number;
14
+ }>;
15
+ };
16
+ interface ExportStrategy {
17
+ export(params: ExportParams): Promise<any>;
18
+ }
19
+
20
+ declare class BolbExportStrategy implements ExportStrategy {
21
+ export(params: ExportParams): Promise<any>;
22
+ }
23
+
24
+ declare class FsAccessExportStrategy implements ExportStrategy {
25
+ private workerManager;
26
+ constructor();
27
+ export(params: ExportParams): Promise<any>;
28
+ }
29
+
30
+ type ExportControllerDeps = {
31
+ fsAccessStrategy: FsAccessExportStrategy;
32
+ blobExportStrategy: BolbExportStrategy;
33
+ };
34
+ declare class ExportController {
35
+ #private;
36
+ private readonly deps;
37
+ constructor(deps: ExportControllerDeps);
38
+ start(params: ExportParams): Promise<any>;
39
+ }
40
+
41
+ declare function useExportCSV(): {
42
+ handler: ExportController;
43
+ };
44
+
45
+ type Payload = {
46
+ total: number;
47
+ loadedItemsCount: number;
48
+ state: "progress" | "failed" | "done";
49
+ };
50
+ declare function useMessageExportCSV(cb: (payload: Payload) => void): void;
51
+
52
+ export { type Column, ExportController, type ExportParams, useExportCSV, useMessageExportCSV };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
+
10
+ // src/hooks/useExportCSV.ts
11
+ import { useRef } from "react";
12
+
13
+ // src/core/controllers/ExportController.ts
14
+ var _ExportController_instances, canUseFSAccess_fn, resolveStrategy_fn;
15
+ var ExportController = class {
16
+ constructor(deps) {
17
+ this.deps = deps;
18
+ __privateAdd(this, _ExportController_instances);
19
+ }
20
+ async start(params) {
21
+ const strategy = __privateMethod(this, _ExportController_instances, resolveStrategy_fn).call(this);
22
+ return strategy.export(params);
23
+ }
24
+ };
25
+ _ExportController_instances = new WeakSet();
26
+ canUseFSAccess_fn = function() {
27
+ return typeof window.showSaveFilePicker === "function";
28
+ };
29
+ resolveStrategy_fn = function() {
30
+ if (__privateMethod(this, _ExportController_instances, canUseFSAccess_fn).call(this)) {
31
+ return this.deps.fsAccessStrategy;
32
+ }
33
+ return this.deps.blobExportStrategy;
34
+ };
35
+
36
+ // src/core/strategy/BolbExportStrategy.ts
37
+ var BolbExportStrategy = class {
38
+ export(params) {
39
+ return Promise.resolve({});
40
+ }
41
+ };
42
+ var BolbExportStrategy_default = BolbExportStrategy;
43
+
44
+ // src/core/contants/index.ts
45
+ var WEB_WORKER_NAME = "scv-worker";
46
+ var BROADCAST_CHANNEL_NAME = "react-csv-exporter";
47
+
48
+ // src/core/WorkerManager.ts
49
+ var pending = /* @__PURE__ */ new Map();
50
+ var _worker, _WorkerManager_instances, listenerRegistry_fn;
51
+ var _WorkerManager = class _WorkerManager {
52
+ constructor() {
53
+ __privateAdd(this, _WorkerManager_instances);
54
+ __privateAdd(this, _worker);
55
+ let workerUrl;
56
+ try {
57
+ workerUrl = new URL("./worker.js", import.meta.url);
58
+ } catch {
59
+ workerUrl = "/worker.js";
60
+ }
61
+ __privateSet(this, _worker, new Worker(workerUrl, {
62
+ type: "module",
63
+ name: WEB_WORKER_NAME
64
+ }));
65
+ __privateMethod(this, _WorkerManager_instances, listenerRegistry_fn).call(this);
66
+ }
67
+ static initialise() {
68
+ return new _WorkerManager();
69
+ }
70
+ async triggerWorker(payload) {
71
+ const id = payload.id ?? Math.random().toString(36).substr(2);
72
+ const p = new Promise((resolve, reject) => {
73
+ pending.set(id, { resolve, reject });
74
+ });
75
+ __privateGet(this, _worker).postMessage(payload);
76
+ return p;
77
+ }
78
+ terminate() {
79
+ if (__privateGet(this, _worker)) {
80
+ __privateGet(this, _worker).terminate();
81
+ __privateSet(this, _worker, null);
82
+ }
83
+ }
84
+ };
85
+ _worker = new WeakMap();
86
+ _WorkerManager_instances = new WeakSet();
87
+ listenerRegistry_fn = function() {
88
+ __privateGet(this, _worker).addEventListener("message", (event) => {
89
+ const { id, result, error } = event.data;
90
+ const entity = pending.get(id);
91
+ if (!entity) {
92
+ return;
93
+ }
94
+ pending.delete(id);
95
+ if (error) {
96
+ entity.reject(error);
97
+ } else {
98
+ entity.resolve(result);
99
+ }
100
+ });
101
+ __privateGet(this, _worker).addEventListener("error", (event) => {
102
+ for (const [, { reject }] of pending) {
103
+ reject(event);
104
+ }
105
+ pending.clear();
106
+ });
107
+ };
108
+ var WorkerManager = _WorkerManager;
109
+ var WorkerManager_default = WorkerManager;
110
+
111
+ // src/core/strategy/FsAccessExportStrategy.ts
112
+ var FsAccessExportStrategy = class {
113
+ constructor() {
114
+ this.workerManager = WorkerManager_default.initialise();
115
+ }
116
+ async export(params) {
117
+ const _suggestedName = params?.fileName || "export";
118
+ const fileHandle = await window.showSaveFilePicker({
119
+ suggestedName: _suggestedName,
120
+ types: [{ description: "CSV file", accept: { "text/csv": [".csv"] } }]
121
+ });
122
+ const writableFileStream = await fileHandle.createWritable();
123
+ let iterator = 0;
124
+ let totalRowsLoaded = 0;
125
+ const encoder = new TextEncoder();
126
+ const messaging = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
127
+ const readable = new ReadableStream({
128
+ pull: async (controller) => {
129
+ try {
130
+ const response = await params.getNextPage(iterator++);
131
+ const safeRows = Array.isArray(response.rows) ? response?.rows : [];
132
+ const safeTotal = response.total ?? 0;
133
+ const isRowsEmpty = !safeRows || !safeRows.length;
134
+ totalRowsLoaded = isRowsEmpty ? safeTotal : totalRowsLoaded += safeRows.length;
135
+ const isFinished = totalRowsLoaded >= safeTotal;
136
+ if (isRowsEmpty) {
137
+ messaging.postMessage(
138
+ JSON.stringify({
139
+ type: "done",
140
+ total: safeTotal,
141
+ loadedItemsCount: totalRowsLoaded
142
+ })
143
+ );
144
+ await this.workerManager.triggerWorker({ id: iterator, type: "completed" });
145
+ messaging.close();
146
+ controller.close();
147
+ return;
148
+ }
149
+ const csvChunks = await this.workerManager.triggerWorker({
150
+ id: iterator,
151
+ type: "to_csv_chunk",
152
+ data: safeRows,
153
+ columns: params.columns
154
+ });
155
+ messaging.postMessage(
156
+ JSON.stringify({
157
+ type: "progress",
158
+ total: safeTotal,
159
+ loadedItemsCount: totalRowsLoaded
160
+ })
161
+ );
162
+ controller.enqueue(encoder.encode(csvChunks));
163
+ if (isFinished) {
164
+ messaging.postMessage(
165
+ JSON.stringify({
166
+ type: "done",
167
+ total: safeTotal,
168
+ loadedItemsCount: totalRowsLoaded
169
+ })
170
+ );
171
+ await this.workerManager.triggerWorker({ id: iterator, type: "completed" });
172
+ messaging.close();
173
+ controller.close();
174
+ return;
175
+ }
176
+ } catch (error) {
177
+ controller.error(error);
178
+ messaging.postMessage(
179
+ JSON.stringify({
180
+ type: "failed",
181
+ total: 0,
182
+ loadedItemsCount: totalRowsLoaded
183
+ })
184
+ );
185
+ }
186
+ }
187
+ });
188
+ try {
189
+ await readable.pipeTo(writableFileStream);
190
+ } catch (err) {
191
+ console.error("Export failed:", err);
192
+ throw err;
193
+ } finally {
194
+ this.workerManager.terminate();
195
+ }
196
+ return {
197
+ finished: true,
198
+ totalRowsLoaded,
199
+ logs: {
200
+ warnings: []
201
+ }
202
+ };
203
+ }
204
+ };
205
+ var FsAccessExportStrategy_default = FsAccessExportStrategy;
206
+
207
+ // src/core/fabric/ExportControlFabric.ts
208
+ var ExportControlFabric = class {
209
+ static create() {
210
+ const controller = new ExportController({
211
+ fsAccessStrategy: new FsAccessExportStrategy_default(),
212
+ blobExportStrategy: new BolbExportStrategy_default()
213
+ });
214
+ return controller;
215
+ }
216
+ };
217
+
218
+ // src/core/ExportControllerSingleton.ts
219
+ var _ExportControllerSingleton = class _ExportControllerSingleton {
220
+ static init() {
221
+ if (_ExportControllerSingleton.instance) {
222
+ return _ExportControllerSingleton.instance;
223
+ }
224
+ _ExportControllerSingleton.instance = ExportControlFabric.create();
225
+ _ExportControllerSingleton.initialized = true;
226
+ return _ExportControllerSingleton.instance;
227
+ }
228
+ static getInstance() {
229
+ if (!_ExportControllerSingleton.instance) {
230
+ return _ExportControllerSingleton.init();
231
+ }
232
+ return _ExportControllerSingleton.instance;
233
+ }
234
+ };
235
+ _ExportControllerSingleton.instance = null;
236
+ _ExportControllerSingleton.initialized = false;
237
+ var ExportControllerSingleton = _ExportControllerSingleton;
238
+ var ExportControllerSingleton_default = ExportControllerSingleton;
239
+
240
+ // src/hooks/useExportCSV.ts
241
+ function useExportCSV() {
242
+ const exportCallbackRef = useRef(ExportControllerSingleton_default.init());
243
+ return {
244
+ handler: exportCallbackRef.current
245
+ };
246
+ }
247
+ var useExportCSV_default = useExportCSV;
248
+
249
+ // src/hooks/useMessageExportCSV.ts
250
+ import { useEffect } from "react";
251
+ function useMessageExportCSV(cb) {
252
+ useEffect(() => {
253
+ const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
254
+ channel.addEventListener("message", (params) => {
255
+ try {
256
+ const json = JSON.parse(params.data);
257
+ cb(json);
258
+ } catch (error) {
259
+ console.error({ error });
260
+ }
261
+ });
262
+ return () => {
263
+ channel.close();
264
+ };
265
+ }, []);
266
+ }
267
+ var useMessageExportCSV_default = useMessageExportCSV;
268
+ export {
269
+ useExportCSV_default as useExportCSV,
270
+ useMessageExportCSV_default as useMessageExportCSV
271
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "react-csv-autopilot",
3
+ "version": "0.0.1-beta.2",
4
+ "description": "React hooks for CSV export with automatic pagination - drop the function, we handle the rest",
5
+ "author": "Pavlo Kuzina",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "main": "dist/index.cjs",
10
+ "module": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^18 || ^19"
30
+ },
31
+ "devDependencies": {
32
+ "@types/react": "^18",
33
+ "@types/wicg-file-system-access": "^2023.10.7",
34
+ "react": "^19.2.3",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.0.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/PashaSchool/utils-kit.git",
41
+ "directory": "packages/react-csv-autopilot"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/PashaSchool/utils-kit/issues"
45
+ },
46
+ "homepage": "https://github.com/PashaSchool/utils-kit/blob/main/packages/react-csv-autopilot/README.md",
47
+ "keywords": [
48
+ "react",
49
+ "csv",
50
+ "export",
51
+ "hooks",
52
+ "autopilot",
53
+ "pagination",
54
+ "streaming",
55
+ "typescript",
56
+ "download",
57
+ "file-export",
58
+ "web-workers",
59
+ "progress-tracking",
60
+ "file-system-access",
61
+ "large-datasets",
62
+ "data-export"
63
+ ]
64
+ }