jtcsv 3.0.0 → 3.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 (258) hide show
  1. package/README.md +205 -146
  2. package/bin/jtcsv.ts +280 -202
  3. package/browser.d.ts +142 -0
  4. package/dist/benchmark.js +446 -0
  5. package/dist/benchmark.js.map +1 -0
  6. package/dist/bin/jtcsv.js +1940 -0
  7. package/dist/bin/jtcsv.js.map +1 -0
  8. package/dist/csv-to-json.js +1262 -0
  9. package/dist/csv-to-json.js.map +1 -0
  10. package/dist/errors.js +291 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/eslint.config.js +147 -0
  13. package/dist/eslint.config.js.map +1 -0
  14. package/dist/index-core.js +95 -0
  15. package/dist/index-core.js.map +1 -0
  16. package/dist/index.js +93 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json-save.js +229 -0
  19. package/dist/json-save.js.map +1 -0
  20. package/dist/json-to-csv.js +576 -0
  21. package/dist/json-to-csv.js.map +1 -0
  22. package/dist/jtcsv-core.cjs.js +336 -7
  23. package/dist/jtcsv-core.cjs.js.map +1 -1
  24. package/dist/jtcsv-core.esm.js +336 -7
  25. package/dist/jtcsv-core.esm.js.map +1 -1
  26. package/dist/jtcsv-core.umd.js +336 -7
  27. package/dist/jtcsv-core.umd.js.map +1 -1
  28. package/dist/jtcsv-full.cjs.js +336 -7
  29. package/dist/jtcsv-full.cjs.js.map +1 -1
  30. package/dist/jtcsv-full.esm.js +336 -7
  31. package/dist/jtcsv-full.esm.js.map +1 -1
  32. package/dist/jtcsv-full.umd.js +336 -7
  33. package/dist/jtcsv-full.umd.js.map +1 -1
  34. package/dist/jtcsv-workers.esm.js +9 -0
  35. package/dist/jtcsv-workers.esm.js.map +1 -1
  36. package/dist/jtcsv-workers.umd.js +9 -0
  37. package/dist/jtcsv-workers.umd.js.map +1 -1
  38. package/dist/jtcsv.cjs.js +1998 -2092
  39. package/dist/jtcsv.cjs.js.map +1 -1
  40. package/dist/jtcsv.esm.js +1994 -2092
  41. package/dist/jtcsv.esm.js.map +1 -1
  42. package/dist/jtcsv.umd.js +2157 -2251
  43. package/dist/jtcsv.umd.js.map +1 -1
  44. package/dist/plugins/express-middleware/index.js +350 -0
  45. package/dist/plugins/express-middleware/index.js.map +1 -0
  46. package/dist/plugins/fastify-plugin/index.js +315 -0
  47. package/dist/plugins/fastify-plugin/index.js.map +1 -0
  48. package/dist/plugins/hono/index.js +111 -0
  49. package/dist/plugins/hono/index.js.map +1 -0
  50. package/dist/plugins/nestjs/index.js +112 -0
  51. package/dist/plugins/nestjs/index.js.map +1 -0
  52. package/dist/plugins/nuxt/index.js +53 -0
  53. package/dist/plugins/nuxt/index.js.map +1 -0
  54. package/dist/plugins/remix/index.js +133 -0
  55. package/dist/plugins/remix/index.js.map +1 -0
  56. package/dist/plugins/sveltekit/index.js +155 -0
  57. package/dist/plugins/sveltekit/index.js.map +1 -0
  58. package/dist/plugins/trpc/index.js +136 -0
  59. package/dist/plugins/trpc/index.js.map +1 -0
  60. package/dist/run-demo.js +49 -0
  61. package/dist/run-demo.js.map +1 -0
  62. package/dist/src/browser/browser-functions.js +193 -0
  63. package/dist/src/browser/browser-functions.js.map +1 -0
  64. package/dist/src/browser/core.js +123 -0
  65. package/dist/src/browser/core.js.map +1 -0
  66. package/dist/src/browser/csv-to-json-browser.js +353 -0
  67. package/dist/src/browser/csv-to-json-browser.js.map +1 -0
  68. package/dist/src/browser/errors-browser.js +219 -0
  69. package/dist/src/browser/errors-browser.js.map +1 -0
  70. package/dist/src/browser/extensions/plugins.js +106 -0
  71. package/dist/src/browser/extensions/plugins.js.map +1 -0
  72. package/dist/src/browser/extensions/workers.js +66 -0
  73. package/dist/src/browser/extensions/workers.js.map +1 -0
  74. package/dist/src/browser/index.js +140 -0
  75. package/dist/src/browser/index.js.map +1 -0
  76. package/dist/src/browser/json-to-csv-browser.js +225 -0
  77. package/dist/src/browser/json-to-csv-browser.js.map +1 -0
  78. package/dist/src/browser/streams.js +340 -0
  79. package/dist/src/browser/streams.js.map +1 -0
  80. package/dist/src/browser/workers/csv-parser.worker.js +264 -0
  81. package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
  82. package/dist/src/browser/workers/worker-pool.js +338 -0
  83. package/dist/src/browser/workers/worker-pool.js.map +1 -0
  84. package/dist/src/core/delimiter-cache.js +196 -0
  85. package/dist/src/core/delimiter-cache.js.map +1 -0
  86. package/dist/src/core/node-optimizations.js +279 -0
  87. package/dist/src/core/node-optimizations.js.map +1 -0
  88. package/dist/src/core/plugin-system.js +399 -0
  89. package/dist/src/core/plugin-system.js.map +1 -0
  90. package/dist/src/core/transform-hooks.js +348 -0
  91. package/dist/src/core/transform-hooks.js.map +1 -0
  92. package/dist/src/engines/fast-path-engine-new.js +262 -0
  93. package/dist/src/engines/fast-path-engine-new.js.map +1 -0
  94. package/dist/src/engines/fast-path-engine.js +671 -0
  95. package/dist/src/engines/fast-path-engine.js.map +1 -0
  96. package/dist/src/errors.js +18 -0
  97. package/dist/src/errors.js.map +1 -0
  98. package/dist/src/formats/ndjson-parser.js +332 -0
  99. package/dist/src/formats/ndjson-parser.js.map +1 -0
  100. package/dist/src/formats/tsv-parser.js +230 -0
  101. package/dist/src/formats/tsv-parser.js.map +1 -0
  102. package/dist/src/index-with-plugins.js +259 -0
  103. package/dist/src/index-with-plugins.js.map +1 -0
  104. package/dist/src/types/index.js +3 -0
  105. package/dist/src/types/index.js.map +1 -0
  106. package/dist/src/utils/bom-utils.js +267 -0
  107. package/dist/src/utils/bom-utils.js.map +1 -0
  108. package/dist/src/utils/encoding-support.js +77 -0
  109. package/dist/src/utils/encoding-support.js.map +1 -0
  110. package/dist/src/utils/schema-validator.js +609 -0
  111. package/dist/src/utils/schema-validator.js.map +1 -0
  112. package/dist/src/utils/transform-loader.js +281 -0
  113. package/dist/src/utils/transform-loader.js.map +1 -0
  114. package/dist/src/utils/validators.js +40 -0
  115. package/dist/src/utils/validators.js.map +1 -0
  116. package/dist/src/utils/zod-adapter.js +144 -0
  117. package/dist/src/utils/zod-adapter.js.map +1 -0
  118. package/{src → dist/src}/web-server/index.js +251 -286
  119. package/dist/src/web-server/index.js.map +1 -0
  120. package/dist/src/workers/csv-multithreaded.js +211 -0
  121. package/dist/src/workers/csv-multithreaded.js.map +1 -0
  122. package/dist/src/workers/csv-parser.worker.js +179 -0
  123. package/dist/src/workers/csv-parser.worker.js.map +1 -0
  124. package/dist/src/workers/worker-pool.js +228 -0
  125. package/dist/src/workers/worker-pool.js.map +1 -0
  126. package/dist/stream-csv-to-json.js +665 -0
  127. package/dist/stream-csv-to-json.js.map +1 -0
  128. package/dist/stream-json-to-csv.js +389 -0
  129. package/dist/stream-json-to-csv.js.map +1 -0
  130. package/examples/advanced/conditional-transformations.ts +2 -2
  131. package/examples/advanced/performance-optimization.ts +2 -2
  132. package/examples/cli-advanced-usage.md +2 -0
  133. package/examples/cli-tool.ts +1 -1
  134. package/examples/large-dataset-example.ts +2 -2
  135. package/examples/simple-usage.ts +2 -2
  136. package/examples/streaming-example.ts +1 -1
  137. package/index.d.ts +186 -15
  138. package/package.json +43 -108
  139. package/plugins.d.ts +37 -0
  140. package/schema.d.ts +103 -0
  141. package/src/browser/csv-to-json-browser.ts +233 -3
  142. package/src/browser/errors-browser.ts +45 -28
  143. package/src/browser/json-to-csv-browser.ts +81 -5
  144. package/src/browser/streams.ts +73 -6
  145. package/src/core/delimiter-cache.ts +21 -11
  146. package/src/core/plugin-system.ts +343 -155
  147. package/src/core/transform-hooks.ts +20 -12
  148. package/src/engines/fast-path-engine.ts +48 -32
  149. package/src/errors.ts +1 -72
  150. package/src/formats/ndjson-parser.ts +6 -0
  151. package/src/formats/tsv-parser.ts +6 -0
  152. package/src/types/index.ts +21 -1
  153. package/src/utils/validators.ts +35 -0
  154. package/src/web-server/index.ts +1 -1
  155. package/bin/jtcsv.js +0 -2532
  156. package/csv-to-json.js +0 -711
  157. package/errors.js +0 -394
  158. package/examples/advanced/conditional-transformations.js +0 -446
  159. package/examples/advanced/csv-parser.worker.js +0 -89
  160. package/examples/advanced/nested-objects-example.js +0 -306
  161. package/examples/advanced/performance-optimization.js +0 -504
  162. package/examples/advanced/run-demo-server.js +0 -116
  163. package/examples/cli-batch-processing.js +0 -38
  164. package/examples/cli-tool.js +0 -183
  165. package/examples/error-handling.js +0 -338
  166. package/examples/express-api.js +0 -164
  167. package/examples/large-dataset-example.js +0 -182
  168. package/examples/ndjson-processing.js +0 -434
  169. package/examples/plugin-excel-exporter.js +0 -406
  170. package/examples/schema-validation.js +0 -640
  171. package/examples/simple-usage.js +0 -282
  172. package/examples/streaming-example.js +0 -418
  173. package/examples/web-workers-advanced.js +0 -28
  174. package/index.js +0 -82
  175. package/json-save.js +0 -255
  176. package/json-to-csv.js +0 -668
  177. package/plugins/README.md +0 -91
  178. package/plugins/express-middleware/README.md +0 -83
  179. package/plugins/express-middleware/example.js +0 -135
  180. package/plugins/express-middleware/example.ts +0 -135
  181. package/plugins/express-middleware/index.d.ts +0 -114
  182. package/plugins/express-middleware/index.js +0 -512
  183. package/plugins/express-middleware/index.ts +0 -557
  184. package/plugins/express-middleware/package.json +0 -52
  185. package/plugins/fastify-plugin/index.js +0 -404
  186. package/plugins/fastify-plugin/index.ts +0 -443
  187. package/plugins/fastify-plugin/package.json +0 -55
  188. package/plugins/hono/README.md +0 -28
  189. package/plugins/hono/index.d.ts +0 -12
  190. package/plugins/hono/index.js +0 -36
  191. package/plugins/hono/index.ts +0 -226
  192. package/plugins/hono/package.json +0 -35
  193. package/plugins/nestjs/README.md +0 -35
  194. package/plugins/nestjs/index.d.ts +0 -25
  195. package/plugins/nestjs/index.js +0 -77
  196. package/plugins/nestjs/index.ts +0 -201
  197. package/plugins/nestjs/package.json +0 -37
  198. package/plugins/nextjs-api/README.md +0 -57
  199. package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
  200. package/plugins/nextjs-api/examples/ConverterComponent.tsx +0 -386
  201. package/plugins/nextjs-api/examples/api-convert.js +0 -67
  202. package/plugins/nextjs-api/examples/api-convert.ts +0 -67
  203. package/plugins/nextjs-api/index.js +0 -387
  204. package/plugins/nextjs-api/index.tsx +0 -339
  205. package/plugins/nextjs-api/package.json +0 -63
  206. package/plugins/nextjs-api/route.js +0 -370
  207. package/plugins/nextjs-api/route.ts +0 -370
  208. package/plugins/nuxt/README.md +0 -24
  209. package/plugins/nuxt/index.js +0 -21
  210. package/plugins/nuxt/index.ts +0 -94
  211. package/plugins/nuxt/package.json +0 -35
  212. package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
  213. package/plugins/nuxt/runtime/composables/useJtcsv.ts +0 -100
  214. package/plugins/nuxt/runtime/plugin.js +0 -6
  215. package/plugins/nuxt/runtime/plugin.ts +0 -71
  216. package/plugins/remix/README.md +0 -26
  217. package/plugins/remix/index.d.ts +0 -16
  218. package/plugins/remix/index.js +0 -62
  219. package/plugins/remix/index.ts +0 -260
  220. package/plugins/remix/package.json +0 -35
  221. package/plugins/sveltekit/README.md +0 -28
  222. package/plugins/sveltekit/index.d.ts +0 -17
  223. package/plugins/sveltekit/index.js +0 -54
  224. package/plugins/sveltekit/index.ts +0 -301
  225. package/plugins/sveltekit/package.json +0 -33
  226. package/plugins/trpc/README.md +0 -25
  227. package/plugins/trpc/index.d.ts +0 -7
  228. package/plugins/trpc/index.js +0 -32
  229. package/plugins/trpc/index.ts +0 -267
  230. package/plugins/trpc/package.json +0 -34
  231. package/src/browser/browser-functions.js +0 -219
  232. package/src/browser/core.js +0 -92
  233. package/src/browser/csv-to-json-browser.js +0 -722
  234. package/src/browser/errors-browser.js +0 -212
  235. package/src/browser/extensions/plugins.js +0 -92
  236. package/src/browser/extensions/workers.js +0 -39
  237. package/src/browser/index.js +0 -113
  238. package/src/browser/json-to-csv-browser.js +0 -319
  239. package/src/browser/streams.js +0 -403
  240. package/src/browser/workers/csv-parser.worker.js +0 -377
  241. package/src/browser/workers/worker-pool.js +0 -527
  242. package/src/core/delimiter-cache.js +0 -200
  243. package/src/core/node-optimizations.js +0 -408
  244. package/src/core/plugin-system.js +0 -494
  245. package/src/core/transform-hooks.js +0 -350
  246. package/src/engines/fast-path-engine-new.js +0 -338
  247. package/src/engines/fast-path-engine.js +0 -844
  248. package/src/errors.js +0 -26
  249. package/src/formats/ndjson-parser.js +0 -467
  250. package/src/formats/tsv-parser.js +0 -339
  251. package/src/index-with-plugins.js +0 -378
  252. package/src/utils/bom-utils.js +0 -259
  253. package/src/utils/encoding-support.js +0 -124
  254. package/src/utils/schema-validator.js +0 -594
  255. package/src/utils/transform-loader.js +0 -205
  256. package/src/utils/zod-adapter.js +0 -170
  257. package/stream-csv-to-json.js +0 -560
  258. package/stream-json-to-csv.js +0 -465
@@ -6,15 +6,22 @@
6
6
  * @date 2026-01-22
7
7
  */
8
8
 
9
- interface PluginStats {
9
+ interface PluginStatsCounters {
10
10
  pluginLoads: number;
11
11
  hookExecutions: number;
12
12
  middlewareExecutions: number;
13
13
  }
14
14
 
15
+ interface PluginStats extends PluginStatsCounters {
16
+ plugins: number;
17
+ hooks: number;
18
+ middlewares: number;
19
+ uniqueHooks: number;
20
+ }
21
+
15
22
  interface Plugin {
16
23
  name: string;
17
- version?: string;
24
+ version: string;
18
25
  description?: string;
19
26
  hooks?: Record<string, Function>;
20
27
  middlewares?: Function[];
@@ -22,6 +29,23 @@ interface Plugin {
22
29
  destroy?: () => void;
23
30
  }
24
31
 
32
+ interface PluginRecord extends Plugin {
33
+ id: string;
34
+ enabled: boolean;
35
+ }
36
+
37
+ interface HookHandlerEntry {
38
+ handler: Function;
39
+ pluginName?: string;
40
+ executionCount: number;
41
+ }
42
+
43
+ interface MiddlewareEntry {
44
+ handler: Function;
45
+ pluginName?: string;
46
+ executionCount: number;
47
+ }
48
+
25
49
  type HookName =
26
50
  | 'before:csvToJson'
27
51
  | 'after:csvToJson'
@@ -36,14 +60,16 @@ type HookName =
36
60
  | 'transformation'
37
61
  | string;
38
62
 
39
- export class PluginManager {
40
- private plugins: Map<string, Plugin>;
41
- private hooks: Map<HookName, Function[]>;
42
- private middlewares: Function[];
63
+ const SLOW_HOOK_THRESHOLD_MS = 100;
64
+
65
+ export class PluginManager {
66
+ private plugins: Map<string, PluginRecord>;
67
+ private hooks: Map<HookName, HookHandlerEntry[]>;
68
+ private middlewares: MiddlewareEntry[];
43
69
  private context: Record<string, any>;
44
- private stats: PluginStats;
70
+ private stats: PluginStatsCounters;
45
71
 
46
- constructor() {
72
+ constructor() {
47
73
  this.plugins = new Map();
48
74
  this.hooks = new Map();
49
75
  this.middlewares = [];
@@ -56,14 +82,14 @@ export class PluginManager {
56
82
 
57
83
  // Регистрируем стандартные hooks
58
84
  this._registerDefaultHooks();
59
- }
60
-
61
- /**
62
- * Backwards-compatible alias for registerPlugin.
63
- */
64
- use(name: string, plugin: Plugin): void {
65
- this.registerPlugin(name, plugin);
66
- }
85
+ }
86
+
87
+ /**
88
+ * Backwards-compatible alias for registerPlugin.
89
+ */
90
+ use(name: string, plugin: Plugin): void {
91
+ this.registerPlugin(name, plugin);
92
+ }
67
93
 
68
94
  /**
69
95
  * Регистрирует стандартные hooks
@@ -94,176 +120,333 @@ export class PluginManager {
94
120
  * @param plugin - Объект плагина
95
121
  */
96
122
  registerPlugin(name: string, plugin: Plugin): void {
123
+ if (!plugin || typeof plugin !== 'object' || !plugin.name || !plugin.version) {
124
+ throw new Error('Plugin должен иметь name и version');
125
+ }
126
+
97
127
  if (this.plugins.has(name)) {
98
- throw new Error(`Plugin "${name}" already registered`);
128
+ throw new Error(`Plugin "${name}" уже зарегистрирован`);
129
+ }
130
+
131
+ if (plugin.hooks && (typeof plugin.hooks !== 'object' || Array.isArray(plugin.hooks))) {
132
+ throw new Error('hooks must be an object');
99
133
  }
100
134
 
101
- // Добавляем плагин
102
- this.plugins.set(name, plugin);
135
+ if (plugin.hooks) {
136
+ for (const [hookName, handler] of Object.entries(plugin.hooks)) {
137
+ if (typeof handler !== 'function') {
138
+ throw new Error(`Hook handler for "${hookName}" must be a function`);
139
+ }
140
+ }
141
+ }
142
+
143
+ if (plugin.middlewares && !Array.isArray(plugin.middlewares)) {
144
+ throw new Error('middlewares must be an array');
145
+ }
146
+
147
+ if (plugin.middlewares) {
148
+ plugin.middlewares.forEach((middleware, index) => {
149
+ if (typeof middleware !== 'function') {
150
+ throw new Error(`Middleware ${index} must be a function`);
151
+ }
152
+ });
153
+ }
154
+
155
+ const record: PluginRecord = {
156
+ id: name,
157
+ enabled: true,
158
+ ...plugin
159
+ };
160
+
161
+ // ?????????????????? ????????????
162
+ this.plugins.set(name, record);
103
163
  this.stats.pluginLoads++;
104
164
 
105
- // Регистрируем hooks плагина
165
+ // ?????????????????????? hooks ??????????????
106
166
  if (plugin.hooks) {
107
167
  Object.entries(plugin.hooks).forEach(([hookName, handler]) => {
108
- this.registerHook(hookName as HookName, handler);
168
+ this.registerHook(hookName as HookName, handler, name);
109
169
  });
110
170
  }
111
171
 
112
- // Регистрируем middleware плагина
172
+ // ?????????????????????? middleware ??????????????
113
173
  if (plugin.middlewares) {
114
174
  plugin.middlewares.forEach(middleware => {
115
- this.registerMiddleware(middleware);
175
+ this.registerMiddleware(middleware, name);
116
176
  });
117
177
  }
118
178
 
119
- // Вызываем init если есть
179
+ // ???????????????? init ???????? ????????
120
180
  if (plugin.init) {
121
181
  plugin.init(this);
122
182
  }
123
-
124
- console.log(`✅ Plugin "${name}" registered successfully`);
125
183
  }
126
184
 
127
- /**
128
- * Регистрирует hook
129
- * @param hookName - Имя hook
130
- * @param handler - Функция обработчик
131
- */
132
- registerHook(hookName: HookName, handler: Function): void {
185
+ registerHook(hookName: HookName, handler: Function, pluginName?: string): void {
186
+ if (typeof handler != 'function') {
187
+ throw new Error('Hook handler must be a function');
188
+ }
133
189
  if (!this.hooks.has(hookName)) {
134
190
  this.hooks.set(hookName, []);
135
191
  }
136
192
 
137
193
  const handlers = this.hooks.get(hookName)!;
138
- handlers.push(handler);
194
+ handlers.push({
195
+ handler,
196
+ pluginName,
197
+ executionCount: 0
198
+ });
139
199
  }
140
200
 
141
- /**
142
- * Регистрирует middleware
143
- * @param middleware - Функция middleware
144
- */
145
- registerMiddleware(middleware: Function): void {
146
- this.middlewares.push(middleware);
201
+ registerMiddleware(middleware: Function, pluginName?: string): void {
202
+ if (typeof middleware !== 'function') {
203
+ throw new Error('Middleware must be a function');
204
+ }
205
+ this.middlewares.push({
206
+ handler: middleware,
207
+ pluginName,
208
+ executionCount: 0
209
+ });
147
210
  }
148
211
 
149
- /**
150
- * Выполняет hook
151
- * @param hookName - Имя hook
152
- * @param data - Данные для обработки
153
- * @param context - Контекст выполнения
154
- */
155
- async executeHook(hookName: HookName, data: any, context: any = {}): Promise<any> {
212
+ private _isPluginEnabled(pluginName?: string): boolean {
213
+ if (!pluginName) {
214
+ return true;
215
+ }
216
+ const plugin = this.plugins.get(pluginName);
217
+ if (!plugin) {
218
+ return true;
219
+ }
220
+ return plugin.enabled !== false;
221
+ }
222
+
223
+ private _runErrorHooks(error: any, context: any): void {
224
+ const errorHandlers = this.hooks.get('error');
225
+ if (!errorHandlers || errorHandlers.length == 0) {
226
+ return;
227
+ }
228
+ for (const handlerEntry of errorHandlers) {
229
+ try {
230
+ handlerEntry.handler(error, context);
231
+ } catch {
232
+ // ignore errors in error handlers
233
+ }
234
+ }
235
+ }
236
+
237
+ async executeHook(hookName: HookName, data: any, context: any = {}): Promise<any> {
156
238
  const handlers = this.hooks.get(hookName);
157
-
239
+
158
240
  if (!handlers || handlers.length === 0) {
159
241
  return data;
160
242
  }
161
243
 
162
- this.stats.hookExecutions++;
163
244
  let result = data;
245
+ let executed = false;
164
246
 
165
- // Выполняем все handlers последовательно
166
- for (const handler of handlers) {
247
+ for (const handlerEntry of handlers) {
248
+ if (!this._isPluginEnabled(handlerEntry.pluginName)) {
249
+ continue;
250
+ }
251
+
252
+ executed = true;
253
+ const startTime = Date.now();
167
254
  try {
168
- result = await handler(result, { ...this.context, ...context, hookName });
255
+ result = await handlerEntry.handler(result, { ...this.context, ...context, hookName, plugin: handlerEntry.pluginName });
256
+ handlerEntry.executionCount++;
169
257
  } catch (error) {
170
- console.error(`Error in hook "${hookName}":`, error);
171
-
172
- // Выполняем error hook если есть
173
- const errorHandlers = this.hooks.get('error');
174
- if (errorHandlers && errorHandlers.length > 0) {
175
- for (const errorHandler of errorHandlers) {
176
- try {
177
- errorHandler(error, { ...this.context, ...context, hookName, data: result });
178
- } catch {
179
- // Игнорируем ошибки в error handlers
180
- }
181
- }
258
+ this._runErrorHooks(error, { ...this.context, ...context, hookName, data: result });
259
+ } finally {
260
+ const duration = Date.now() - startTime;
261
+ if (duration > SLOW_HOOK_THRESHOLD_MS) {
262
+ console.warn(`Slow hook "${hookName}" detected (${duration}ms)`);
182
263
  }
183
264
  }
184
265
  }
185
266
 
267
+ if (executed) {
268
+ this.stats.hookExecutions++;
269
+ }
270
+
186
271
  return result;
187
- }
188
-
189
- /**
190
- * Backwards-compatible alias for executeHook.
191
- */
192
- async executeHooks(hookName: HookName, data: any, context: any = {}): Promise<any> {
193
- return this.executeHook(hookName, data, context);
194
- }
195
-
196
- /**
197
- * Executes an operation with before/after hooks and middleware.
198
- */
199
- async executeWithPlugins(
200
- operation: string,
201
- input: any,
202
- options: any,
203
- handler: (input: any, options: any) => any | Promise<any>
204
- ): Promise<any> {
205
- const context = { operation, options, metadata: {} as Record<string, any> };
206
- const beforeHook = `before:${operation}` as HookName;
207
- const afterHook = `after:${operation}` as HookName;
208
-
209
- const beforeInput = await this.executeHook(beforeHook, input, context);
210
- const middlewareInput = { input: beforeInput, options, operation, metadata: context.metadata };
211
- const middlewareResult = await this.executeMiddleware(middlewareInput, context);
212
- const result = await handler(middlewareResult.input ?? beforeInput, options);
213
- return this.executeHook(afterHook, result, context);
214
- }
215
-
216
- /**
217
- * Returns registered plugin names.
218
- */
219
- listPlugins(): string[] {
220
- return Array.from(this.plugins.keys());
221
- }
272
+ }
222
273
 
223
274
  /**
224
- * Выполняет цепочку middleware
225
- * @param input - Входные данные
226
- * @param context - Контекст выполнения
275
+ * Backwards-compatible alias for executeHook.
227
276
  */
228
- async executeMiddleware(input: any, context: any = {}): Promise<any> {
229
- if (this.middlewares.length === 0) {
230
- return input;
277
+ async executeHooks(hookName: HookName, data: any, context: any = {}): Promise<any> {
278
+ return this.executeHook(hookName, data, context);
279
+ }
280
+
281
+ /**
282
+ * Executes an operation with before/after hooks and middleware.
283
+ */
284
+ async executeWithPlugins(
285
+ operation: string,
286
+ input: any,
287
+ options: any,
288
+ handler: (input: any, options: any) => any | Promise<any>
289
+ ): Promise<any> {
290
+ const metadata = options && options.metadata ? options.metadata : {};
291
+ const context = { operation, options, metadata };
292
+ const beforeHook = `before:${operation}` as HookName;
293
+ const afterHook = `after:${operation}` as HookName;
294
+
295
+ const beforeInput = await this.executeHook(beforeHook, input, context);
296
+ const middlewareContext: { input: any; options: any; operation: string; metadata: any; result?: any } = {
297
+ input: beforeInput,
298
+ options,
299
+ operation,
300
+ metadata
301
+ };
302
+ const resultHolder = { set: false, value: undefined as any };
303
+
304
+ try {
305
+ await this.executeMiddlewares(middlewareContext, context, async (ctx: any) => {
306
+ const handlerInput = ctx && Object.prototype.hasOwnProperty.call(ctx, 'input')
307
+ ? ctx.input
308
+ : beforeInput;
309
+ const result = await handler(handlerInput, options);
310
+ ctx.result = result;
311
+ resultHolder.set = true;
312
+ resultHolder.value = result;
313
+ return result;
314
+ });
315
+ } catch (error) {
316
+ this._runErrorHooks(error, { ...this.context, ...context, data: beforeInput });
317
+ throw error;
318
+ }
319
+
320
+ const finalResult = resultHolder.set
321
+ ? resultHolder.value
322
+ : (Object.prototype.hasOwnProperty.call(middlewareContext, 'result') ? middlewareContext.result : undefined);
323
+
324
+ return this.executeHook(afterHook, finalResult, context);
325
+ }
326
+
327
+ /**
328
+ * Returns registered plugin names.
329
+ */
330
+ listPlugins(): Array<{ id: string; pluginName: string; version: string; description?: string; enabled: boolean }> {
331
+ return Array.from(this.plugins.values()).map((plugin) => ({
332
+ id: plugin.id,
333
+ pluginName: plugin.name,
334
+ version: plugin.version,
335
+ description: plugin.description,
336
+ enabled: plugin.enabled
337
+ }));
338
+ }
339
+
340
+ listHooks(): Record<string, { count: number; handlers: Array<{ handler: Function; pluginName?: string; executionCount: number }> }> {
341
+ const result: Record<string, { count: number; handlers: Array<{ handler: Function; pluginName?: string; executionCount: number }> }> = {};
342
+ for (const [hookName, handlers] of this.hooks.entries()) {
343
+ result[hookName] = {
344
+ count: handlers.length,
345
+ handlers: handlers.map((handlerEntry) => ({
346
+ handler: handlerEntry.handler,
347
+ pluginName: handlerEntry.pluginName,
348
+ executionCount: handlerEntry.executionCount
349
+ }))
350
+ };
351
+ }
352
+ return result;
353
+ }
354
+
355
+
356
+ setPluginEnabled(name: string, enabled: boolean): void {
357
+ const plugin = this.plugins.get(name);
358
+ if (!plugin) {
359
+ throw new Error(`Plugin "${name}" не найден`);
231
360
  }
361
+ plugin.enabled = Boolean(enabled);
362
+ }
232
363
 
233
- this.stats.middlewareExecutions++;
234
- let result = input;
364
+ removePlugin(name: string): boolean {
365
+ const plugin = this.plugins.get(name);
366
+ if (!plugin) {
367
+ throw new Error(`Plugin "${name}" не найден`);
368
+ }
235
369
 
236
- // Выполняем middleware последовательно
237
- for (const middleware of this.middlewares) {
370
+ if (plugin.destroy) {
238
371
  try {
239
- result = await middleware(result, { ...this.context, ...context });
372
+ plugin.destroy();
240
373
  } catch (error) {
241
- console.error('Error in middleware:', error);
242
-
243
- // Выполняем error hook если есть
244
- const errorHandlers = this.hooks.get('error');
245
- if (errorHandlers && errorHandlers.length > 0) {
246
- for (const errorHandler of errorHandlers) {
247
- try {
248
- errorHandler(error, { ...this.context, ...context, data: result });
249
- } catch {
250
- // Игнорируем ошибки в error handlers
251
- }
252
- }
374
+ console.error(`Error destroying plugin "${name}":`, error);
375
+ }
376
+ }
377
+
378
+ this.plugins.delete(name);
379
+
380
+ for (const [hookName, handlers] of this.hooks.entries()) {
381
+ if (!handlers.length) {
382
+ continue;
383
+ }
384
+ const remaining = handlers.filter((handlerEntry) => handlerEntry.pluginName !== name);
385
+ this.hooks.set(hookName, remaining);
386
+ }
387
+
388
+ this.middlewares = this.middlewares.filter((middleware) => middleware.pluginName !== name);
389
+
390
+ return true;
391
+ }
392
+
393
+ resetStats(): void {
394
+ this.stats.pluginLoads = 0;
395
+ this.stats.hookExecutions = 0;
396
+ this.stats.middlewareExecutions = 0;
397
+ }
398
+
399
+ async executeMiddlewares(ctx: any, context: any = {}, finalHandler?: (ctx: any) => any | Promise<any>): Promise<any> {
400
+ const entries = this.middlewares.filter((entry) => this._isPluginEnabled(entry.pluginName));
401
+ if (entries.length === 0) {
402
+ if (finalHandler) {
403
+ await finalHandler(ctx);
404
+ }
405
+ return ctx;
406
+ }
407
+
408
+ let index = -1;
409
+ const dispatch = async (i: number): Promise<any> => {
410
+ if (i <= index) {
411
+ throw new Error('next() вызван несколько раз');
412
+ }
413
+ index = i;
414
+ const entry = entries[i];
415
+ if (!entry) {
416
+ if (finalHandler) {
417
+ return finalHandler(ctx);
253
418
  }
254
-
419
+ return;
420
+ }
421
+
422
+ const startTime = Date.now();
423
+ try {
424
+ const result = entry.handler(ctx, () => dispatch(i + 1));
425
+ await result;
426
+ entry.executionCount++;
427
+ this.stats.middlewareExecutions++;
428
+ } catch (error) {
429
+ this._runErrorHooks(error, { ...this.context, ...context, data: ctx });
255
430
  throw error;
431
+ } finally {
432
+ const duration = Date.now() - startTime;
433
+ if (duration > SLOW_HOOK_THRESHOLD_MS) {
434
+ console.warn(`Slow middleware "${entry.pluginName || 'anonymous'}" detected (${duration}ms)`);
435
+ }
256
436
  }
257
- }
437
+ };
258
438
 
259
- return result;
439
+ await dispatch(0);
440
+ return ctx;
260
441
  }
261
442
 
262
443
  /**
263
- * Устанавливает контекст
264
- * @param key - Ключ контекста
265
- * @param value - Значение
444
+ * Backwards-compatible alias for executeMiddlewares.
266
445
  */
446
+ async executeMiddleware(input: any, context: any = {}): Promise<any> {
447
+ return this.executeMiddlewares(input, context);
448
+ }
449
+
267
450
  setContext(key: string, value: any): void {
268
451
  this.context[key] = value;
269
452
  }
@@ -283,7 +466,18 @@ export class PluginManager {
283
466
  * Возвращает статистику
284
467
  */
285
468
  getStats(): PluginStats {
286
- return { ...this.stats };
469
+ let hookCount = 0;
470
+ for (const handlers of this.hooks.values()) {
471
+ hookCount += handlers.length;
472
+ }
473
+
474
+ return {
475
+ ...this.stats,
476
+ plugins: this.plugins.size,
477
+ hooks: hookCount,
478
+ middlewares: this.middlewares.length,
479
+ uniqueHooks: this.hooks.size
480
+ };
287
481
  }
288
482
 
289
483
  /**
@@ -305,28 +499,12 @@ export class PluginManager {
305
499
  * @param name - Имя плагина
306
500
  */
307
501
  unregisterPlugin(name: string): boolean {
308
- const plugin = this.plugins.get(name);
309
-
310
- if (!plugin) {
502
+ try {
503
+ this.removePlugin(name);
504
+ return true;
505
+ } catch {
311
506
  return false;
312
507
  }
313
-
314
- // Вызываем destroy если есть
315
- if (plugin.destroy) {
316
- try {
317
- plugin.destroy();
318
- } catch (error) {
319
- console.error(`Error destroying plugin "${name}":`, error);
320
- }
321
- }
322
-
323
- // Удаляем плагин
324
- this.plugins.delete(name);
325
-
326
- // TODO: Удалить связанные hooks и middleware
327
-
328
- console.log(`🗑️ Plugin "${name}" unregistered`);
329
- return true;
330
508
  }
331
509
 
332
510
  /**
@@ -366,7 +544,7 @@ export class PluginManager {
366
544
  * Асинхронная версия executeMiddleware
367
545
  */
368
546
  async executeMiddlewareAsync(input: any, context: any = {}): Promise<any> {
369
- return this.executeMiddleware(input, context);
547
+ return this.executeMiddlewares(input, context);
370
548
  }
371
549
  }
372
550
 
@@ -390,11 +568,21 @@ export async function getGlobalPluginManagerAsync(): Promise<PluginManager> {
390
568
  return getGlobalPluginManager();
391
569
  }
392
570
 
571
+ export default PluginManager;
572
+
393
573
  // Экспорт для CommonJS
394
574
  if (typeof module !== 'undefined' && module.exports) {
395
- module.exports = {
396
- PluginManager,
397
- getGlobalPluginManager,
398
- getGlobalPluginManagerAsync
399
- };
400
- }
575
+ const current = module.exports;
576
+ if (current && current.__esModule) {
577
+ current.PluginManager = PluginManager;
578
+ current.getGlobalPluginManager = getGlobalPluginManager;
579
+ current.getGlobalPluginManagerAsync = getGlobalPluginManagerAsync;
580
+ current.default = PluginManager;
581
+ } else {
582
+ module.exports = PluginManager;
583
+ module.exports.PluginManager = PluginManager;
584
+ module.exports.getGlobalPluginManager = getGlobalPluginManager;
585
+ module.exports.getGlobalPluginManagerAsync = getGlobalPluginManagerAsync;
586
+ module.exports.default = PluginManager;
587
+ }
588
+ }
@@ -445,11 +445,11 @@ export const predefinedHooks = {
445
445
  * @param onError - Обработчик ошибки
446
446
  * @returns Хук валидации
447
447
  */
448
- validate<T>(validator: (item: T, index: number) => boolean, onError: (errors: any[]) => void = console.error): HookFunction<T[], T[]> {
449
- return (data: T[]) => {
450
- if (Array.isArray(data)) {
451
- const validData: T[] = [];
452
- const errors: any[] = [];
448
+ validate<T>(validator: (item: T, index: number) => boolean, onError: (errors: any[] | string, details?: any[]) => void = console.error): HookFunction<T[], T[]> {
449
+ return (data: T[]) => {
450
+ if (Array.isArray(data)) {
451
+ const validData: T[] = [];
452
+ const errors: any[] = [];
453
453
 
454
454
  data.forEach((item, index) => {
455
455
  try {
@@ -464,9 +464,13 @@ export const predefinedHooks = {
464
464
  }
465
465
  });
466
466
 
467
- if (errors.length > 0) {
468
- onError(errors);
469
- }
467
+ if (errors.length > 0) {
468
+ if (onError.length >= 2) {
469
+ onError('Validation errors', errors);
470
+ } else {
471
+ onError(errors);
472
+ }
473
+ }
470
474
 
471
475
  return validData;
472
476
  }
@@ -480,7 +484,7 @@ export const predefinedHooks = {
480
484
  * @param onError - Обработчик ошибки
481
485
  * @returns Асинхронный хук валидации
482
486
  */
483
- validateAsync<T>(validator: (item: T, index: number) => Promise<boolean>, onError: (errors: any[]) => void = console.error): AsyncHookFunction<T[], T[]> {
487
+ validateAsync<T>(validator: (item: T, index: number) => Promise<boolean>, onError: (errors: any[] | string, details?: any[]) => void = console.error): AsyncHookFunction<T[], T[]> {
484
488
  return async (data: T[]) => {
485
489
  if (Array.isArray(data)) {
486
490
  const validData: T[] = [];
@@ -499,9 +503,13 @@ export const predefinedHooks = {
499
503
  }
500
504
  }
501
505
 
502
- if (errors.length > 0) {
503
- onError(errors);
504
- }
506
+ if (errors.length > 0) {
507
+ if (onError.length >= 2) {
508
+ onError('Validation errors', errors);
509
+ } else {
510
+ onError(errors);
511
+ }
512
+ }
505
513
 
506
514
  return validData;
507
515
  }