@polotno/pdf-export 0.1.18 → 0.1.19

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/lib/text.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface TextElement {
2
+ fontFamily: string;
3
+ fontWeight?: string;
4
+ fontSize: number;
5
+ fill: string;
6
+ opacity: number;
7
+ strokeWidth: number;
8
+ stroke?: string;
9
+ align: string;
10
+ textDecoration: string;
11
+ letterSpacing?: number;
12
+ lineHeight: number;
13
+ text: string;
14
+ width: number;
15
+ height: number;
16
+ verticalAlign?: string;
17
+ backgroundEnabled?: boolean;
18
+ backgroundPadding?: number;
19
+ backgroundCornerRadius?: number;
20
+ backgroundColor?: string;
21
+ }
22
+ export interface RenderAttrs {
23
+ pdfx1a?: boolean;
24
+ textVerticalResizeEnabled?: boolean;
25
+ }
26
+ export declare function getGoogleFontPath(fontFamily: string, fontWeight?: string): Promise<string>;
27
+ export declare function loadFontIfNeeded(doc: any, element: TextElement, fonts: Record<string, boolean>): Promise<string>;
28
+ export declare function renderText(doc: any, element: TextElement, attrs?: RenderAttrs): void;
package/lib/text.js CHANGED
@@ -1,184 +1,157 @@
1
- const { parseColor, srcToBuffer } = require('./utils');
2
- const getUrls = require('get-urls').default;
3
- const fetch = require('node-fetch').default;
4
-
5
- async function getGoogleFontPath(fontFamily, fontWeight = 'normal') {
6
- const weight = fontWeight === 'bold' ? '700' : '400';
7
- const url = `https://fonts.googleapis.com/css?family=${fontFamily}:${weight}`;
8
- const req = await fetch(url);
9
- if (!req.ok) {
10
- if (weight !== '400') {
11
- return getGoogleFontPath(fontFamily, 'normal');
1
+ import { parseColor, srcToBuffer } from './utils.js';
2
+ import getUrls from 'get-urls';
3
+ import fetch from 'node-fetch';
4
+ export async function getGoogleFontPath(fontFamily, fontWeight = 'normal') {
5
+ const weight = fontWeight === 'bold' ? '700' : '400';
6
+ const url = `https://fonts.googleapis.com/css?family=${fontFamily}:${weight}`;
7
+ const req = await fetch(url);
8
+ if (!req.ok) {
9
+ if (weight !== '400') {
10
+ return getGoogleFontPath(fontFamily, 'normal');
11
+ }
12
+ throw new Error(`Failed to fetch Google font: ${fontFamily}`);
12
13
  }
13
- throw new Error(`Failed to fetch Google font: ${fontFamily}`);
14
- }
15
- const text = await req.text();
16
- const urls = getUrls(text);
17
- return urls.values().next().value;
14
+ const text = await req.text();
15
+ const urls = getUrls(text);
16
+ return urls.values().next().value;
18
17
  }
19
-
20
- async function loadFontIfNeeded(doc, element, fonts) {
21
- // check if universal font is already defined
22
- if (fonts[element.fontFamily]) {
23
- doc.font(element.fontFamily);
24
- return element.fontFamily;
25
- }
26
- const fontKey = `${element.fontFamily}-${element.fontWeight || 'normal'}`;
27
- if (!fonts[fontKey]) {
28
- const src = await getGoogleFontPath(element.fontFamily, element.fontWeight);
29
- doc.registerFont(fontKey, await srcToBuffer(src));
30
- fonts[fontKey] = true;
31
- }
32
- doc.font(fontKey);
33
- return fontKey;
18
+ export async function loadFontIfNeeded(doc, element, fonts) {
19
+ // check if universal font is already defined
20
+ if (fonts[element.fontFamily]) {
21
+ doc.font(element.fontFamily);
22
+ return element.fontFamily;
23
+ }
24
+ const fontKey = `${element.fontFamily}-${element.fontWeight || 'normal'}`;
25
+ if (!fonts[fontKey]) {
26
+ const src = await getGoogleFontPath(element.fontFamily, element.fontWeight);
27
+ doc.registerFont(fontKey, await srcToBuffer(src));
28
+ fonts[fontKey] = true;
29
+ }
30
+ doc.font(fontKey);
31
+ return fontKey;
34
32
  }
35
-
36
- function renderText(doc, element, attrs = {}) {
37
- doc.fontSize(element.fontSize);
38
- doc.fillColor(parseColor(element.fill).hex, element.opacity);
39
-
40
- // Handle stroked text differently for PDF/X-1a compatibility
41
- const hasStroke = element.strokeWidth > 0;
42
- const isPDFX1a = attrs.pdfx1a;
43
-
44
- if (hasStroke && !isPDFX1a) {
45
- // Standard PDF: use PDFKit's built-in stroke support
46
- doc.lineWidth(element.strokeWidth / 2);
47
- doc.strokeColor(parseColor(element.stroke).hex);
48
- }
49
-
50
- const props = {
51
- align: element.align,
52
- fill: element.fill,
53
- baseline: 'top',
54
- lineGap: 1,
55
- width: element.width,
56
- underline: element.textDecoration.indexOf('underline') >= 0,
57
- characterSpacing: element.letterSpacing
58
- ? element.letterSpacing * element.fontSize
59
- : 0,
60
- lineBreak: false,
61
- stroke: hasStroke && !isPDFX1a, // Only use stroke for non-PDF/X-1a
62
- };
63
-
64
- const currentLineHeight = doc.heightOfString('A', props);
65
- const lineHeight = element.lineHeight * element.fontSize;
66
-
67
- const fontBoundingBoxAscent = (doc._font.ascender / 1000) * element.fontSize;
68
- const fontBoundingBoxDescent =
69
- (doc._font.descender / 1000) * element.fontSize;
70
- const translateY = (fontBoundingBoxAscent - fontBoundingBoxDescent) / 2;
71
-
72
- const diff = currentLineHeight - lineHeight;
73
- props.lineGap = props.lineGap - diff;
74
-
75
- let yOffset = 0;
76
- if ((attrs.textVerticalResizeEnabled || true) && element.verticalAlign) {
77
- const textHeight = doc.heightOfString(element.text, props);
78
- if (element.verticalAlign === 'middle') {
79
- yOffset = (element.height - textHeight) / 2;
80
- } else if (element.verticalAlign === 'bottom') {
81
- yOffset = element.height - textHeight;
33
+ export function renderText(doc, element, attrs = {}) {
34
+ doc.fontSize(element.fontSize);
35
+ doc.fillColor(parseColor(element.fill).hex, element.opacity);
36
+ // Handle stroked text differently for PDF/X-1a compatibility
37
+ const hasStroke = element.strokeWidth > 0;
38
+ const isPDFX1a = attrs.pdfx1a;
39
+ if (hasStroke && !isPDFX1a) {
40
+ // Standard PDF: use PDFKit's built-in stroke support
41
+ doc.lineWidth(element.strokeWidth / 2);
42
+ doc.strokeColor(parseColor(element.stroke).hex);
82
43
  }
83
- }
84
-
85
- for (var size = element.fontSize; size > 0; size -= 1) {
86
- doc.fontSize(size);
87
- const height = doc.heightOfString(element.text, {
88
- ...props,
89
- });
90
- if (height <= element.height) {
91
- break;
44
+ const props = {
45
+ align: element.align,
46
+ fill: element.fill,
47
+ baseline: 'top',
48
+ lineGap: 1,
49
+ width: element.width,
50
+ underline: element.textDecoration.indexOf('underline') >= 0,
51
+ characterSpacing: element.letterSpacing
52
+ ? element.letterSpacing * element.fontSize
53
+ : 0,
54
+ lineBreak: false,
55
+ stroke: hasStroke && !isPDFX1a, // Only use stroke for non-PDF/X-1a
56
+ };
57
+ const currentLineHeight = doc.heightOfString('A', props);
58
+ const lineHeight = element.lineHeight * element.fontSize;
59
+ const fontBoundingBoxAscent = (doc._font.ascender / 1000) * element.fontSize;
60
+ const fontBoundingBoxDescent = (doc._font.descender / 1000) * element.fontSize;
61
+ const translateY = (fontBoundingBoxAscent - fontBoundingBoxDescent) / 2;
62
+ const diff = currentLineHeight - lineHeight;
63
+ props.lineGap = props.lineGap - diff;
64
+ let yOffset = 0;
65
+ if ((attrs.textVerticalResizeEnabled || true) && element.verticalAlign) {
66
+ const textHeight = doc.heightOfString(element.text, props);
67
+ if (element.verticalAlign === 'middle') {
68
+ yOffset = (element.height - textHeight) / 2;
69
+ }
70
+ else if (element.verticalAlign === 'bottom') {
71
+ yOffset = element.height - textHeight;
72
+ }
92
73
  }
93
- }
94
-
95
- const halfLineHeight = ((element.lineHeight - 1) / 2) * element.fontSize;
96
-
97
- if (element.backgroundEnabled) {
98
- const backPadding =
99
- element.backgroundPadding * (element.fontSize * element.lineHeight);
100
- const cornerRadius =
101
- element.backgroundCornerRadius *
102
- (element.fontSize * element.lineHeight * 0.5);
103
-
104
- const textWidth = doc.widthOfString(element.text, {
105
- ...props,
106
- width: element.width,
107
- });
108
- const textHeight = doc.heightOfString(element.text, {
109
- ...props,
110
- width: element.width,
111
- });
112
-
113
- let bgX = -backPadding / 2;
114
- let bgY = -backPadding / 2;
115
- let bgWidth = textWidth + backPadding;
116
- let bgHeight = textHeight + backPadding;
117
-
118
- if (element.align === 'center') {
119
- bgX = (element.width - textWidth) / 2 - backPadding / 2;
120
- } else if (element.align === 'right') {
121
- bgX = element.width - textWidth - backPadding / 2;
74
+ for (var size = element.fontSize; size > 0; size -= 1) {
75
+ doc.fontSize(size);
76
+ const height = doc.heightOfString(element.text, {
77
+ ...props,
78
+ });
79
+ if (height <= element.height) {
80
+ break;
81
+ }
122
82
  }
123
-
124
- if (element.verticalAlign === 'middle') {
125
- bgY = (element.height - textHeight) / 2 - backPadding / 2;
126
- } else if (element.verticalAlign === 'bottom') {
127
- bgY = element.height - textHeight - backPadding / 2;
83
+ const halfLineHeight = ((element.lineHeight - 1) / 2) * element.fontSize;
84
+ if (element.backgroundEnabled) {
85
+ const backPadding = element.backgroundPadding * (element.fontSize * element.lineHeight);
86
+ const cornerRadius = element.backgroundCornerRadius *
87
+ (element.fontSize * element.lineHeight * 0.5);
88
+ const textWidth = doc.widthOfString(element.text, {
89
+ ...props,
90
+ width: element.width,
91
+ });
92
+ const textHeight = doc.heightOfString(element.text, {
93
+ ...props,
94
+ width: element.width,
95
+ });
96
+ let bgX = -backPadding / 2;
97
+ let bgY = -backPadding / 2;
98
+ let bgWidth = textWidth + backPadding;
99
+ let bgHeight = textHeight + backPadding;
100
+ if (element.align === 'center') {
101
+ bgX = (element.width - textWidth) / 2 - backPadding / 2;
102
+ }
103
+ else if (element.align === 'right') {
104
+ bgX = element.width - textWidth - backPadding / 2;
105
+ }
106
+ if (element.verticalAlign === 'middle') {
107
+ bgY = (element.height - textHeight) / 2 - backPadding / 2;
108
+ }
109
+ else if (element.verticalAlign === 'bottom') {
110
+ bgY = element.height - textHeight - backPadding / 2;
111
+ }
112
+ doc.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
113
+ doc.fillColor(parseColor(element.backgroundColor).hex);
114
+ doc.fill();
115
+ doc.fillColor(parseColor(element.fill).hex, element.opacity);
128
116
  }
129
-
130
- doc.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
131
- doc.fillColor(parseColor(element.backgroundColor).hex);
132
- doc.fill();
133
- doc.fillColor(parseColor(element.fill).hex, element.opacity);
134
- }
135
-
136
- // Render text with PDF/X-1a compatible stroke simulation
137
- if (hasStroke && isPDFX1a) {
138
- // For PDF/X-1a: simulate stroke by drawing text multiple times
139
- const strokeColor = parseColor(element.stroke).hex;
140
- const strokeWidth = element.strokeWidth;
141
-
142
- // Draw stroke by rendering text multiple times with offsets
143
- const offsets = [];
144
- for (let angle = 0; angle < 360; angle += 45) {
145
- const radian = (angle * Math.PI) / 180;
146
- offsets.push({
147
- x: Math.cos(radian) * strokeWidth,
148
- y: Math.sin(radian) * strokeWidth,
149
- });
117
+ // Render text with PDF/X-1a compatible stroke simulation
118
+ if (hasStroke && isPDFX1a) {
119
+ // For PDF/X-1a: simulate stroke by drawing text multiple times
120
+ const strokeColor = parseColor(element.stroke).hex;
121
+ const strokeWidth = element.strokeWidth;
122
+ // Draw stroke by rendering text multiple times with offsets
123
+ const offsets = [];
124
+ for (let angle = 0; angle < 360; angle += 45) {
125
+ const radian = (angle * Math.PI) / 180;
126
+ offsets.push({
127
+ x: Math.cos(radian) * strokeWidth,
128
+ y: Math.sin(radian) * strokeWidth,
129
+ });
130
+ }
131
+ // Draw stroke layers
132
+ doc.save();
133
+ doc.fillColor(strokeColor, element.opacity);
134
+ for (const offset of offsets) {
135
+ doc.text(element.text, offset.x, yOffset + halfLineHeight + offset.y, {
136
+ ...props,
137
+ stroke: false, // Force no stroke for compatibility
138
+ height: element.height + element.fontSize,
139
+ });
140
+ }
141
+ doc.restore();
142
+ // Draw fill text on top
143
+ doc.fillColor(parseColor(element.fill).hex, element.opacity);
144
+ doc.text(element.text, 0, yOffset + halfLineHeight, {
145
+ ...props,
146
+ stroke: false, // Force no stroke for compatibility
147
+ height: element.height + element.fontSize,
148
+ });
150
149
  }
151
-
152
- // Draw stroke layers
153
- doc.save();
154
- doc.fillColor(strokeColor, element.opacity);
155
- for (const offset of offsets) {
156
- doc.text(element.text, offset.x, yOffset + halfLineHeight + offset.y, {
157
- ...props,
158
- stroke: false, // Force no stroke for compatibility
159
- height: element.height + element.fontSize,
160
- });
150
+ else {
151
+ // Standard rendering
152
+ doc.text(element.text, 0, yOffset + halfLineHeight, {
153
+ ...props,
154
+ height: element.height + element.fontSize,
155
+ });
161
156
  }
162
- doc.restore();
163
-
164
- // Draw fill text on top
165
- doc.fillColor(parseColor(element.fill).hex, element.opacity);
166
- doc.text(element.text, 0, yOffset + halfLineHeight, {
167
- ...props,
168
- stroke: false, // Force no stroke for compatibility
169
- height: element.height + element.fontSize,
170
- });
171
- } else {
172
- // Standard rendering
173
- doc.text(element.text, 0, yOffset + halfLineHeight, {
174
- ...props,
175
- height: element.height + element.fontSize,
176
- });
177
- }
178
157
  }
179
-
180
- module.exports = {
181
- getGoogleFontPath,
182
- renderText,
183
- loadFontIfNeeded,
184
- };
package/lib/utils.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import parseColor from 'parse-color';
2
+ export declare const DPI = 75;
3
+ export declare const PIXEL_RATIO = 2;
4
+ export declare function pxToPt(px: number): number;
5
+ export interface ImageCache {
6
+ images: Map<string, any>;
7
+ buffers: Map<string, any>;
8
+ processedImages: Map<string, string>;
9
+ imageFiles: Map<string, string>;
10
+ tempDir: string | null;
11
+ }
12
+ export declare function loadImage(src: string, cache?: ImageCache | null): Promise<any>;
13
+ export declare function srcToBase64(src: string, cache?: ImageCache | null): Promise<string>;
14
+ export declare function srcToBuffer(src: string, cache?: ImageCache | null): Promise<Buffer>;
15
+ export { parseColor };
package/lib/utils.js CHANGED
@@ -1,78 +1,94 @@
1
- const parseColor = require('parse-color');
2
- const fetch = require('node-fetch').default;
3
- const fileTypePromise = import('file-type');
4
- const sharp = require('sharp');
5
- const Canvas = require('canvas');
6
-
7
- const DPI = 75;
8
- const PIXEL_RATIO = 2;
9
-
10
- function pxToPt(px) {
11
- return (px * DPI) / 100;
1
+ import parseColor from 'parse-color';
2
+ import fetch from 'node-fetch';
3
+ import Canvas from 'canvas';
4
+ import sharp from 'sharp';
5
+ export const DPI = 75;
6
+ export const PIXEL_RATIO = 2;
7
+ export function pxToPt(px) {
8
+ return (px * DPI) / 100;
12
9
  }
13
-
14
- async function loadImage(src) {
15
- let buffer;
16
- let mime = 'unknown';
17
-
18
- if (src.startsWith('data:')) {
19
- const matches = src.match(/^data:(.+);base64,(.*)$/);
20
- if (matches) {
21
- mime = matches[1];
22
- buffer = Buffer.from(matches[2], 'base64');
23
- } else {
24
- throw new Error('Invalid data URL');
10
+ export async function loadImage(src, cache = null) {
11
+ // Check cache first
12
+ if (cache && cache.images.has(src)) {
13
+ return cache.images.get(src);
25
14
  }
26
- } else {
27
- try {
28
- const response = await fetch(src);
29
- if (!response.ok) {
30
- throw new Error(
31
- `Failed to fetch image: ${src} (Status: ${response.status})`
32
- );
33
- }
34
- buffer = await response.buffer();
35
- const fileType = await fileTypePromise;
36
- const typeData = await fileType.fileTypeFromBuffer(buffer);
37
- if (typeData) {
38
- ({ mime } = typeData);
39
- }
40
- } catch (error) {
41
- throw new Error(`Failed to process image from ${src}: ${error.message}`);
15
+ let buffer;
16
+ let mime = 'unknown';
17
+ if (src.startsWith('data:')) {
18
+ const matches = src.match(/^data:(.+);base64,(.*)$/);
19
+ if (matches) {
20
+ mime = matches[1];
21
+ buffer = Buffer.from(matches[2], 'base64');
22
+ }
23
+ else {
24
+ throw new Error('Invalid data URL');
25
+ }
42
26
  }
43
- }
44
-
45
- let imageBuffer = buffer;
46
- if (mime !== 'image/png' && mime !== 'image/jpeg') {
47
- imageBuffer = await sharp(buffer).toFormat('png').toBuffer();
48
- mime = 'image/png';
49
- }
50
-
51
- return await Canvas.loadImage(imageBuffer);
27
+ else {
28
+ try {
29
+ const response = await fetch(src);
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch image: ${src} (Status: ${response.status})`);
32
+ }
33
+ buffer = await response.buffer();
34
+ const { fileTypeFromBuffer } = await import('file-type');
35
+ const typeData = await fileTypeFromBuffer(buffer);
36
+ if (typeData) {
37
+ mime = typeData.mime;
38
+ }
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Failed to process image from ${src}: ${error.message}`);
42
+ }
43
+ }
44
+ let imageBuffer = buffer;
45
+ if (mime !== 'image/png' && mime !== 'image/jpeg') {
46
+ imageBuffer = await sharp(buffer).toFormat('png').toBuffer();
47
+ mime = 'image/png';
48
+ }
49
+ const image = await Canvas.loadImage(imageBuffer);
50
+ // Store in cache
51
+ if (cache) {
52
+ cache.images.set(src, image);
53
+ }
54
+ return image;
52
55
  }
53
-
54
- async function srcToBase64(src) {
55
- if (src.indexOf('base64') >= 0) {
56
- return src.split('base64,')[1];
57
- }
58
- const res = await fetch(src);
59
- if (!res.ok) {
60
- throw new Error(`Failed to fetch: ${src} (Status: ${res.status})`);
61
- }
62
- const data = await res.buffer();
63
- return data.toString('base64');
56
+ export async function srcToBase64(src, cache = null) {
57
+ // For base64 caching, we use a different cache key to avoid collision with buffer cache
58
+ const base64CacheKey = `base64:${src}`;
59
+ // Check cache first
60
+ if (cache && cache.buffers.has(base64CacheKey)) {
61
+ return cache.buffers.get(base64CacheKey);
62
+ }
63
+ let base64;
64
+ if (src.indexOf('base64') >= 0) {
65
+ base64 = src.split('base64,')[1];
66
+ }
67
+ else {
68
+ const res = await fetch(src);
69
+ if (!res.ok) {
70
+ throw new Error(`Failed to fetch: ${src} (Status: ${res.status})`);
71
+ }
72
+ const data = await res.buffer();
73
+ base64 = data.toString('base64');
74
+ }
75
+ // Store in cache
76
+ if (cache) {
77
+ cache.buffers.set(base64CacheKey, base64);
78
+ }
79
+ return base64;
64
80
  }
65
-
66
- async function srcToBuffer(src) {
67
- return Buffer.from(await srcToBase64(src), 'base64');
81
+ export async function srcToBuffer(src, cache = null) {
82
+ // Check if we have a cached Buffer for this source
83
+ if (cache && cache.buffers.has(src)) {
84
+ return cache.buffers.get(src);
85
+ }
86
+ const base64 = await srcToBase64(src, cache);
87
+ const buffer = Buffer.from(base64, 'base64');
88
+ // Cache the actual Buffer object so PDFKit can reuse it
89
+ if (cache) {
90
+ cache.buffers.set(src, buffer);
91
+ }
92
+ return buffer;
68
93
  }
69
-
70
- module.exports = {
71
- pxToPt,
72
- loadImage,
73
- srcToBase64,
74
- srcToBuffer,
75
- DPI,
76
- PIXEL_RATIO,
77
- parseColor,
78
- };
94
+ export { parseColor };
package/package.json CHANGED
@@ -1,38 +1,46 @@
1
1
  {
2
2
  "name": "@polotno/pdf-export",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Convert Polotno JSON into vector PDF",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./lib/index.js",
11
+ "types": "./lib/index.d.ts"
12
+ }
13
+ },
6
14
  "scripts": {
15
+ "build": "tsc",
7
16
  "test": "vitest",
8
17
  "test:update-snapshots": "vitest -u"
9
18
  },
10
19
  "author": "Anton Lavrenov",
11
20
  "files": [
12
- "index.js",
13
- "README.md",
14
- "lib"
21
+ "lib",
22
+ "README.md"
15
23
  ],
16
24
  "dependencies": {
17
- "@canvas/image": "^2.0.0",
18
- "@file-type/xml": "^0.4.3",
19
- "canvas": "^3.1.1",
25
+ "canvas": "^3.2.0",
20
26
  "file-type": "^21.0.0",
21
27
  "get-urls": "^12.1.0",
22
- "konva": "^9.3.20",
28
+ "konva": "^10.0.8",
23
29
  "node-fetch": "^3.3.2",
24
30
  "parse-color": "^1.0.0",
31
+ "pdfkit": "^0.17.2",
25
32
  "pdf2pic": "^3.2.0",
26
- "pdfkit": "^0.17.1",
27
- "polotno": "^2.24.1",
28
- "sharp": "^0.34.2",
33
+ "polotno": "^2.29.5",
34
+ "sharp": "^0.34.4",
29
35
  "svg-to-pdfkit": "^0.1.8",
30
36
  "xmldom": "^0.6.0"
31
37
  },
32
38
  "devDependencies": {
33
- "@types/pdfkit": "^0.14.0",
39
+ "@types/node": "^24.9.1",
40
+ "@types/pdfkit": "^0.17.3",
34
41
  "jest-image-snapshot": "^6.5.1",
35
42
  "pdf-img-convert": "^2.0.0",
36
- "vitest": "^3.2.4"
43
+ "typescript": "~5.9.3",
44
+ "vitest": "^4.0.3"
37
45
  }
38
46
  }