@vizzly-testing/cli 0.9.1 → 0.10.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 +77 -20
- package/dist/cli.js +43 -0
- package/dist/client/index.js +0 -2
- package/dist/commands/init.js +1 -6
- package/dist/plugin-loader.js +183 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +13 -13
- package/dist/types/client/index.d.ts +0 -2
- package/dist/types/plugin-loader.d.ts +8 -0
- package/dist/types/utils/config-loader.d.ts +1 -1
- package/dist/types/utils/config-schema.d.ts +217 -0
- package/dist/utils/config-loader.js +23 -12
- package/dist/utils/config-schema.js +134 -0
- package/docs/api-reference.md +1 -2
- package/docs/plugins.md +496 -0
- package/package.json +4 -3
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# Vizzly Plugin System
|
|
2
|
+
|
|
3
|
+
The Vizzly CLI supports a powerful plugin system that allows you to extend its functionality with
|
|
4
|
+
custom commands. This enables community contributions and specialized integrations while keeping the
|
|
5
|
+
core CLI lean and focused.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Plugins are JavaScript modules that export a simple registration function. The CLI automatically
|
|
10
|
+
discovers plugins from `node_modules/@vizzly-testing/*` or loads them explicitly from your config
|
|
11
|
+
file.
|
|
12
|
+
|
|
13
|
+
## Benefits
|
|
14
|
+
|
|
15
|
+
- **Zero Configuration** - Just `npm install` and the plugin is available
|
|
16
|
+
- **Shared Infrastructure** - Plugins get access to config, logger, and services
|
|
17
|
+
- **Independent Releases** - Plugins can iterate without requiring CLI updates
|
|
18
|
+
- **Smaller Core** - Keep the main CLI lean by moving optional features to plugins
|
|
19
|
+
- **Community Extensible** - Anyone can build and share plugins
|
|
20
|
+
|
|
21
|
+
## Using Plugins
|
|
22
|
+
|
|
23
|
+
### Official Plugins
|
|
24
|
+
|
|
25
|
+
Official Vizzly plugins are published under the `@vizzly-testing/*` scope and are automatically
|
|
26
|
+
discovered:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install plugin
|
|
30
|
+
npm install @vizzly-testing/storybook
|
|
31
|
+
|
|
32
|
+
# Use immediately - no configuration needed!
|
|
33
|
+
vizzly storybook ./storybook-static
|
|
34
|
+
|
|
35
|
+
# Plugin commands appear in help
|
|
36
|
+
vizzly --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Community Plugins
|
|
40
|
+
|
|
41
|
+
You can use community plugins or your own local plugins by configuring them explicitly:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
// vizzly.config.js
|
|
45
|
+
export default {
|
|
46
|
+
plugins: [
|
|
47
|
+
'./my-custom-plugin.js', // Local file path
|
|
48
|
+
'npm-package-name', // npm package name
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This is useful for:
|
|
54
|
+
- Local plugin development and testing
|
|
55
|
+
- Private/internal company plugins
|
|
56
|
+
- Community plugins not in the `@vizzly-testing` scope
|
|
57
|
+
- Custom workflow automation
|
|
58
|
+
|
|
59
|
+
## Creating a Plugin
|
|
60
|
+
|
|
61
|
+
### Basic Plugin Structure
|
|
62
|
+
|
|
63
|
+
A plugin is a JavaScript module that exports an object with `name` and a `register` function:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// my-plugin.js
|
|
67
|
+
export default {
|
|
68
|
+
name: 'my-plugin',
|
|
69
|
+
version: '1.0.0', // Optional but recommended
|
|
70
|
+
|
|
71
|
+
register(program, { config, logger, services }) {
|
|
72
|
+
// Register your command with Commander.js
|
|
73
|
+
program
|
|
74
|
+
.command('my-command <arg>')
|
|
75
|
+
.description('Description of my command')
|
|
76
|
+
.option('--option <value>', 'An option')
|
|
77
|
+
.action(async (arg, options) => {
|
|
78
|
+
logger.info(`Running my-command with ${arg}`);
|
|
79
|
+
|
|
80
|
+
// Access shared services if needed
|
|
81
|
+
let apiService = await services.get('apiService');
|
|
82
|
+
|
|
83
|
+
// Your command logic here
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Plugin Interface
|
|
90
|
+
|
|
91
|
+
#### Required Fields
|
|
92
|
+
|
|
93
|
+
- **`name`** (string) - Unique identifier for your plugin
|
|
94
|
+
- **`register`** (function) - Called during CLI initialization to register commands
|
|
95
|
+
|
|
96
|
+
#### Optional Fields
|
|
97
|
+
|
|
98
|
+
- **`version`** (string) - Plugin version (recommended for debugging and compatibility)
|
|
99
|
+
|
|
100
|
+
#### Register Function Parameters
|
|
101
|
+
|
|
102
|
+
The `register` function receives two arguments:
|
|
103
|
+
|
|
104
|
+
1. **`program`** - [Commander.js](https://github.com/tj/commander.js) program instance for registering commands
|
|
105
|
+
2. **`context`** - Object containing:
|
|
106
|
+
- `config` - Merged Vizzly configuration object
|
|
107
|
+
- `logger` - Shared logger instance with `.debug()`, `.info()`, `.warn()`, `.error()` methods
|
|
108
|
+
- `services` - Service container with access to internal Vizzly services
|
|
109
|
+
|
|
110
|
+
### Available Services
|
|
111
|
+
|
|
112
|
+
Plugins can access these services from the container:
|
|
113
|
+
|
|
114
|
+
- **`logger`** - Component logger for consistent output
|
|
115
|
+
- **`apiService`** - Vizzly API client for interacting with the platform
|
|
116
|
+
- **`uploader`** - Screenshot upload service
|
|
117
|
+
- **`buildManager`** - Build lifecycle management
|
|
118
|
+
- **`serverManager`** - Screenshot server management
|
|
119
|
+
- **`tddService`** - TDD mode services
|
|
120
|
+
- **`testRunner`** - Test execution service
|
|
121
|
+
|
|
122
|
+
Example accessing a service:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
register(program, { config, logger, services }) {
|
|
126
|
+
program
|
|
127
|
+
.command('upload-screenshots <dir>')
|
|
128
|
+
.action(async (dir) => {
|
|
129
|
+
let uploader = await services.get('uploader');
|
|
130
|
+
await uploader.uploadScreenshots(screenshots);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Local Plugin Development
|
|
136
|
+
|
|
137
|
+
### Setup
|
|
138
|
+
|
|
139
|
+
1. Create your plugin file:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
mkdir -p plugins
|
|
143
|
+
touch plugins/my-plugin.js
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. Write your plugin:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// plugins/my-plugin.js
|
|
150
|
+
export default {
|
|
151
|
+
name: 'my-plugin',
|
|
152
|
+
version: '1.0.0',
|
|
153
|
+
register(program, { config, logger }) {
|
|
154
|
+
program
|
|
155
|
+
.command('greet <name>')
|
|
156
|
+
.description('Greet someone')
|
|
157
|
+
.action((name) => {
|
|
158
|
+
logger.info(`Hello, ${name}!`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
3. Configure Vizzly to load your plugin:
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// vizzly.config.js
|
|
168
|
+
export default {
|
|
169
|
+
plugins: ['./plugins/my-plugin.js'],
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
4. Test your plugin:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
vizzly --help # See your command listed
|
|
177
|
+
vizzly greet World # Test your command
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Package Structure (For Distribution)
|
|
181
|
+
|
|
182
|
+
If you want to share your plugin via npm:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
my-vizzly-plugin/
|
|
186
|
+
├── package.json
|
|
187
|
+
├── plugin.js # Main plugin file
|
|
188
|
+
├── lib/ # Plugin implementation
|
|
189
|
+
├── README.md
|
|
190
|
+
└── tests/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Add a `vizzly.plugin` field to your `package.json`:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"name": "vizzly-plugin-custom",
|
|
198
|
+
"version": "1.0.0",
|
|
199
|
+
"description": "My custom Vizzly plugin",
|
|
200
|
+
"main": "./lib/index.js",
|
|
201
|
+
"vizzly": {
|
|
202
|
+
"plugin": "./plugin.js"
|
|
203
|
+
},
|
|
204
|
+
"keywords": ["vizzly", "vizzly-plugin"],
|
|
205
|
+
"peerDependencies": {
|
|
206
|
+
"@vizzly-testing/cli": "^0.9.0"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Users can then install and use your plugin:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npm install vizzly-plugin-custom
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// vizzly.config.js
|
|
219
|
+
export default {
|
|
220
|
+
plugins: ['vizzly-plugin-custom'],
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Best Practices
|
|
225
|
+
|
|
226
|
+
### Error Handling
|
|
227
|
+
|
|
228
|
+
Always handle errors gracefully and provide helpful error messages:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
register(program, { logger }) {
|
|
232
|
+
program
|
|
233
|
+
.command('process <file>')
|
|
234
|
+
.action(async (file) => {
|
|
235
|
+
try {
|
|
236
|
+
if (!existsSync(file)) {
|
|
237
|
+
logger.error(`File not found: ${file}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
// Process file...
|
|
241
|
+
} catch (error) {
|
|
242
|
+
logger.error(`Failed to process file: ${error.message}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Logging
|
|
250
|
+
|
|
251
|
+
Use the provided logger for consistent output across all CLI commands:
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
logger.debug('Detailed debug info'); // Only shown with --verbose
|
|
255
|
+
logger.info('Normal information'); // Standard output
|
|
256
|
+
logger.warn('Warning message'); // Warning output
|
|
257
|
+
logger.error('Error message'); // Error output
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Async Operations
|
|
261
|
+
|
|
262
|
+
Use async/await for asynchronous operations:
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
.action(async (options) => {
|
|
266
|
+
let service = await services.get('apiService');
|
|
267
|
+
let result = await service.doSomething();
|
|
268
|
+
logger.info(`Result: ${result}`);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Command Naming
|
|
273
|
+
|
|
274
|
+
Choose clear, descriptive command names that don't conflict with core commands:
|
|
275
|
+
|
|
276
|
+
✅ **Good command names:**
|
|
277
|
+
- `vizzly storybook`
|
|
278
|
+
- `vizzly import-chromatic`
|
|
279
|
+
- `vizzly generate-report`
|
|
280
|
+
|
|
281
|
+
❌ **Avoid these (conflicts with core):**
|
|
282
|
+
- `vizzly run`
|
|
283
|
+
- `vizzly upload`
|
|
284
|
+
- `vizzly init`
|
|
285
|
+
- `vizzly tdd`
|
|
286
|
+
|
|
287
|
+
### Input Validation
|
|
288
|
+
|
|
289
|
+
Validate user input and provide helpful error messages:
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
.action(async (path, options) => {
|
|
293
|
+
if (!path) {
|
|
294
|
+
logger.error('Path is required');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!existsSync(path)) {
|
|
299
|
+
logger.error(`Path not found: ${path}`);
|
|
300
|
+
logger.info('Please provide a valid path to your build directory');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Continue with logic
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Lazy Loading
|
|
309
|
+
|
|
310
|
+
Import heavy dependencies only when needed to keep CLI startup fast:
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
register(program, { logger }) {
|
|
314
|
+
program
|
|
315
|
+
.command('process-images <dir>')
|
|
316
|
+
.action(async (dir) => {
|
|
317
|
+
// Only import when command runs
|
|
318
|
+
let { processImages } = await import('./heavy-dependency.js');
|
|
319
|
+
await processImages(dir);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Examples
|
|
325
|
+
|
|
326
|
+
### Simple Command Plugin
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
export default {
|
|
330
|
+
name: 'hello',
|
|
331
|
+
version: '1.0.0',
|
|
332
|
+
register(program, { logger }) {
|
|
333
|
+
program
|
|
334
|
+
.command('hello <name>')
|
|
335
|
+
.description('Say hello')
|
|
336
|
+
.option('-l, --loud', 'Say it loudly')
|
|
337
|
+
.action((name, options) => {
|
|
338
|
+
let greeting = `Hello, ${name}!`;
|
|
339
|
+
if (options.loud) {
|
|
340
|
+
greeting = greeting.toUpperCase();
|
|
341
|
+
}
|
|
342
|
+
logger.info(greeting);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Screenshot Capture Plugin
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
export default {
|
|
352
|
+
name: 'storybook',
|
|
353
|
+
version: '1.0.0',
|
|
354
|
+
register(program, { config, logger, services }) {
|
|
355
|
+
program
|
|
356
|
+
.command('storybook <path>')
|
|
357
|
+
.description('Capture screenshots from Storybook build')
|
|
358
|
+
.option('--viewports <list>', 'Comma-separated viewports', '1280x720')
|
|
359
|
+
.action(async (path, options) => {
|
|
360
|
+
logger.info(`Crawling Storybook at ${path}`);
|
|
361
|
+
|
|
362
|
+
// Import dependencies lazily
|
|
363
|
+
let { crawlStorybook } = await import('./crawler.js');
|
|
364
|
+
|
|
365
|
+
// Capture screenshots
|
|
366
|
+
let screenshots = await crawlStorybook(path, {
|
|
367
|
+
viewports: options.viewports.split(','),
|
|
368
|
+
logger,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
logger.info(`Captured ${screenshots.length} screenshots`);
|
|
372
|
+
|
|
373
|
+
// Upload using Vizzly's uploader service
|
|
374
|
+
let uploader = await services.get('uploader');
|
|
375
|
+
await uploader.uploadScreenshots(screenshots);
|
|
376
|
+
|
|
377
|
+
logger.info('Upload complete!');
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Multi-Command Plugin
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
export default {
|
|
387
|
+
name: 'reports',
|
|
388
|
+
version: '1.0.0',
|
|
389
|
+
register(program, { logger }) {
|
|
390
|
+
let reports = program
|
|
391
|
+
.command('reports')
|
|
392
|
+
.description('Report generation commands');
|
|
393
|
+
|
|
394
|
+
reports
|
|
395
|
+
.command('generate')
|
|
396
|
+
.description('Generate a new report')
|
|
397
|
+
.action(() => {
|
|
398
|
+
logger.info('Generating report...');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
reports
|
|
402
|
+
.command('list')
|
|
403
|
+
.description('List all reports')
|
|
404
|
+
.action(() => {
|
|
405
|
+
logger.info('Listing reports...');
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Troubleshooting
|
|
412
|
+
|
|
413
|
+
### Plugin Not Loading
|
|
414
|
+
|
|
415
|
+
**Check plugin configuration:**
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
vizzly --verbose --help
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
This will show debug output about plugin loading.
|
|
422
|
+
|
|
423
|
+
**Common issues:**
|
|
424
|
+
- File path is incorrect (should be relative to `vizzly.config.js`)
|
|
425
|
+
- Plugin file has syntax errors
|
|
426
|
+
- Missing `export default` statement
|
|
427
|
+
|
|
428
|
+
### Plugin Command Not Showing
|
|
429
|
+
|
|
430
|
+
**Verify plugin loaded successfully:**
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
vizzly --verbose --help 2>&1 | grep -i plugin
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Common issues:**
|
|
437
|
+
- Missing `name` or `register` function
|
|
438
|
+
- Command name conflicts with existing commands
|
|
439
|
+
- Plugin threw an error during registration
|
|
440
|
+
|
|
441
|
+
### Type Errors in Editor
|
|
442
|
+
|
|
443
|
+
If you're using TypeScript or want better IDE support, you can add JSDoc types:
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
/**
|
|
447
|
+
* @param {import('commander').Command} program
|
|
448
|
+
* @param {Object} context
|
|
449
|
+
* @param {Object} context.config
|
|
450
|
+
* @param {Object} context.logger
|
|
451
|
+
* @param {Object} context.services
|
|
452
|
+
*/
|
|
453
|
+
function register(program, { config, logger, services }) {
|
|
454
|
+
// Your plugin code with full autocomplete!
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export default {
|
|
458
|
+
name: 'my-plugin',
|
|
459
|
+
register,
|
|
460
|
+
};
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Contributing a Plugin
|
|
464
|
+
|
|
465
|
+
### Share Your Plugin
|
|
466
|
+
|
|
467
|
+
Built a useful plugin? We'd love to feature it! Here's how to share:
|
|
468
|
+
|
|
469
|
+
1. **Publish to npm** (optional but recommended for reusability)
|
|
470
|
+
2. **Add documentation** - Include a clear README with usage examples
|
|
471
|
+
3. **Open an issue** on the [Vizzly CLI repository](https://github.com/vizzly-testing/cli/issues) with:
|
|
472
|
+
- Plugin name and description
|
|
473
|
+
- Link to repository or npm package
|
|
474
|
+
- Usage example
|
|
475
|
+
- Screenshots/demo if applicable
|
|
476
|
+
|
|
477
|
+
### Plugin Ideas
|
|
478
|
+
|
|
479
|
+
Here are some plugin ideas the community might find useful:
|
|
480
|
+
|
|
481
|
+
- **Playwright integration** - Automated screenshot capture from Playwright tests
|
|
482
|
+
- **Cypress integration** - Screenshot capture from Cypress tests
|
|
483
|
+
- **Percy migration** - Import screenshots from Percy
|
|
484
|
+
- **Chromatic migration** - Import screenshots from Chromatic
|
|
485
|
+
- **Figma comparison** - Compare designs with Figma files
|
|
486
|
+
- **Image optimization** - Compress screenshots before upload
|
|
487
|
+
- **Custom reporters** - Generate custom HTML/PDF reports
|
|
488
|
+
- **CI/CD integrations** - Specialized workflows for different CI platforms
|
|
489
|
+
|
|
490
|
+
## Support
|
|
491
|
+
|
|
492
|
+
Need help building a plugin?
|
|
493
|
+
|
|
494
|
+
- 📖 [View example plugin source](https://github.com/vizzly-testing/cli/tree/main/examples/custom-plugin)
|
|
495
|
+
- 🐛 [Report issues](https://github.com/vizzly-testing/cli/issues)
|
|
496
|
+
- 📧 [Email support](https://vizzly.dev/support/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Visual review platform for UI developers and designers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visual-testing",
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
"dotenv": "^17.2.1",
|
|
84
84
|
"form-data": "^4.0.0",
|
|
85
85
|
"glob": "^11.0.3",
|
|
86
|
-
"odiff-bin": "^3.2.1"
|
|
86
|
+
"odiff-bin": "^3.2.1",
|
|
87
|
+
"zod": "^3.24.1"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@babel/cli": "^7.28.0",
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
"eslint-config-prettier": "^10.1.8",
|
|
104
105
|
"eslint-plugin-prettier": "^5.5.3",
|
|
105
106
|
"eslint-plugin-react": "^7.37.5",
|
|
106
|
-
"eslint-plugin-react-hooks": "^
|
|
107
|
+
"eslint-plugin-react-hooks": "^6.1.1",
|
|
107
108
|
"postcss": "^8.5.6",
|
|
108
109
|
"prettier": "^3.6.2",
|
|
109
110
|
"react": "^19.1.1",
|