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.
Files changed (140) hide show
  1. package/README.md +31 -1
  2. package/bin/jtcsv.js +891 -821
  3. package/bin/jtcsv.ts +2534 -0
  4. package/csv-to-json.js +168 -145
  5. package/dist/jtcsv-core.cjs.js +1407 -0
  6. package/dist/jtcsv-core.cjs.js.map +1 -0
  7. package/dist/jtcsv-core.esm.js +1379 -0
  8. package/dist/jtcsv-core.esm.js.map +1 -0
  9. package/dist/jtcsv-core.umd.js +1413 -0
  10. package/dist/jtcsv-core.umd.js.map +1 -0
  11. package/dist/jtcsv-full.cjs.js +1912 -0
  12. package/dist/jtcsv-full.cjs.js.map +1 -0
  13. package/dist/jtcsv-full.esm.js +1880 -0
  14. package/dist/jtcsv-full.esm.js.map +1 -0
  15. package/dist/jtcsv-full.umd.js +1918 -0
  16. package/dist/jtcsv-full.umd.js.map +1 -0
  17. package/dist/jtcsv-workers.esm.js +759 -0
  18. package/dist/jtcsv-workers.esm.js.map +1 -0
  19. package/dist/jtcsv-workers.umd.js +773 -0
  20. package/dist/jtcsv-workers.umd.js.map +1 -0
  21. package/dist/jtcsv.cjs.js +61 -19
  22. package/dist/jtcsv.cjs.js.map +1 -1
  23. package/dist/jtcsv.esm.js +61 -19
  24. package/dist/jtcsv.esm.js.map +1 -1
  25. package/dist/jtcsv.umd.js +61 -19
  26. package/dist/jtcsv.umd.js.map +1 -1
  27. package/errors.js +188 -2
  28. package/examples/advanced/conditional-transformations.js +446 -0
  29. package/examples/advanced/conditional-transformations.ts +446 -0
  30. package/examples/advanced/csv-parser.worker.js +89 -0
  31. package/examples/advanced/csv-parser.worker.ts +89 -0
  32. package/examples/advanced/nested-objects-example.js +306 -0
  33. package/examples/advanced/nested-objects-example.ts +306 -0
  34. package/examples/advanced/performance-optimization.js +504 -0
  35. package/examples/advanced/performance-optimization.ts +504 -0
  36. package/examples/advanced/run-demo-server.js +116 -0
  37. package/examples/advanced/run-demo-server.ts +116 -0
  38. package/examples/advanced/web-worker-usage.html +874 -0
  39. package/examples/async-multithreaded-example.ts +335 -0
  40. package/examples/cli-advanced-usage.md +288 -0
  41. package/examples/cli-batch-processing.ts +38 -0
  42. package/examples/cli-tool.js +0 -3
  43. package/examples/cli-tool.ts +183 -0
  44. package/examples/error-handling.js +21 -7
  45. package/examples/error-handling.ts +356 -0
  46. package/examples/express-api.js +0 -3
  47. package/examples/express-api.ts +164 -0
  48. package/examples/large-dataset-example.js +0 -3
  49. package/examples/large-dataset-example.ts +204 -0
  50. package/examples/ndjson-processing.js +1 -1
  51. package/examples/ndjson-processing.ts +456 -0
  52. package/examples/plugin-excel-exporter.js +3 -4
  53. package/examples/plugin-excel-exporter.ts +406 -0
  54. package/examples/react-integration.tsx +637 -0
  55. package/examples/schema-validation.ts +640 -0
  56. package/examples/simple-usage.js +254 -254
  57. package/examples/simple-usage.ts +194 -0
  58. package/examples/streaming-example.js +4 -5
  59. package/examples/streaming-example.ts +419 -0
  60. package/examples/web-workers-advanced.ts +28 -0
  61. package/index.d.ts +1 -3
  62. package/index.js +15 -1
  63. package/json-save.js +9 -3
  64. package/json-to-csv.js +168 -21
  65. package/package.json +69 -10
  66. package/plugins/express-middleware/README.md +21 -2
  67. package/plugins/express-middleware/example.js +3 -4
  68. package/plugins/express-middleware/example.ts +135 -0
  69. package/plugins/express-middleware/index.d.ts +1 -1
  70. package/plugins/express-middleware/index.js +270 -118
  71. package/plugins/express-middleware/index.ts +557 -0
  72. package/plugins/fastify-plugin/index.js +2 -4
  73. package/plugins/fastify-plugin/index.ts +443 -0
  74. package/plugins/hono/index.ts +226 -0
  75. package/plugins/nestjs/index.ts +201 -0
  76. package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
  77. package/plugins/nextjs-api/examples/api-convert.js +0 -2
  78. package/plugins/nextjs-api/examples/api-convert.ts +67 -0
  79. package/plugins/nextjs-api/index.tsx +339 -0
  80. package/plugins/nextjs-api/route.js +2 -3
  81. package/plugins/nextjs-api/route.ts +370 -0
  82. package/plugins/nuxt/index.ts +94 -0
  83. package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
  84. package/plugins/nuxt/runtime/plugin.ts +71 -0
  85. package/plugins/remix/index.js +1 -1
  86. package/plugins/remix/index.ts +260 -0
  87. package/plugins/sveltekit/index.js +1 -1
  88. package/plugins/sveltekit/index.ts +301 -0
  89. package/plugins/trpc/index.ts +267 -0
  90. package/src/browser/browser-functions.ts +402 -0
  91. package/src/browser/core.js +92 -0
  92. package/src/browser/core.ts +152 -0
  93. package/src/browser/csv-to-json-browser.d.ts +3 -0
  94. package/src/browser/csv-to-json-browser.js +36 -14
  95. package/src/browser/csv-to-json-browser.ts +264 -0
  96. package/src/browser/errors-browser.ts +303 -0
  97. package/src/browser/extensions/plugins.js +92 -0
  98. package/src/browser/extensions/plugins.ts +93 -0
  99. package/src/browser/extensions/workers.js +39 -0
  100. package/src/browser/extensions/workers.ts +39 -0
  101. package/src/browser/globals.d.ts +5 -0
  102. package/src/browser/index.ts +192 -0
  103. package/src/browser/json-to-csv-browser.d.ts +3 -0
  104. package/src/browser/json-to-csv-browser.js +13 -3
  105. package/src/browser/json-to-csv-browser.ts +262 -0
  106. package/src/browser/streams.js +12 -2
  107. package/src/browser/streams.ts +336 -0
  108. package/src/browser/workers/csv-parser.worker.ts +377 -0
  109. package/src/browser/workers/worker-pool.ts +548 -0
  110. package/src/core/delimiter-cache.js +22 -8
  111. package/src/core/delimiter-cache.ts +310 -0
  112. package/src/core/node-optimizations.ts +449 -0
  113. package/src/core/plugin-system.js +29 -11
  114. package/src/core/plugin-system.ts +400 -0
  115. package/src/core/transform-hooks.ts +558 -0
  116. package/src/engines/fast-path-engine-new.ts +347 -0
  117. package/src/engines/fast-path-engine.ts +854 -0
  118. package/src/errors.ts +72 -0
  119. package/src/formats/ndjson-parser.ts +469 -0
  120. package/src/formats/tsv-parser.ts +334 -0
  121. package/src/index-with-plugins.js +16 -9
  122. package/src/index-with-plugins.ts +395 -0
  123. package/src/types/index.ts +255 -0
  124. package/src/utils/bom-utils.js +259 -0
  125. package/src/utils/bom-utils.ts +373 -0
  126. package/src/utils/encoding-support.js +124 -0
  127. package/src/utils/encoding-support.ts +155 -0
  128. package/src/utils/schema-validator.js +19 -19
  129. package/src/utils/schema-validator.ts +819 -0
  130. package/src/utils/transform-loader.js +1 -1
  131. package/src/utils/transform-loader.ts +389 -0
  132. package/src/utils/zod-adapter.js +170 -0
  133. package/src/utils/zod-adapter.ts +280 -0
  134. package/src/web-server/index.js +10 -10
  135. package/src/web-server/index.ts +683 -0
  136. package/src/workers/csv-multithreaded.ts +310 -0
  137. package/src/workers/csv-parser.worker.ts +227 -0
  138. package/src/workers/worker-pool.ts +409 -0
  139. package/stream-csv-to-json.js +26 -8
  140. package/stream-json-to-csv.js +1 -0
@@ -0,0 +1,192 @@
1
+ // Браузерный entry point для jtcsv
2
+ // Экспортирует все функции с поддержкой браузера
3
+
4
+ import * as jsonToCsvBrowser from './json-to-csv-browser';
5
+ import * as csvToJsonBrowser from './csv-to-json-browser';
6
+ import {
7
+ downloadAsCsv,
8
+ parseCsvFile,
9
+ parseCsvFileStream,
10
+ jsonToCsvStream,
11
+ jsonToNdjsonStream,
12
+ csvToJsonStream
13
+ } from './browser-functions';
14
+ import { createWorkerPool, parseCSVWithWorker } from './workers/worker-pool';
15
+ import {
16
+ ValidationError,
17
+ SecurityError,
18
+ FileSystemError,
19
+ ParsingError,
20
+ LimitError,
21
+ ConfigurationError,
22
+ ERROR_CODES
23
+ } from './errors-browser';
24
+
25
+ import type { JsonToCsvOptions, CsvToJsonOptions } from '../types';
26
+
27
+ const { jsonToCsv, preprocessData, deepUnwrap } = jsonToCsvBrowser as any;
28
+ const { csvToJson, csvToJsonIterator, autoDetectDelimiter } = csvToJsonBrowser as any;
29
+
30
+ /**
31
+ * Опции для ленивой инициализации Worker Pool
32
+ */
33
+ interface WorkerPoolOptions {
34
+ size?: number;
35
+ timeout?: number;
36
+ onError?: (error: Error) => void;
37
+ }
38
+
39
+ /**
40
+ * Ленивая инициализация Worker Pool
41
+ */
42
+ async function createWorkerPoolLazy(options: any = {}): Promise<any> {
43
+ const mod = await import('./workers/worker-pool');
44
+ return mod.createWorkerPool(options);
45
+ }
46
+
47
+ /**
48
+ * Ленивый парсинг CSV с использованием Worker
49
+ */
50
+ async function parseCSVWithWorkerLazy(
51
+ csvInput: string | File,
52
+ options: CsvToJsonOptions = {},
53
+ onProgress?: (progress: number) => void
54
+ ): Promise<any[]> {
55
+ const mod = await import('./workers/worker-pool');
56
+ return mod.parseCSVWithWorker(csvInput, options, onProgress);
57
+ }
58
+
59
+ /**
60
+ * Асинхронная версия jsonToCsv
61
+ */
62
+ async function jsonToCsvAsync(data: any, options: JsonToCsvOptions = {}): Promise<string> {
63
+ return jsonToCsv(data, options);
64
+ }
65
+
66
+ /**
67
+ * Асинхронная версия csvToJson
68
+ */
69
+ async function csvToJsonAsync(csv: string, options: CsvToJsonOptions = {}): Promise<any[]> {
70
+ return csvToJson(csv, options);
71
+ }
72
+
73
+ /**
74
+ * Асинхронная версия parseCsvFile
75
+ */
76
+ async function parseCsvFileAsync(file: File, options: CsvToJsonOptions = {}): Promise<any[]> {
77
+ return parseCsvFile(file, options);
78
+ }
79
+
80
+ /**
81
+ * Асинхронная версия autoDetectDelimiter
82
+ */
83
+ async function autoDetectDelimiterAsync(csv: string): Promise<string> {
84
+ return autoDetectDelimiter(csv);
85
+ }
86
+
87
+ /**
88
+ * Асинхронная версия downloadAsCsv
89
+ */
90
+ async function downloadAsCsvAsync(
91
+ data: any,
92
+ filename: string = 'export.csv',
93
+ options: JsonToCsvOptions = {}
94
+ ): Promise<void> {
95
+ return downloadAsCsv(data, filename, options);
96
+ }
97
+
98
+ // Основной экспорт
99
+ const jtcsv = {
100
+ // JSON to CSV функции
101
+ jsonToCsv,
102
+ preprocessData,
103
+ downloadAsCsv,
104
+ deepUnwrap,
105
+
106
+ // CSV to JSON функции
107
+ csvToJson,
108
+ csvToJsonIterator,
109
+ parseCsvFile,
110
+ parseCsvFileStream,
111
+ jsonToCsvStream,
112
+ jsonToNdjsonStream,
113
+ csvToJsonStream,
114
+ autoDetectDelimiter,
115
+
116
+ // Web Workers функции
117
+ createWorkerPool,
118
+ parseCSVWithWorker,
119
+ createWorkerPoolLazy,
120
+ parseCSVWithWorkerLazy,
121
+
122
+ // Асинхронные функции
123
+ jsonToCsvAsync,
124
+ csvToJsonAsync,
125
+ parseCsvFileAsync,
126
+ autoDetectDelimiterAsync,
127
+ downloadAsCsvAsync,
128
+
129
+ // Error classes
130
+ ValidationError,
131
+ SecurityError,
132
+ FileSystemError,
133
+ ParsingError,
134
+ LimitError,
135
+ ConfigurationError,
136
+ ERROR_CODES,
137
+
138
+ // Удобные алиасы
139
+ parse: csvToJson,
140
+ unparse: jsonToCsv,
141
+ parseAsync: csvToJsonAsync,
142
+ unparseAsync: jsonToCsvAsync,
143
+
144
+ // Версия
145
+ version: '2.0.0-browser'
146
+ };
147
+
148
+ // Экспорт для разных сред
149
+ if (typeof module !== 'undefined' && module.exports) {
150
+ // Node.js CommonJS
151
+ module.exports = jtcsv;
152
+ } else if (typeof define === 'function' && define.amd) {
153
+ // AMD
154
+ define([], () => jtcsv);
155
+ } else if (typeof window !== 'undefined') {
156
+ // Браузер (глобальная переменная)
157
+ (window as any).jtcsv = jtcsv;
158
+ }
159
+
160
+ export default jtcsv;
161
+ export {
162
+ jsonToCsv,
163
+ preprocessData,
164
+ downloadAsCsv,
165
+ deepUnwrap,
166
+ csvToJson,
167
+ csvToJsonIterator,
168
+ parseCsvFile,
169
+ parseCsvFileStream,
170
+ jsonToCsvStream,
171
+ jsonToNdjsonStream,
172
+ csvToJsonStream,
173
+ autoDetectDelimiter,
174
+ createWorkerPool,
175
+ parseCSVWithWorker,
176
+ createWorkerPoolLazy,
177
+ parseCSVWithWorkerLazy,
178
+ // Асинхронные функции
179
+ jsonToCsvAsync,
180
+ csvToJsonAsync,
181
+ parseCsvFileAsync,
182
+ autoDetectDelimiterAsync,
183
+ downloadAsCsvAsync,
184
+ // Error classes
185
+ ValidationError,
186
+ SecurityError,
187
+ FileSystemError,
188
+ ParsingError,
189
+ LimitError,
190
+ ConfigurationError,
191
+ ERROR_CODES
192
+ };
@@ -0,0 +1,3 @@
1
+ export function jsonToCsv(data: any, options?: any): string;
2
+ export function preprocessData(data: any[]): any[];
3
+ export function deepUnwrap(value: any, depth?: number, maxDepth?: number, visited?: Set<any>): any;
@@ -152,8 +152,18 @@ export function jsonToCsv(data, options = {}) {
152
152
 
153
153
  // Защита от CSV инъекций
154
154
  let escapedValue = stringValue;
155
- if (preventCsvInjection && /^[=+\-@]/.test(stringValue)) {
156
- escapedValue = "'" + stringValue;
155
+ if (preventCsvInjection) {
156
+ // Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
157
+ if (/^[=+\-@\t\r]/.test(stringValue)) {
158
+ escapedValue = "'" + stringValue;
159
+ }
160
+ // Unicode Bidi override characters
161
+ const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
162
+ for (const bidi of bidiChars) {
163
+ if (stringValue.includes(bidi)) {
164
+ escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
165
+ }
166
+ }
157
167
  }
158
168
 
159
169
  // Соответствие RFC 4180
@@ -306,4 +316,4 @@ if (typeof module !== 'undefined' && module.exports) {
306
316
  preprocessData,
307
317
  deepUnwrap
308
318
  };
309
- }
319
+ }
@@ -0,0 +1,262 @@
1
+ // Браузерная версия JSON to CSV конвертера
2
+ // Адаптирована для работы в браузере без Node.js API
3
+
4
+ import {
5
+ ValidationError,
6
+ ConfigurationError,
7
+ LimitError,
8
+ safeExecute
9
+ } from './errors-browser';
10
+
11
+ import type { JsonToCsvOptions } from '../types';
12
+
13
+ /**
14
+ * Валидация входных данных и опций
15
+ * @private
16
+ */
17
+ function validateInput(data: any[], options: JsonToCsvOptions): boolean {
18
+ // Validate data
19
+ if (!Array.isArray(data)) {
20
+ throw new ValidationError('Input data must be an array');
21
+ }
22
+
23
+ // Validate options
24
+ if (options && typeof options !== 'object') {
25
+ throw new ConfigurationError('Options must be an object');
26
+ }
27
+
28
+ // Validate delimiter
29
+ if (options?.delimiter && typeof options.delimiter !== 'string') {
30
+ throw new ConfigurationError('Delimiter must be a string');
31
+ }
32
+
33
+ if (options?.delimiter && options.delimiter.length !== 1) {
34
+ throw new ConfigurationError('Delimiter must be a single character');
35
+ }
36
+
37
+ // Validate renameMap
38
+ if (options?.renameMap && typeof options.renameMap !== 'object') {
39
+ throw new ConfigurationError('renameMap must be an object');
40
+ }
41
+
42
+ // Validate maxRecords
43
+ if (options && options.maxRecords !== undefined) {
44
+ if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
45
+ throw new ConfigurationError('maxRecords must be a positive number');
46
+ }
47
+ }
48
+
49
+ // Validate preventCsvInjection
50
+ if (options?.preventCsvInjection !== undefined && typeof options.preventCsvInjection !== 'boolean') {
51
+ throw new ConfigurationError('preventCsvInjection must be a boolean');
52
+ }
53
+
54
+ // Validate rfc4180Compliant
55
+ if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
56
+ throw new ConfigurationError('rfc4180Compliant must be a boolean');
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Экранирование CSV значений для предотвращения инъекций
64
+ * @private
65
+ */
66
+ function escapeCsvValue(value: string, preventInjection: boolean = true): string {
67
+ if (value === null || value === undefined) {
68
+ return '';
69
+ }
70
+
71
+ const str = String(value);
72
+
73
+ // Экранирование формул для предотвращения CSV инъекций
74
+ if (preventInjection && /^[=+\-@]/.test(str)) {
75
+ return "'" + str;
76
+ }
77
+
78
+ // Экранирование кавычек и переносов строк
79
+ if (str.includes('"') || str.includes('\n') || str.includes('\r') || str.includes(',')) {
80
+ return '"' + str.replace(/"/g, '""') + '"';
81
+ }
82
+
83
+ return str;
84
+ }
85
+
86
+ /**
87
+ * Извлечение всех уникальных ключей из массива объектов
88
+ * @private
89
+ */
90
+ function extractAllKeys(data: any[]): string[] {
91
+ const keys = new Set<string>();
92
+
93
+ for (const item of data) {
94
+ if (item && typeof item === 'object') {
95
+ Object.keys(item).forEach(key => keys.add(key));
96
+ }
97
+ }
98
+
99
+ return Array.from(keys);
100
+ }
101
+
102
+ /**
103
+ * Конвертация массива объектов в CSV строку
104
+ *
105
+ * @param data - Массив объектов для конвертации
106
+ * @param options - Опции конвертации
107
+ * @returns CSV строка
108
+ */
109
+ export function jsonToCsv(data: any[], options: JsonToCsvOptions = {}): string {
110
+ return safeExecute(() => {
111
+ validateInput(data, options);
112
+
113
+ if (data.length === 0) {
114
+ return '';
115
+ }
116
+
117
+ // Настройки по умолчанию
118
+ const delimiter = options.delimiter || ';';
119
+ const includeHeaders = options.includeHeaders !== false;
120
+ const maxRecords = options.maxRecords || data.length;
121
+ const preventInjection = options.preventCsvInjection !== false;
122
+ const rfc4180Compliant = options.rfc4180Compliant !== false;
123
+
124
+ // Ограничение количества записей
125
+ const limitedData = data.slice(0, maxRecords);
126
+
127
+ // Извлечение всех ключей
128
+ const allKeys = extractAllKeys(limitedData);
129
+
130
+ // Применение renameMap если есть
131
+ const renameMap = options.renameMap || {};
132
+ const finalKeys = allKeys.map(key => renameMap[key] || key);
133
+
134
+ // Создание CSV строки
135
+ const lines: string[] = [];
136
+
137
+ // Заголовки
138
+ if (includeHeaders) {
139
+ const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
140
+ lines.push(headerLine);
141
+ }
142
+
143
+ // Данные
144
+ for (const item of limitedData) {
145
+ const rowValues = allKeys.map(key => {
146
+ const value = item?.[key];
147
+ return escapeCsvValue(value, preventInjection);
148
+ });
149
+
150
+ lines.push(rowValues.join(delimiter));
151
+ }
152
+
153
+ // RFC 4180 compliance: CRLF line endings
154
+ if (rfc4180Compliant) {
155
+ return lines.join('\r\n');
156
+ }
157
+
158
+ return lines.join('\n');
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Асинхронная версия jsonToCsv
164
+ */
165
+ export async function jsonToCsvAsync(data: any[], options: JsonToCsvOptions = {}): Promise<string> {
166
+ return jsonToCsv(data, options);
167
+ }
168
+
169
+ /**
170
+ * Создает итератор для потоковой конвертации JSON в CSV
171
+ *
172
+ * @param data - Массив объектов или async итератор
173
+ * @param options - Опции конвертации
174
+ * @returns AsyncIterator с CSV чанками
175
+ */
176
+ export async function* jsonToCsvIterator(data: any[] | AsyncIterable<any>, options: JsonToCsvOptions = {}): AsyncGenerator<string> {
177
+ validateInput(Array.isArray(data) ? data : [], options);
178
+
179
+ const delimiter = options.delimiter || ';';
180
+ const includeHeaders = options.includeHeaders !== false;
181
+ const preventInjection = options.preventCsvInjection !== false;
182
+ const rfc4180Compliant = options.rfc4180Compliant !== false;
183
+
184
+ let isFirstChunk = true;
185
+ let allKeys: string[] = [];
186
+ let renameMap: Record<string, string> = {};
187
+
188
+ // Если данные - массив, обрабатываем как массив
189
+ if (Array.isArray(data)) {
190
+ if (data.length === 0) {
191
+ return;
192
+ }
193
+
194
+ allKeys = extractAllKeys(data);
195
+ renameMap = options.renameMap || {};
196
+ const finalKeys = allKeys.map(key => renameMap[key] || key);
197
+
198
+ // Заголовки
199
+ if (includeHeaders) {
200
+ const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
201
+ yield headerLine + (rfc4180Compliant ? '\r\n' : '\n');
202
+ }
203
+
204
+ // Данные
205
+ for (const item of data) {
206
+ const rowValues = allKeys.map(key => {
207
+ const value = item?.[key];
208
+ return escapeCsvValue(value, preventInjection);
209
+ });
210
+
211
+ yield rowValues.join(delimiter) + (rfc4180Compliant ? '\r\n' : '\n');
212
+ }
213
+ } else {
214
+ // Для async итератора нужна другая логика
215
+ throw new ValidationError('Async iterators not yet implemented in browser version');
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Асинхронная версия jsonToCsvIterator (псевдоним)
221
+ */
222
+ export const jsonToCsvIteratorAsync = jsonToCsvIterator;
223
+
224
+ /**
225
+ * Безопасная конвертация с обработкой ошибок
226
+ *
227
+ * @param data - Массив объектов
228
+ * @param options - Опции конвертации
229
+ * @returns CSV строка или null при ошибке
230
+ */
231
+ export function jsonToCsvSafe(data: any[], options: JsonToCsvOptions = {}): string | null {
232
+ try {
233
+ return jsonToCsv(data, options);
234
+ } catch (error) {
235
+ console.error('JSON to CSV conversion error:', error);
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Асинхронная версия jsonToCsvSafe
242
+ */
243
+ export async function jsonToCsvSafeAsync(data: any[], options: JsonToCsvOptions = {}): Promise<string | null> {
244
+ try {
245
+ return await jsonToCsvAsync(data, options);
246
+ } catch (error) {
247
+ console.error('JSON to CSV conversion error:', error);
248
+ return null;
249
+ }
250
+ }
251
+
252
+ // Экспорт для Node.js совместимости
253
+ if (typeof module !== 'undefined' && module.exports) {
254
+ module.exports = {
255
+ jsonToCsv,
256
+ jsonToCsvAsync,
257
+ jsonToCsvIterator,
258
+ jsonToCsvIteratorAsync,
259
+ jsonToCsvSafe,
260
+ jsonToCsvSafeAsync
261
+ };
262
+ }
@@ -211,8 +211,18 @@ function escapeCsvValue(value, options) {
211
211
 
212
212
  const stringValue = String(value);
213
213
  let escapedValue = stringValue;
214
- if (preventCsvInjection && /^[=+\-@]/.test(stringValue)) {
215
- escapedValue = "'" + stringValue;
214
+ if (preventCsvInjection) {
215
+ // Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
216
+ if (/^[=+\-@\t\r]/.test(stringValue)) {
217
+ escapedValue = "'" + stringValue;
218
+ }
219
+ // Unicode Bidi override characters
220
+ const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
221
+ for (const bidi of bidiChars) {
222
+ if (stringValue.includes(bidi)) {
223
+ escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
224
+ }
225
+ }
216
226
  }
217
227
 
218
228
  const needsQuoting = rfc4180Compliant