figma-metadata-extractor 1.0.11 → 1.0.12
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/dist/index.cjs +100 -35
- package/dist/index.js +100 -35
- package/package.json +67 -67
package/dist/index.cjs
CHANGED
|
@@ -289,15 +289,20 @@ class FigmaService {
|
|
|
289
289
|
*/
|
|
290
290
|
filterValidImages(images) {
|
|
291
291
|
if (!images) return {};
|
|
292
|
-
return Object.fromEntries(
|
|
292
|
+
return Object.fromEntries(
|
|
293
|
+
Object.entries(images).filter(([, value]) => !!value)
|
|
294
|
+
);
|
|
293
295
|
}
|
|
294
296
|
async request(endpoint) {
|
|
295
297
|
try {
|
|
296
298
|
Logger.log(`Calling ${this.baseUrl}${endpoint}`);
|
|
297
299
|
const headers = this.getAuthHeaders();
|
|
298
|
-
return await fetchWithRetry(
|
|
299
|
-
|
|
300
|
-
|
|
300
|
+
return await fetchWithRetry(
|
|
301
|
+
`${this.baseUrl}${endpoint}`,
|
|
302
|
+
{
|
|
303
|
+
headers
|
|
304
|
+
}
|
|
305
|
+
);
|
|
301
306
|
} catch (error) {
|
|
302
307
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
303
308
|
throw new Error(
|
|
@@ -372,7 +377,9 @@ class FigmaService {
|
|
|
372
377
|
const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
373
378
|
resolvedPath = path.resolve(sanitizedPath);
|
|
374
379
|
if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
|
|
375
|
-
throw new Error(
|
|
380
|
+
throw new Error(
|
|
381
|
+
"Invalid path specified. Directory traversal is not allowed."
|
|
382
|
+
);
|
|
376
383
|
}
|
|
377
384
|
}
|
|
378
385
|
const downloadPromises = [];
|
|
@@ -384,25 +391,45 @@ class FigmaService {
|
|
|
384
391
|
);
|
|
385
392
|
if (imageFills.length > 0) {
|
|
386
393
|
const fillUrls = await this.getImageFillUrls(fileKey);
|
|
387
|
-
const fillDownloads = imageFills.map(
|
|
388
|
-
|
|
389
|
-
|
|
394
|
+
const fillDownloads = imageFills.map(
|
|
395
|
+
({
|
|
396
|
+
imageRef,
|
|
390
397
|
fileName,
|
|
391
|
-
resolvedPath,
|
|
392
|
-
imageUrl,
|
|
393
398
|
needsCropping,
|
|
394
399
|
cropTransform,
|
|
395
|
-
requiresImageDimensions
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
requiresImageDimensions
|
|
401
|
+
}) => {
|
|
402
|
+
const imageUrl = fillUrls[imageRef];
|
|
403
|
+
if (!imageUrl) {
|
|
404
|
+
Logger.log(
|
|
405
|
+
`Skipping image fill with missing URL for imageRef: ${imageRef}`
|
|
406
|
+
);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return downloadAndProcessImage(
|
|
410
|
+
fileName,
|
|
411
|
+
resolvedPath,
|
|
412
|
+
imageUrl,
|
|
413
|
+
needsCropping,
|
|
414
|
+
cropTransform,
|
|
415
|
+
requiresImageDimensions,
|
|
416
|
+
returnBuffer
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
).filter(
|
|
420
|
+
(promise) => promise !== null
|
|
421
|
+
);
|
|
399
422
|
if (fillDownloads.length > 0) {
|
|
400
423
|
downloadPromises.push(Promise.all(fillDownloads));
|
|
401
424
|
}
|
|
402
425
|
}
|
|
403
426
|
if (renderNodes.length > 0) {
|
|
404
|
-
const pngNodes = renderNodes.filter(
|
|
405
|
-
|
|
427
|
+
const pngNodes = renderNodes.filter(
|
|
428
|
+
(node) => !node.fileName.toLowerCase().endsWith(".svg")
|
|
429
|
+
);
|
|
430
|
+
const svgNodes = renderNodes.filter(
|
|
431
|
+
(node) => node.fileName.toLowerCase().endsWith(".svg")
|
|
432
|
+
);
|
|
406
433
|
if (pngNodes.length > 0) {
|
|
407
434
|
const pngUrls = await this.getNodeRenderUrls(
|
|
408
435
|
fileKey,
|
|
@@ -410,18 +437,36 @@ class FigmaService {
|
|
|
410
437
|
"png",
|
|
411
438
|
{ pngScale }
|
|
412
439
|
);
|
|
413
|
-
const pngDownloads = pngNodes.map(
|
|
414
|
-
|
|
415
|
-
|
|
440
|
+
const pngDownloads = pngNodes.map(
|
|
441
|
+
({
|
|
442
|
+
nodeId,
|
|
416
443
|
fileName,
|
|
417
|
-
resolvedPath,
|
|
418
|
-
imageUrl,
|
|
419
444
|
needsCropping,
|
|
420
445
|
cropTransform,
|
|
421
|
-
requiresImageDimensions
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
446
|
+
requiresImageDimensions
|
|
447
|
+
}) => {
|
|
448
|
+
const imageUrl = pngUrls[nodeId];
|
|
449
|
+
if (!imageUrl) {
|
|
450
|
+
Logger.log(
|
|
451
|
+
`Skipping PNG render with missing URL for nodeId: ${nodeId}`
|
|
452
|
+
);
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
return downloadAndProcessImage(
|
|
456
|
+
fileName,
|
|
457
|
+
resolvedPath,
|
|
458
|
+
imageUrl,
|
|
459
|
+
needsCropping,
|
|
460
|
+
cropTransform,
|
|
461
|
+
requiresImageDimensions,
|
|
462
|
+
returnBuffer
|
|
463
|
+
).then(
|
|
464
|
+
(result) => ({ ...result, nodeId })
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
).filter(
|
|
468
|
+
(promise) => promise !== null
|
|
469
|
+
);
|
|
425
470
|
if (pngDownloads.length > 0) {
|
|
426
471
|
downloadPromises.push(Promise.all(pngDownloads));
|
|
427
472
|
}
|
|
@@ -433,18 +478,36 @@ class FigmaService {
|
|
|
433
478
|
"svg",
|
|
434
479
|
{ svgOptions }
|
|
435
480
|
);
|
|
436
|
-
const svgDownloads = svgNodes.map(
|
|
437
|
-
|
|
438
|
-
|
|
481
|
+
const svgDownloads = svgNodes.map(
|
|
482
|
+
({
|
|
483
|
+
nodeId,
|
|
439
484
|
fileName,
|
|
440
|
-
resolvedPath,
|
|
441
|
-
imageUrl,
|
|
442
485
|
needsCropping,
|
|
443
486
|
cropTransform,
|
|
444
|
-
requiresImageDimensions
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
487
|
+
requiresImageDimensions
|
|
488
|
+
}) => {
|
|
489
|
+
const imageUrl = svgUrls[nodeId];
|
|
490
|
+
if (!imageUrl) {
|
|
491
|
+
Logger.log(
|
|
492
|
+
`Skipping SVG render with missing URL for nodeId: ${nodeId}`
|
|
493
|
+
);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return downloadAndProcessImage(
|
|
497
|
+
fileName,
|
|
498
|
+
resolvedPath,
|
|
499
|
+
imageUrl,
|
|
500
|
+
needsCropping,
|
|
501
|
+
cropTransform,
|
|
502
|
+
requiresImageDimensions,
|
|
503
|
+
returnBuffer
|
|
504
|
+
).then(
|
|
505
|
+
(result) => ({ ...result, nodeId })
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
).filter(
|
|
509
|
+
(promise) => promise !== null
|
|
510
|
+
);
|
|
448
511
|
if (svgDownloads.length > 0) {
|
|
449
512
|
downloadPromises.push(Promise.all(svgDownloads));
|
|
450
513
|
}
|
|
@@ -458,7 +521,9 @@ class FigmaService {
|
|
|
458
521
|
*/
|
|
459
522
|
async getRawFile(fileKey, depth) {
|
|
460
523
|
const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`;
|
|
461
|
-
Logger.log(
|
|
524
|
+
Logger.log(
|
|
525
|
+
`Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`
|
|
526
|
+
);
|
|
462
527
|
const response = await this.request(endpoint);
|
|
463
528
|
writeLogs("figma-raw.json", response);
|
|
464
529
|
return response;
|
package/dist/index.js
CHANGED
|
@@ -287,15 +287,20 @@ class FigmaService {
|
|
|
287
287
|
*/
|
|
288
288
|
filterValidImages(images) {
|
|
289
289
|
if (!images) return {};
|
|
290
|
-
return Object.fromEntries(
|
|
290
|
+
return Object.fromEntries(
|
|
291
|
+
Object.entries(images).filter(([, value]) => !!value)
|
|
292
|
+
);
|
|
291
293
|
}
|
|
292
294
|
async request(endpoint) {
|
|
293
295
|
try {
|
|
294
296
|
Logger.log(`Calling ${this.baseUrl}${endpoint}`);
|
|
295
297
|
const headers = this.getAuthHeaders();
|
|
296
|
-
return await fetchWithRetry(
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
return await fetchWithRetry(
|
|
299
|
+
`${this.baseUrl}${endpoint}`,
|
|
300
|
+
{
|
|
301
|
+
headers
|
|
302
|
+
}
|
|
303
|
+
);
|
|
299
304
|
} catch (error) {
|
|
300
305
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
301
306
|
throw new Error(
|
|
@@ -370,7 +375,9 @@ class FigmaService {
|
|
|
370
375
|
const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
371
376
|
resolvedPath = path.resolve(sanitizedPath);
|
|
372
377
|
if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
|
|
373
|
-
throw new Error(
|
|
378
|
+
throw new Error(
|
|
379
|
+
"Invalid path specified. Directory traversal is not allowed."
|
|
380
|
+
);
|
|
374
381
|
}
|
|
375
382
|
}
|
|
376
383
|
const downloadPromises = [];
|
|
@@ -382,25 +389,45 @@ class FigmaService {
|
|
|
382
389
|
);
|
|
383
390
|
if (imageFills.length > 0) {
|
|
384
391
|
const fillUrls = await this.getImageFillUrls(fileKey);
|
|
385
|
-
const fillDownloads = imageFills.map(
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
const fillDownloads = imageFills.map(
|
|
393
|
+
({
|
|
394
|
+
imageRef,
|
|
388
395
|
fileName,
|
|
389
|
-
resolvedPath,
|
|
390
|
-
imageUrl,
|
|
391
396
|
needsCropping,
|
|
392
397
|
cropTransform,
|
|
393
|
-
requiresImageDimensions
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
398
|
+
requiresImageDimensions
|
|
399
|
+
}) => {
|
|
400
|
+
const imageUrl = fillUrls[imageRef];
|
|
401
|
+
if (!imageUrl) {
|
|
402
|
+
Logger.log(
|
|
403
|
+
`Skipping image fill with missing URL for imageRef: ${imageRef}`
|
|
404
|
+
);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
return downloadAndProcessImage(
|
|
408
|
+
fileName,
|
|
409
|
+
resolvedPath,
|
|
410
|
+
imageUrl,
|
|
411
|
+
needsCropping,
|
|
412
|
+
cropTransform,
|
|
413
|
+
requiresImageDimensions,
|
|
414
|
+
returnBuffer
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
).filter(
|
|
418
|
+
(promise) => promise !== null
|
|
419
|
+
);
|
|
397
420
|
if (fillDownloads.length > 0) {
|
|
398
421
|
downloadPromises.push(Promise.all(fillDownloads));
|
|
399
422
|
}
|
|
400
423
|
}
|
|
401
424
|
if (renderNodes.length > 0) {
|
|
402
|
-
const pngNodes = renderNodes.filter(
|
|
403
|
-
|
|
425
|
+
const pngNodes = renderNodes.filter(
|
|
426
|
+
(node) => !node.fileName.toLowerCase().endsWith(".svg")
|
|
427
|
+
);
|
|
428
|
+
const svgNodes = renderNodes.filter(
|
|
429
|
+
(node) => node.fileName.toLowerCase().endsWith(".svg")
|
|
430
|
+
);
|
|
404
431
|
if (pngNodes.length > 0) {
|
|
405
432
|
const pngUrls = await this.getNodeRenderUrls(
|
|
406
433
|
fileKey,
|
|
@@ -408,18 +435,36 @@ class FigmaService {
|
|
|
408
435
|
"png",
|
|
409
436
|
{ pngScale }
|
|
410
437
|
);
|
|
411
|
-
const pngDownloads = pngNodes.map(
|
|
412
|
-
|
|
413
|
-
|
|
438
|
+
const pngDownloads = pngNodes.map(
|
|
439
|
+
({
|
|
440
|
+
nodeId,
|
|
414
441
|
fileName,
|
|
415
|
-
resolvedPath,
|
|
416
|
-
imageUrl,
|
|
417
442
|
needsCropping,
|
|
418
443
|
cropTransform,
|
|
419
|
-
requiresImageDimensions
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
444
|
+
requiresImageDimensions
|
|
445
|
+
}) => {
|
|
446
|
+
const imageUrl = pngUrls[nodeId];
|
|
447
|
+
if (!imageUrl) {
|
|
448
|
+
Logger.log(
|
|
449
|
+
`Skipping PNG render with missing URL for nodeId: ${nodeId}`
|
|
450
|
+
);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return downloadAndProcessImage(
|
|
454
|
+
fileName,
|
|
455
|
+
resolvedPath,
|
|
456
|
+
imageUrl,
|
|
457
|
+
needsCropping,
|
|
458
|
+
cropTransform,
|
|
459
|
+
requiresImageDimensions,
|
|
460
|
+
returnBuffer
|
|
461
|
+
).then(
|
|
462
|
+
(result) => ({ ...result, nodeId })
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
).filter(
|
|
466
|
+
(promise) => promise !== null
|
|
467
|
+
);
|
|
423
468
|
if (pngDownloads.length > 0) {
|
|
424
469
|
downloadPromises.push(Promise.all(pngDownloads));
|
|
425
470
|
}
|
|
@@ -431,18 +476,36 @@ class FigmaService {
|
|
|
431
476
|
"svg",
|
|
432
477
|
{ svgOptions }
|
|
433
478
|
);
|
|
434
|
-
const svgDownloads = svgNodes.map(
|
|
435
|
-
|
|
436
|
-
|
|
479
|
+
const svgDownloads = svgNodes.map(
|
|
480
|
+
({
|
|
481
|
+
nodeId,
|
|
437
482
|
fileName,
|
|
438
|
-
resolvedPath,
|
|
439
|
-
imageUrl,
|
|
440
483
|
needsCropping,
|
|
441
484
|
cropTransform,
|
|
442
|
-
requiresImageDimensions
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
485
|
+
requiresImageDimensions
|
|
486
|
+
}) => {
|
|
487
|
+
const imageUrl = svgUrls[nodeId];
|
|
488
|
+
if (!imageUrl) {
|
|
489
|
+
Logger.log(
|
|
490
|
+
`Skipping SVG render with missing URL for nodeId: ${nodeId}`
|
|
491
|
+
);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return downloadAndProcessImage(
|
|
495
|
+
fileName,
|
|
496
|
+
resolvedPath,
|
|
497
|
+
imageUrl,
|
|
498
|
+
needsCropping,
|
|
499
|
+
cropTransform,
|
|
500
|
+
requiresImageDimensions,
|
|
501
|
+
returnBuffer
|
|
502
|
+
).then(
|
|
503
|
+
(result) => ({ ...result, nodeId })
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
).filter(
|
|
507
|
+
(promise) => promise !== null
|
|
508
|
+
);
|
|
446
509
|
if (svgDownloads.length > 0) {
|
|
447
510
|
downloadPromises.push(Promise.all(svgDownloads));
|
|
448
511
|
}
|
|
@@ -456,7 +519,9 @@ class FigmaService {
|
|
|
456
519
|
*/
|
|
457
520
|
async getRawFile(fileKey, depth) {
|
|
458
521
|
const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`;
|
|
459
|
-
Logger.log(
|
|
522
|
+
Logger.log(
|
|
523
|
+
`Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`
|
|
524
|
+
);
|
|
460
525
|
const response = await this.request(endpoint);
|
|
461
526
|
writeLogs("figma-raw.json", response);
|
|
462
527
|
return response;
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"README.md",
|
|
19
|
-
"example.js"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "vite build && tsc --project tsconfig.declarations.json",
|
|
23
|
-
"typecheck": "tsc --noEmit",
|
|
24
|
-
"test": "jest",
|
|
25
|
-
"dev": "vite build --watch",
|
|
26
|
-
"lint": "eslint .",
|
|
27
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
-
"prepack": "npm run build"
|
|
29
|
-
},
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=18.0.0"
|
|
32
|
-
},
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/mikmokpok/figma-context-extractor.git"
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"figma",
|
|
39
|
-
"design",
|
|
40
|
-
"metadata",
|
|
41
|
-
"extractor",
|
|
42
|
-
"typescript",
|
|
43
|
-
"api",
|
|
44
|
-
"images"
|
|
45
|
-
],
|
|
46
|
-
"author": "",
|
|
47
|
-
"license": "MIT",
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"@figma/rest-api-spec": "^0.33.0",
|
|
50
|
-
"js-yaml": "^4.1.0",
|
|
51
|
-
"remeda": "^2.20.1",
|
|
52
|
-
"sharp": "^0.34.3",
|
|
53
|
-
"zod": "^3.24.2"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@types/jest": "^29.5.14",
|
|
57
|
-
"@types/js-yaml": "^4.0.9",
|
|
58
|
-
"@types/node": "^20.17.0",
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
60
|
-
"@typescript-eslint/parser": "^8.24.0",
|
|
61
|
-
"eslint": "^9.20.1",
|
|
62
|
-
"eslint-config-prettier": "^10.0.1",
|
|
63
|
-
"jest": "^29.7.0",
|
|
64
|
-
"prettier": "^3.5.0",
|
|
65
|
-
"ts-jest": "^29.2.5",
|
|
66
|
-
"typescript": "^5.7.3",
|
|
67
|
-
"vite": "^6.4.1"
|
|
2
|
+
"name": "figma-metadata-extractor",
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
68
14
|
}
|
|
69
|
-
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"example.js"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vite build && tsc --project tsconfig.declarations.json",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"dev": "vite build --watch",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
+
"prepack": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/mikmokpok/figma-context-extractor.git"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"figma",
|
|
39
|
+
"design",
|
|
40
|
+
"metadata",
|
|
41
|
+
"extractor",
|
|
42
|
+
"typescript",
|
|
43
|
+
"api",
|
|
44
|
+
"images"
|
|
45
|
+
],
|
|
46
|
+
"author": "",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@figma/rest-api-spec": "^0.33.0",
|
|
50
|
+
"js-yaml": "^4.1.0",
|
|
51
|
+
"remeda": "^2.20.1",
|
|
52
|
+
"sharp": "^0.34.3",
|
|
53
|
+
"zod": "^3.24.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/jest": "^29.5.14",
|
|
57
|
+
"@types/js-yaml": "^4.0.9",
|
|
58
|
+
"@types/node": "^20.17.0",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.24.0",
|
|
61
|
+
"eslint": "^9.20.1",
|
|
62
|
+
"eslint-config-prettier": "^10.0.1",
|
|
63
|
+
"jest": "^29.7.0",
|
|
64
|
+
"prettier": "^3.5.0",
|
|
65
|
+
"ts-jest": "^29.2.5",
|
|
66
|
+
"typescript": "^5.7.3",
|
|
67
|
+
"vite": "^6.4.1"
|
|
68
|
+
}
|
|
69
|
+
}
|