jtcsv 2.2.7 → 3.0.0
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 +31 -1
- package/bin/jtcsv.js +891 -821
- package/bin/jtcsv.ts +2534 -0
- package/csv-to-json.js +168 -145
- package/dist/jtcsv-core.cjs.js +1407 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1379 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1413 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +1912 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +1880 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +1918 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +759 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +773 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +61 -19
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +61 -19
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +61 -19
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +188 -2
- package/examples/advanced/conditional-transformations.js +446 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.js +89 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.js +306 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.js +504 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.js +116 -0
- package/examples/advanced/run-demo-server.ts +116 -0
- package/examples/advanced/web-worker-usage.html +874 -0
- package/examples/async-multithreaded-example.ts +335 -0
- package/examples/cli-advanced-usage.md +288 -0
- package/examples/cli-batch-processing.ts +38 -0
- package/examples/cli-tool.js +0 -3
- package/examples/cli-tool.ts +183 -0
- package/examples/error-handling.js +21 -7
- package/examples/error-handling.ts +356 -0
- package/examples/express-api.js +0 -3
- package/examples/express-api.ts +164 -0
- package/examples/large-dataset-example.js +0 -3
- package/examples/large-dataset-example.ts +204 -0
- package/examples/ndjson-processing.js +1 -1
- package/examples/ndjson-processing.ts +456 -0
- package/examples/plugin-excel-exporter.js +3 -4
- package/examples/plugin-excel-exporter.ts +406 -0
- package/examples/react-integration.tsx +637 -0
- package/examples/schema-validation.ts +640 -0
- package/examples/simple-usage.js +254 -254
- package/examples/simple-usage.ts +194 -0
- package/examples/streaming-example.js +4 -5
- package/examples/streaming-example.ts +419 -0
- package/examples/web-workers-advanced.ts +28 -0
- package/index.d.ts +1 -3
- package/index.js +15 -1
- package/json-save.js +9 -3
- package/json-to-csv.js +168 -21
- package/package.json +69 -10
- package/plugins/express-middleware/README.md +21 -2
- package/plugins/express-middleware/example.js +3 -4
- package/plugins/express-middleware/example.ts +135 -0
- package/plugins/express-middleware/index.d.ts +1 -1
- package/plugins/express-middleware/index.js +270 -118
- package/plugins/express-middleware/index.ts +557 -0
- package/plugins/fastify-plugin/index.js +2 -4
- package/plugins/fastify-plugin/index.ts +443 -0
- package/plugins/hono/index.ts +226 -0
- package/plugins/nestjs/index.ts +201 -0
- package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +0 -2
- package/plugins/nextjs-api/examples/api-convert.ts +67 -0
- package/plugins/nextjs-api/index.tsx +339 -0
- package/plugins/nextjs-api/route.js +2 -3
- package/plugins/nextjs-api/route.ts +370 -0
- package/plugins/nuxt/index.ts +94 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
- package/plugins/nuxt/runtime/plugin.ts +71 -0
- package/plugins/remix/index.js +1 -1
- package/plugins/remix/index.ts +260 -0
- package/plugins/sveltekit/index.js +1 -1
- package/plugins/sveltekit/index.ts +301 -0
- package/plugins/trpc/index.ts +267 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.js +92 -0
- package/src/browser/core.ts +152 -0
- package/src/browser/csv-to-json-browser.d.ts +3 -0
- package/src/browser/csv-to-json-browser.js +36 -14
- package/src/browser/csv-to-json-browser.ts +264 -0
- package/src/browser/errors-browser.ts +303 -0
- package/src/browser/extensions/plugins.js +92 -0
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.js +39 -0
- package/src/browser/extensions/workers.ts +39 -0
- package/src/browser/globals.d.ts +5 -0
- package/src/browser/index.ts +192 -0
- package/src/browser/json-to-csv-browser.d.ts +3 -0
- package/src/browser/json-to-csv-browser.js +13 -3
- package/src/browser/json-to-csv-browser.ts +262 -0
- package/src/browser/streams.js +12 -2
- package/src/browser/streams.ts +336 -0
- package/src/browser/workers/csv-parser.worker.ts +377 -0
- package/src/browser/workers/worker-pool.ts +548 -0
- package/src/core/delimiter-cache.js +22 -8
- package/src/core/delimiter-cache.ts +310 -0
- package/src/core/node-optimizations.ts +449 -0
- package/src/core/plugin-system.js +29 -11
- package/src/core/plugin-system.ts +400 -0
- package/src/core/transform-hooks.ts +558 -0
- package/src/engines/fast-path-engine-new.ts +347 -0
- package/src/engines/fast-path-engine.ts +854 -0
- package/src/errors.ts +72 -0
- package/src/formats/ndjson-parser.ts +469 -0
- package/src/formats/tsv-parser.ts +334 -0
- package/src/index-with-plugins.js +16 -9
- package/src/index-with-plugins.ts +395 -0
- package/src/types/index.ts +255 -0
- package/src/utils/bom-utils.js +259 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.js +124 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/schema-validator.js +19 -19
- package/src/utils/schema-validator.ts +819 -0
- package/src/utils/transform-loader.js +1 -1
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/zod-adapter.js +170 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/index.js +10 -10
- package/src/web-server/index.ts +683 -0
- package/src/workers/csv-multithreaded.ts +310 -0
- package/src/workers/csv-parser.worker.ts +227 -0
- package/src/workers/worker-pool.ts +409 -0
- package/stream-csv-to-json.js +26 -8
- package/stream-json-to-csv.js +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remix plugin for jtcsv
|
|
3
|
+
* Provides utilities for CSV parsing and generation in Remix applications
|
|
4
|
+
* @module plugins/remix
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { csvToJson, jsonToCsv } from '../../index-core';
|
|
8
|
+
import type { CsvToJsonOptions, JsonToCsvOptions } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remix Request type (simplified)
|
|
12
|
+
*/
|
|
13
|
+
interface RemixRequest {
|
|
14
|
+
formData(): Promise<FormData>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for CSV parsing from form data
|
|
19
|
+
*/
|
|
20
|
+
export interface ParseFormDataOptions extends CsvToJsonOptions {
|
|
21
|
+
/** Field name containing the CSV file (default: 'file') */
|
|
22
|
+
fieldName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for CSV response generation
|
|
27
|
+
*/
|
|
28
|
+
export interface GenerateCsvResponseOptions extends JsonToCsvOptions {
|
|
29
|
+
// Additional options specific to CSV response
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize filename for CSV download
|
|
34
|
+
*/
|
|
35
|
+
function normalizeFilename(filename?: string): string {
|
|
36
|
+
if (!filename || typeof filename !== 'string') {
|
|
37
|
+
return 'export.csv';
|
|
38
|
+
}
|
|
39
|
+
return filename.includes('.') ? filename : `${filename}.csv`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract CSV text from FormData
|
|
44
|
+
*/
|
|
45
|
+
async function extractCsvText(formData: FormData, fieldName: string): Promise<string | null> {
|
|
46
|
+
if (formData.has(fieldName)) {
|
|
47
|
+
const value = formData.get(fieldName);
|
|
48
|
+
if (value && typeof (value as any).text === 'function') {
|
|
49
|
+
return await (value as any).text();
|
|
50
|
+
}
|
|
51
|
+
if (value !== null) {
|
|
52
|
+
return String(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const value of formData.values()) {
|
|
57
|
+
if (value && typeof (value as any).text === 'function') {
|
|
58
|
+
return await (value as any).text();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse CSV from Remix form data
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // In a Remix action:
|
|
70
|
+
* export async function action({ request }: ActionArgs) {
|
|
71
|
+
* const data = await parseFormData(request, { delimiter: ',' });
|
|
72
|
+
* return json({ success: true, data });
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
export async function parseFormData(
|
|
76
|
+
request: RemixRequest,
|
|
77
|
+
options: ParseFormDataOptions = {}
|
|
78
|
+
): Promise<any[]> {
|
|
79
|
+
if (!request || typeof request.formData !== 'function') {
|
|
80
|
+
throw new Error('parseFormData expects a Remix Request with formData()');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { fieldName = 'file', ...csvOptions } = options;
|
|
84
|
+
const formData = await request.formData();
|
|
85
|
+
const csvText = await extractCsvText(formData, fieldName);
|
|
86
|
+
|
|
87
|
+
if (!csvText) {
|
|
88
|
+
throw new Error('No CSV file or field found in form data');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return await csvToJson(csvText, csvOptions);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Async version of parseFormData with better error handling
|
|
96
|
+
*/
|
|
97
|
+
export async function parseFormDataAsync(
|
|
98
|
+
request: RemixRequest,
|
|
99
|
+
options: ParseFormDataOptions = {}
|
|
100
|
+
): Promise<any[]> {
|
|
101
|
+
try {
|
|
102
|
+
return await parseFormData(request, options);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(`Failed to parse CSV from form data: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate a CSV response for Remix
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // In a Remix loader:
|
|
113
|
+
* export async function loader() {
|
|
114
|
+
* const data = await getData();
|
|
115
|
+
* return generateCsvResponse(data, 'export.csv');
|
|
116
|
+
* }
|
|
117
|
+
*/
|
|
118
|
+
export function generateCsvResponse(
|
|
119
|
+
data: any,
|
|
120
|
+
filename: string = 'export.csv',
|
|
121
|
+
options: GenerateCsvResponseOptions = {}
|
|
122
|
+
): Response {
|
|
123
|
+
const safeName = normalizeFilename(filename);
|
|
124
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
125
|
+
const csv = jsonToCsv(rows, options);
|
|
126
|
+
|
|
127
|
+
return new Response(csv, {
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
130
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Async version of generateCsvResponse
|
|
137
|
+
*/
|
|
138
|
+
export async function generateAsyncCsvResponse(
|
|
139
|
+
data: any,
|
|
140
|
+
filename: string = 'export.csv',
|
|
141
|
+
options: GenerateCsvResponseOptions = {}
|
|
142
|
+
): Promise<Response> {
|
|
143
|
+
const safeName = normalizeFilename(filename);
|
|
144
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
145
|
+
const csv = await jsonToCsv(rows, options);
|
|
146
|
+
|
|
147
|
+
return new Response(csv, {
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
150
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* CSV loader helper for Remix
|
|
157
|
+
* Creates a loader that returns CSV data
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* export const loader = createCsvLoader(async () => {
|
|
161
|
+
* return await getData();
|
|
162
|
+
* }, 'data.csv');
|
|
163
|
+
*/
|
|
164
|
+
export function createCsvLoader(
|
|
165
|
+
dataLoader: () => Promise<any> | any,
|
|
166
|
+
filename: string = 'export.csv',
|
|
167
|
+
options: GenerateCsvResponseOptions = {}
|
|
168
|
+
): () => Promise<Response> {
|
|
169
|
+
return async () => {
|
|
170
|
+
try {
|
|
171
|
+
const data = await (typeof dataLoader === 'function' ? dataLoader() : dataLoader);
|
|
172
|
+
return generateCsvResponse(data, filename, options);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return new Response(
|
|
175
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
176
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* CSV action helper for Remix
|
|
184
|
+
* Creates an action that parses CSV from form data
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* export const action = createCsvAction({ delimiter: ',' });
|
|
188
|
+
*/
|
|
189
|
+
export function createCsvAction(
|
|
190
|
+
options: ParseFormDataOptions = {}
|
|
191
|
+
): (args: { request: RemixRequest }) => Promise<Response> {
|
|
192
|
+
return async ({ request }) => {
|
|
193
|
+
try {
|
|
194
|
+
const data = await parseFormData(request, options);
|
|
195
|
+
return new Response(
|
|
196
|
+
JSON.stringify({ success: true, data }),
|
|
197
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
198
|
+
);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return new Response(
|
|
201
|
+
JSON.stringify({
|
|
202
|
+
success: false,
|
|
203
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
204
|
+
}),
|
|
205
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Multi-part form data CSV parser
|
|
213
|
+
* Handles multiple CSV files in a single form
|
|
214
|
+
*/
|
|
215
|
+
export async function parseMultiPartFormData(
|
|
216
|
+
request: RemixRequest,
|
|
217
|
+
options: ParseFormDataOptions & { multiple?: boolean } = {}
|
|
218
|
+
): Promise<any[] | any[][]> {
|
|
219
|
+
const { multiple = false, ...csvOptions } = options;
|
|
220
|
+
const formData = await request.formData();
|
|
221
|
+
const results: any[][] = [];
|
|
222
|
+
|
|
223
|
+
for (const [fieldName, value] of formData.entries()) {
|
|
224
|
+
if (value && typeof (value as any).text === 'function') {
|
|
225
|
+
const csvText = await (value as any).text();
|
|
226
|
+
if (csvText) {
|
|
227
|
+
const parsed = await csvToJson(csvText, csvOptions);
|
|
228
|
+
results.push(parsed);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!multiple && results.length > 0) {
|
|
234
|
+
return results[0];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* CSV export utility for Remix routes
|
|
242
|
+
*/
|
|
243
|
+
export function csvExport(
|
|
244
|
+
data: any,
|
|
245
|
+
filename?: string,
|
|
246
|
+
options?: GenerateCsvResponseOptions
|
|
247
|
+
): () => Promise<Response> {
|
|
248
|
+
return () => generateAsyncCsvResponse(data, filename, options);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
parseFormData,
|
|
253
|
+
parseFormDataAsync,
|
|
254
|
+
generateCsvResponse,
|
|
255
|
+
generateAsyncCsvResponse,
|
|
256
|
+
createCsvLoader,
|
|
257
|
+
createCsvAction,
|
|
258
|
+
parseMultiPartFormData,
|
|
259
|
+
csvExport,
|
|
260
|
+
};
|
|
@@ -21,7 +21,7 @@ async function parseCsv(request, options = {}) {
|
|
|
21
21
|
const value = formData.get(fieldName) ?? formData.values().next().value;
|
|
22
22
|
if (value && typeof value.text === 'function') {
|
|
23
23
|
csvText = await value.text();
|
|
24
|
-
} else if (value
|
|
24
|
+
} else if (value !== null) {
|
|
25
25
|
csvText = String(value);
|
|
26
26
|
}
|
|
27
27
|
} else {
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit plugin for jtcsv
|
|
3
|
+
* Provides utilities for CSV parsing and generation in SvelteKit applications
|
|
4
|
+
* @module plugins/sveltekit
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { csvToJson, jsonToCsv } from '../../index-core';
|
|
8
|
+
import type { CsvToJsonOptions, JsonToCsvOptions } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SvelteKit Request type (simplified)
|
|
12
|
+
*/
|
|
13
|
+
interface SvelteKitRequest {
|
|
14
|
+
text(): Promise<string>;
|
|
15
|
+
formData(): Promise<FormData>;
|
|
16
|
+
headers?: {
|
|
17
|
+
get(name: string): string | null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for CSV parsing from request
|
|
23
|
+
*/
|
|
24
|
+
export interface ParseCsvOptions extends CsvToJsonOptions {
|
|
25
|
+
/** Field name containing the CSV file (default: 'file') */
|
|
26
|
+
fieldName?: string;
|
|
27
|
+
/** Force form data parsing (default: auto-detect) */
|
|
28
|
+
formData?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for CSV response generation
|
|
33
|
+
*/
|
|
34
|
+
export interface GenerateCsvOptions extends JsonToCsvOptions {
|
|
35
|
+
// Additional options specific to CSV response
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalize filename for CSV download
|
|
40
|
+
*/
|
|
41
|
+
function normalizeFilename(filename?: string): string {
|
|
42
|
+
if (!filename || typeof filename !== 'string') {
|
|
43
|
+
return 'export.csv';
|
|
44
|
+
}
|
|
45
|
+
return filename.includes('.') ? filename : `${filename}.csv`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract CSV text from FormData
|
|
50
|
+
*/
|
|
51
|
+
async function extractCsvText(formData: FormData, fieldName: string): Promise<string | null> {
|
|
52
|
+
if (formData.has(fieldName)) {
|
|
53
|
+
const value = formData.get(fieldName);
|
|
54
|
+
if (value && typeof (value as any).text === 'function') {
|
|
55
|
+
return await (value as any).text();
|
|
56
|
+
}
|
|
57
|
+
if (value !== null) {
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const value of formData.values()) {
|
|
63
|
+
if (value && typeof (value as any).text === 'function') {
|
|
64
|
+
return await (value as any).text();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse CSV from SvelteKit request
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // In a SvelteKit endpoint:
|
|
76
|
+
* export async function POST({ request }) {
|
|
77
|
+
* const data = await parseCsv(request, { delimiter: ',' });
|
|
78
|
+
* return json({ success: true, data });
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export async function parseCsv(
|
|
82
|
+
request: SvelteKitRequest,
|
|
83
|
+
options: ParseCsvOptions = {}
|
|
84
|
+
): Promise<any[]> {
|
|
85
|
+
if (!request || typeof request.text !== 'function') {
|
|
86
|
+
throw new Error('parseCsv expects a Request instance');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { fieldName = 'file', formData: forceFormData, ...csvOptions } = options;
|
|
90
|
+
const contentType = request.headers?.get?.('content-type') || '';
|
|
91
|
+
let csvText: string | null = null;
|
|
92
|
+
|
|
93
|
+
if (forceFormData || contentType.includes('multipart/form-data')) {
|
|
94
|
+
const formData = await request.formData();
|
|
95
|
+
csvText = await extractCsvText(formData, fieldName);
|
|
96
|
+
} else {
|
|
97
|
+
csvText = await request.text();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!csvText) {
|
|
101
|
+
throw new Error('No CSV payload found in request');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return await csvToJson(csvText, csvOptions);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Async version of parseCsv with better error handling
|
|
109
|
+
*/
|
|
110
|
+
export async function parseCsvAsync(
|
|
111
|
+
request: SvelteKitRequest,
|
|
112
|
+
options: ParseCsvOptions = {}
|
|
113
|
+
): Promise<any[]> {
|
|
114
|
+
try {
|
|
115
|
+
return await parseCsv(request, options);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(`Failed to parse CSV from request: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate a CSV response for SvelteKit
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // In a SvelteKit endpoint:
|
|
126
|
+
* export async function GET() {
|
|
127
|
+
* const data = await getData();
|
|
128
|
+
* return generateCsv(data, 'export.csv');
|
|
129
|
+
* }
|
|
130
|
+
*/
|
|
131
|
+
export function generateCsv(
|
|
132
|
+
data: any,
|
|
133
|
+
filename: string = 'export.csv',
|
|
134
|
+
options: GenerateCsvOptions = {}
|
|
135
|
+
): Response {
|
|
136
|
+
const safeName = normalizeFilename(filename);
|
|
137
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
138
|
+
const csv = jsonToCsv(rows, options);
|
|
139
|
+
|
|
140
|
+
return new Response(csv, {
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
143
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Async version of generateCsv
|
|
150
|
+
*/
|
|
151
|
+
export async function generateAsyncCsv(
|
|
152
|
+
data: any,
|
|
153
|
+
filename: string = 'export.csv',
|
|
154
|
+
options: GenerateCsvOptions = {}
|
|
155
|
+
): Promise<Response> {
|
|
156
|
+
const safeName = normalizeFilename(filename);
|
|
157
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
158
|
+
const csv = await jsonToCsv(rows, options);
|
|
159
|
+
|
|
160
|
+
return new Response(csv, {
|
|
161
|
+
headers: {
|
|
162
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
163
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* CSV loader helper for SvelteKit
|
|
170
|
+
* Creates a loader that returns CSV data
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* export const loader = createCsvLoader(async () => {
|
|
174
|
+
* return await getData();
|
|
175
|
+
* }, 'data.csv');
|
|
176
|
+
*/
|
|
177
|
+
export function createCsvLoader(
|
|
178
|
+
dataLoader: () => Promise<any> | any,
|
|
179
|
+
filename: string = 'export.csv',
|
|
180
|
+
options: GenerateCsvOptions = {}
|
|
181
|
+
): () => Promise<Response> {
|
|
182
|
+
return async () => {
|
|
183
|
+
try {
|
|
184
|
+
const data = await (typeof dataLoader === 'function' ? dataLoader() : dataLoader);
|
|
185
|
+
return generateCsv(data, filename, options);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return new Response(
|
|
188
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
189
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* CSV action helper for SvelteKit
|
|
197
|
+
* Creates an action that parses CSV from form data
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* export const action = createCsvAction({ delimiter: ',' });
|
|
201
|
+
*/
|
|
202
|
+
export function createCsvAction(
|
|
203
|
+
options: ParseCsvOptions = {}
|
|
204
|
+
): (args: { request: SvelteKitRequest }) => Promise<Response> {
|
|
205
|
+
return async ({ request }) => {
|
|
206
|
+
try {
|
|
207
|
+
const data = await parseCsv(request, options);
|
|
208
|
+
return new Response(
|
|
209
|
+
JSON.stringify({ success: true, data }),
|
|
210
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
211
|
+
);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return new Response(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
success: false,
|
|
216
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
217
|
+
}),
|
|
218
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Multi-part form data CSV parser
|
|
226
|
+
* Handles multiple CSV files in a single request
|
|
227
|
+
*/
|
|
228
|
+
export async function parseMultiPartCsv(
|
|
229
|
+
request: SvelteKitRequest,
|
|
230
|
+
options: ParseCsvOptions & { multiple?: boolean } = {}
|
|
231
|
+
): Promise<any[] | any[][]> {
|
|
232
|
+
const { multiple = false, ...csvOptions } = options;
|
|
233
|
+
const formData = await request.formData();
|
|
234
|
+
const results: any[][] = [];
|
|
235
|
+
|
|
236
|
+
for (const [fieldName, value] of formData.entries()) {
|
|
237
|
+
if (value && typeof (value as any).text === 'function') {
|
|
238
|
+
const csvText = await (value as any).text();
|
|
239
|
+
if (csvText) {
|
|
240
|
+
const parsed = await csvToJson(csvText, csvOptions);
|
|
241
|
+
results.push(parsed);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!multiple && results.length > 0) {
|
|
247
|
+
return results[0];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* CSV export utility for SvelteKit routes
|
|
255
|
+
*/
|
|
256
|
+
export function csvExport(
|
|
257
|
+
data: any,
|
|
258
|
+
filename?: string,
|
|
259
|
+
options?: GenerateCsvOptions
|
|
260
|
+
): () => Promise<Response> {
|
|
261
|
+
return () => generateAsyncCsv(data, filename, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* SvelteKit server hook for CSV processing
|
|
266
|
+
* Can be used in hooks.server.js/ts
|
|
267
|
+
*/
|
|
268
|
+
export function createCsvHook(options: {
|
|
269
|
+
parseOptions?: ParseCsvOptions;
|
|
270
|
+
generateOptions?: GenerateCsvOptions;
|
|
271
|
+
} = {}) {
|
|
272
|
+
return {
|
|
273
|
+
async handle({ event, resolve }: { event: any; resolve: any }) {
|
|
274
|
+
// Add CSV utilities to event.locals
|
|
275
|
+
event.locals.csv = {
|
|
276
|
+
parse: (request: SvelteKitRequest, opts?: ParseCsvOptions) =>
|
|
277
|
+
parseCsv(request, { ...options.parseOptions, ...opts }),
|
|
278
|
+
parseAsync: (request: SvelteKitRequest, opts?: ParseCsvOptions) =>
|
|
279
|
+
parseCsvAsync(request, { ...options.parseOptions, ...opts }),
|
|
280
|
+
generate: (data: any, filename?: string, opts?: GenerateCsvOptions) =>
|
|
281
|
+
generateCsv(data, filename, { ...options.generateOptions, ...opts }),
|
|
282
|
+
generateAsync: (data: any, filename?: string, opts?: GenerateCsvOptions) =>
|
|
283
|
+
generateAsyncCsv(data, filename, { ...options.generateOptions, ...opts }),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return resolve(event);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default {
|
|
292
|
+
parseCsv,
|
|
293
|
+
parseCsvAsync,
|
|
294
|
+
generateCsv,
|
|
295
|
+
generateAsyncCsv,
|
|
296
|
+
createCsvLoader,
|
|
297
|
+
createCsvAction,
|
|
298
|
+
parseMultiPartCsv,
|
|
299
|
+
csvExport,
|
|
300
|
+
createCsvHook,
|
|
301
|
+
};
|