jtcsv 1.2.0 → 2.1.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 (46) hide show
  1. package/README.md +252 -337
  2. package/bin/jtcsv.js +167 -85
  3. package/cli-tui.js +0 -0
  4. package/dist/jtcsv.cjs.js +1619 -0
  5. package/dist/jtcsv.cjs.js.map +1 -0
  6. package/dist/jtcsv.esm.js +1599 -0
  7. package/dist/jtcsv.esm.js.map +1 -0
  8. package/dist/jtcsv.umd.js +1625 -0
  9. package/dist/jtcsv.umd.js.map +1 -0
  10. package/examples/cli-tool.js +186 -0
  11. package/examples/express-api.js +167 -0
  12. package/examples/large-dataset-example.js +185 -0
  13. package/examples/plugin-excel-exporter.js +407 -0
  14. package/examples/simple-usage.js +280 -0
  15. package/examples/streaming-example.js +419 -0
  16. package/index.d.ts +4 -0
  17. package/json-save.js +1 -1
  18. package/package.json +128 -14
  19. package/plugins/README.md +373 -0
  20. package/plugins/express-middleware/README.md +306 -0
  21. package/plugins/express-middleware/example.js +136 -0
  22. package/plugins/express-middleware/index.d.ts +114 -0
  23. package/plugins/express-middleware/index.js +360 -0
  24. package/plugins/express-middleware/package.json +52 -0
  25. package/plugins/fastify-plugin/index.js +406 -0
  26. package/plugins/fastify-plugin/package.json +55 -0
  27. package/plugins/nextjs-api/README.md +452 -0
  28. package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
  29. package/plugins/nextjs-api/examples/api-convert.js +69 -0
  30. package/plugins/nextjs-api/index.js +388 -0
  31. package/plugins/nextjs-api/package.json +63 -0
  32. package/plugins/nextjs-api/route.js +372 -0
  33. package/src/browser/browser-functions.js +189 -0
  34. package/src/browser/csv-to-json-browser.js +442 -0
  35. package/src/browser/errors-browser.js +194 -0
  36. package/src/browser/index.js +79 -0
  37. package/src/browser/json-to-csv-browser.js +309 -0
  38. package/src/browser/workers/csv-parser.worker.js +359 -0
  39. package/src/browser/workers/worker-pool.js +467 -0
  40. package/src/core/plugin-system.js +472 -0
  41. package/src/engines/fast-path-engine-new.js +338 -0
  42. package/src/engines/fast-path-engine.js +347 -0
  43. package/src/formats/ndjson-parser.js +419 -0
  44. package/src/index-with-plugins.js +349 -0
  45. package/stream-csv-to-json.js +1 -1
  46. package/stream-json-to-csv.js +1 -1
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Plugin System для JTCSV
3
+ * Middleware-like архитектура с поддержкой hooks и плагинов
4
+ *
5
+ * @version 1.0.0
6
+ * @date 2026-01-22
7
+ */
8
+
9
+ class PluginManager {
10
+ constructor() {
11
+ this.plugins = new Map();
12
+ this.hooks = new Map();
13
+ this.middlewares = [];
14
+ this.context = {};
15
+ this.stats = {
16
+ pluginLoads: 0,
17
+ hookExecutions: 0,
18
+ middlewareExecutions: 0
19
+ };
20
+
21
+ // Регистрируем стандартные hooks
22
+ this._registerDefaultHooks();
23
+ }
24
+
25
+ /**
26
+ * Регистрирует стандартные hooks
27
+ */
28
+ _registerDefaultHooks() {
29
+ const defaultHooks = [
30
+ 'before:csvToJson',
31
+ 'after:csvToJson',
32
+ 'before:jsonToCsv',
33
+ 'after:jsonToCsv',
34
+ 'before:parse',
35
+ 'after:parse',
36
+ 'before:serialize',
37
+ 'after:serialize',
38
+ 'error',
39
+ 'validation',
40
+ 'transformation'
41
+ ];
42
+
43
+ defaultHooks.forEach(hook => {
44
+ this.hooks.set(hook, []);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Регистрирует плагин
50
+ * @param {string} name - Уникальное имя плагина
51
+ * @param {Object} plugin - Конфигурация плагина
52
+ * @returns {PluginManager} this для chaining
53
+ *
54
+ * @example
55
+ * pluginManager.use('excel-exporter', {
56
+ * name: 'Excel Exporter',
57
+ * version: '1.0.0',
58
+ * description: 'Экспорт в Excel формат',
59
+ * hooks: {
60
+ * 'after:jsonToCsv': (csv) => convertToExcel(csv)
61
+ * },
62
+ * middlewares: [
63
+ * async (ctx, next) => {
64
+ * console.log('Before conversion:', ctx);
65
+ * await next();
66
+ * console.log('After conversion:', ctx);
67
+ * }
68
+ * ]
69
+ * });
70
+ */
71
+ use(name, plugin) {
72
+ if (this.plugins.has(name)) {
73
+ throw new Error(`Plugin "${name}" уже зарегистрирован`);
74
+ }
75
+
76
+ this._validatePlugin(plugin);
77
+
78
+ // Сохраняем плагин
79
+ this.plugins.set(name, {
80
+ ...plugin,
81
+ registeredAt: new Date(),
82
+ enabled: true
83
+ });
84
+
85
+ // Регистрируем hooks
86
+ if (plugin.hooks) {
87
+ Object.entries(plugin.hooks).forEach(([hookName, handler]) => {
88
+ this.registerHook(hookName, handler, name);
89
+ });
90
+ }
91
+
92
+ // Регистрируем middlewares
93
+ if (plugin.middlewares) {
94
+ plugin.middlewares.forEach((middleware, index) => {
95
+ this.registerMiddleware(middleware, `${name}:${index}`);
96
+ });
97
+ }
98
+
99
+ this.stats.pluginLoads++;
100
+ console.log(`✅ Plugin "${name}" зарегистрирован`);
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Валидирует плагин
106
+ */
107
+ _validatePlugin(plugin) {
108
+ if (!plugin.name || !plugin.version) {
109
+ throw new Error('Plugin должен иметь name и version');
110
+ }
111
+
112
+ // Проверяем обязательные поля
113
+ const required = ['name', 'version'];
114
+ required.forEach(field => {
115
+ if (!plugin[field]) {
116
+ throw new Error(`Plugin missing required field: ${field}`);
117
+ }
118
+ });
119
+
120
+ // Проверяем hooks если есть
121
+ if (plugin.hooks) {
122
+ if (typeof plugin.hooks !== 'object') {
123
+ throw new Error('Plugin hooks должен быть объектом');
124
+ }
125
+
126
+ Object.entries(plugin.hooks).forEach(([hookName, handler]) => {
127
+ if (typeof handler !== 'function') {
128
+ throw new Error(`Hook handler для "${hookName}" должен быть функцией`);
129
+ }
130
+ });
131
+ }
132
+
133
+ // Проверяем middlewares если есть
134
+ if (plugin.middlewares) {
135
+ if (!Array.isArray(plugin.middlewares)) {
136
+ throw new Error('Plugin middlewares должен быть массивом');
137
+ }
138
+
139
+ plugin.middlewares.forEach((middleware, index) => {
140
+ if (typeof middleware !== 'function') {
141
+ throw new Error(`Middleware ${index} должен быть функцией`);
142
+ }
143
+ });
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Регистрирует hook
149
+ * @param {string} hookName - Имя hook
150
+ * @param {Function} handler - Обработчик hook
151
+ * @param {string} pluginName - Имя плагина (опционально)
152
+ */
153
+ registerHook(hookName, handler, pluginName = null) {
154
+ if (!this.hooks.has(hookName)) {
155
+ this.hooks.set(hookName, []);
156
+ }
157
+
158
+ this.hooks.get(hookName).push({
159
+ handler,
160
+ pluginName,
161
+ registeredAt: new Date()
162
+ });
163
+
164
+ console.log(`📌 Hook "${hookName}" зарегистрирован${pluginName ? ` для плагина "${pluginName}"` : ''}`);
165
+ }
166
+
167
+ /**
168
+ * Регистрирует middleware
169
+ * @param {Function} middleware - Middleware функция
170
+ * @param {string} name - Имя middleware (опционально)
171
+ */
172
+ registerMiddleware(middleware, name = null) {
173
+ this.middlewares.push({
174
+ middleware,
175
+ name,
176
+ registeredAt: new Date()
177
+ });
178
+
179
+ console.log(`🔄 Middleware "${name || 'anonymous'}" зарегистрирован`);
180
+ }
181
+
182
+ /**
183
+ * Выполняет все handlers для конкретного hook
184
+ * @param {string} hookName - Имя hook
185
+ * @param {any} data - Данные для обработки
186
+ * @param {Object} context - Контекст выполнения
187
+ * @returns {Promise<any>} Результат обработки
188
+ */
189
+ async executeHooks(hookName, data, context = {}) {
190
+ const handlers = this.hooks.get(hookName) || [];
191
+
192
+ if (handlers.length === 0) {
193
+ return data;
194
+ }
195
+
196
+ console.log(`⚡ Выполнение hook "${hookName}" с ${handlers.length} обработчиками`);
197
+
198
+ let result = data;
199
+
200
+ for (const { handler, pluginName } of handlers) {
201
+ try {
202
+ const startTime = Date.now();
203
+ result = await handler(result, { ...context, pluginName });
204
+ const duration = Date.now() - startTime;
205
+
206
+ if (duration > 100) {
207
+ console.warn(`⚠️ Hook "${hookName}" от плагина "${pluginName}" выполнился за ${duration}ms`);
208
+ }
209
+ } catch (error) {
210
+ console.error(`❌ Ошибка в hook "${hookName}" от плагина "${pluginName}":`, error.message);
211
+
212
+ // Выполняем error hook если есть
213
+ await this.executeHooks('error', {
214
+ hook: hookName,
215
+ plugin: pluginName,
216
+ error,
217
+ data: result
218
+ }, context);
219
+
220
+ // Продолжаем выполнение с другими обработчиками
221
+ continue;
222
+ }
223
+ }
224
+
225
+ this.stats.hookExecutions++;
226
+ return result;
227
+ }
228
+
229
+ /**
230
+ * Выполняет middleware pipeline
231
+ * @param {Object} ctx - Контекст выполнения
232
+ * @returns {Promise<Object>} Обработанный контекст
233
+ */
234
+ async executeMiddlewares(ctx) {
235
+ if (this.middlewares.length === 0) {
236
+ return ctx;
237
+ }
238
+
239
+ console.log(`🚀 Запуск middleware pipeline с ${this.middlewares.length} middleware`);
240
+
241
+ let index = -1;
242
+ const middlewares = this.middlewares.map(m => m.middleware);
243
+
244
+ const dispatch = async (i) => {
245
+ if (i <= index) {
246
+ throw new Error('next() вызван несколько раз');
247
+ }
248
+
249
+ index = i;
250
+ const middleware = middlewares[i];
251
+
252
+ if (!middleware) {
253
+ return ctx;
254
+ }
255
+
256
+ try {
257
+ const startTime = Date.now();
258
+ await middleware(ctx, () => dispatch(i + 1));
259
+ const duration = Date.now() - startTime;
260
+
261
+ if (duration > 50) {
262
+ console.warn(`⚠️ Middleware ${i} выполнился за ${duration}ms`);
263
+ }
264
+ } catch (error) {
265
+ console.error(`❌ Ошибка в middleware ${i}:`, error.message);
266
+
267
+ // Выполняем error hook
268
+ await this.executeHooks('error', {
269
+ middleware: i,
270
+ error,
271
+ context: ctx
272
+ }, ctx);
273
+
274
+ throw error;
275
+ }
276
+ };
277
+
278
+ await dispatch(0);
279
+ this.stats.middlewareExecutions++;
280
+ return ctx;
281
+ }
282
+
283
+ /**
284
+ * Создает контекст для операции
285
+ * @param {string} operation - Тип операции
286
+ * @param {any} input - Входные данные
287
+ * @param {Object} options - Опции
288
+ * @returns {Object} Контекст
289
+ */
290
+ createContext(operation, input, options = {}) {
291
+ return {
292
+ operation,
293
+ input,
294
+ options,
295
+ startTime: Date.now(),
296
+ metadata: {
297
+ version: '1.0.0',
298
+ timestamp: new Date().toISOString(),
299
+ ...options.metadata
300
+ },
301
+ state: {},
302
+ result: null,
303
+ errors: [],
304
+ warnings: []
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Выполняет операцию с поддержкой плагинов
310
+ * @param {string} operation - Тип операции
311
+ * @param {any} input - Входные данные
312
+ * @param {Object} options - Опции
313
+ * @param {Function} coreFunction - Основная функция
314
+ * @returns {Promise<any>} Результат
315
+ */
316
+ async executeWithPlugins(operation, input, options, coreFunction) {
317
+ // Создаем контекст
318
+ const ctx = this.createContext(operation, input, options);
319
+
320
+ try {
321
+ // Выполняем before hooks
322
+ ctx.input = await this.executeHooks(`before:${operation}`, ctx.input, ctx);
323
+
324
+ // Выполняем middlewares
325
+ await this.executeMiddlewares(ctx);
326
+
327
+ // Выполняем основную функцию
328
+ ctx.result = await coreFunction(ctx.input, ctx.options);
329
+
330
+ // Выполняем after hooks
331
+ ctx.result = await this.executeHooks(`after:${operation}`, ctx.result, ctx);
332
+
333
+ // Записываем время выполнения
334
+ ctx.duration = Date.now() - ctx.startTime;
335
+
336
+ // Логируем успешное выполнение
337
+ console.log(`✅ Операция "${operation}" выполнена за ${ctx.duration}ms`);
338
+
339
+ return ctx.result;
340
+ } catch (error) {
341
+ // Записываем ошибку
342
+ ctx.errors.push(error);
343
+ ctx.duration = Date.now() - ctx.startTime;
344
+
345
+ // Выполняем error hooks
346
+ await this.executeHooks('error', {
347
+ operation,
348
+ error,
349
+ context: ctx
350
+ }, ctx);
351
+
352
+ console.error(`❌ Ошибка в операции "${operation}":`, error.message);
353
+ throw error;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Возвращает список всех зарегистрированных плагинов
359
+ * @returns {Array} Список плагинов
360
+ */
361
+ listPlugins() {
362
+ return Array.from(this.plugins.entries()).map(([name, plugin]) => ({
363
+ name,
364
+ pluginName: plugin.name,
365
+ version: plugin.version,
366
+ description: plugin.description || '',
367
+ enabled: plugin.enabled,
368
+ registeredAt: plugin.registeredAt,
369
+ hooks: Object.keys(plugin.hooks || {}).length,
370
+ middlewares: (plugin.middlewares || []).length
371
+ }));
372
+ }
373
+
374
+ /**
375
+ * Возвращает список всех hooks
376
+ * @returns {Object} Статистика hooks
377
+ */
378
+ listHooks() {
379
+ const result = {};
380
+
381
+ for (const [hookName, handlers] of this.hooks.entries()) {
382
+ result[hookName] = {
383
+ count: handlers.length,
384
+ handlers: handlers.map(h => ({
385
+ pluginName: h.pluginName,
386
+ registeredAt: h.registeredAt
387
+ }))
388
+ };
389
+ }
390
+
391
+ return result;
392
+ }
393
+
394
+ /**
395
+ * Включает/выключает плагин
396
+ * @param {string} pluginName - Имя плагина
397
+ * @param {boolean} enabled - Состояние
398
+ */
399
+ setPluginEnabled(pluginName, enabled) {
400
+ const plugin = this.plugins.get(pluginName);
401
+ if (!plugin) {
402
+ throw new Error(`Plugin "${pluginName}" не найден`);
403
+ }
404
+
405
+ plugin.enabled = enabled;
406
+ console.log(`🔧 Plugin "${pluginName}" ${enabled ? 'включен' : 'выключен'}`);
407
+ }
408
+
409
+ /**
410
+ * Удаляет плагин
411
+ * @param {string} pluginName - Имя плагина
412
+ */
413
+ removePlugin(pluginName) {
414
+ if (!this.plugins.has(pluginName)) {
415
+ throw new Error(`Plugin "${pluginName}" не найден`);
416
+ }
417
+
418
+ // Удаляем связанные hooks
419
+ for (const [hookName, handlers] of this.hooks.entries()) {
420
+ const filtered = handlers.filter(h => h.pluginName !== pluginName);
421
+ this.hooks.set(hookName, filtered);
422
+ }
423
+
424
+ // Удаляем связанные middlewares
425
+ this.middlewares = this.middlewares.filter(m => !m.name?.startsWith(`${pluginName}:`));
426
+
427
+ // Удаляем плагин
428
+ this.plugins.delete(pluginName);
429
+
430
+ console.log(`🗑️ Plugin "${pluginName}" удален`);
431
+ }
432
+
433
+ /**
434
+ * Возвращает статистику
435
+ * @returns {Object} Статистика
436
+ */
437
+ getStats() {
438
+ return {
439
+ ...this.stats,
440
+ plugins: this.plugins.size,
441
+ hooks: Array.from(this.hooks.values()).reduce((sum, handlers) => sum + handlers.length, 0),
442
+ middlewares: this.middlewares.length,
443
+ uniqueHooks: this.hooks.size
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Сбрасывает статистику
449
+ */
450
+ resetStats() {
451
+ this.stats = {
452
+ pluginLoads: 0,
453
+ hookExecutions: 0,
454
+ middlewareExecutions: 0
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Очищает все плагины и hooks
460
+ */
461
+ clear() {
462
+ this.plugins.clear();
463
+ this.hooks.clear();
464
+ this.middlewares = [];
465
+ this.resetStats();
466
+ this._registerDefaultHooks();
467
+
468
+ console.log('🧹 Все плагины и hooks очищены');
469
+ }
470
+ }
471
+
472
+ module.exports = PluginManager;