@vizzly-testing/cli 0.31.0 → 0.32.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.
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 2D boxes that users have manually confirmed via the cloud UI.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
let REGION_CENTER_TOLERANCE = 10;
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Check if a diff cluster intersects with a region (2D box intersection)
|
|
14
16
|
*
|
|
@@ -41,13 +43,77 @@ export function clusterIntersectsRegion(cluster, region) {
|
|
|
41
43
|
let noOverlap = clusterX2 < region.x1 || clusterX1 > region.x2 || clusterY2 < region.y1 || clusterY1 > region.y2;
|
|
42
44
|
return !noOverlap;
|
|
43
45
|
}
|
|
46
|
+
function normalizeBoundingBox(cluster) {
|
|
47
|
+
let box = cluster?.boundingBox || cluster;
|
|
48
|
+
if (!box) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height)) {
|
|
52
|
+
return {
|
|
53
|
+
x1: box.x,
|
|
54
|
+
y1: box.y,
|
|
55
|
+
x2: box.x + box.width,
|
|
56
|
+
y2: box.y + box.height,
|
|
57
|
+
width: Math.abs(box.width),
|
|
58
|
+
height: Math.abs(box.height)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (Number.isFinite(box.x1) && Number.isFinite(box.y1) && Number.isFinite(box.x2) && Number.isFinite(box.y2)) {
|
|
62
|
+
let width = Math.abs(box.x2 - box.x1);
|
|
63
|
+
let height = Math.abs(box.y2 - box.y1);
|
|
64
|
+
return {
|
|
65
|
+
x1: box.x1,
|
|
66
|
+
y1: box.y1,
|
|
67
|
+
x2: box.x2,
|
|
68
|
+
y2: box.y2,
|
|
69
|
+
width,
|
|
70
|
+
height
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function getBoundingBoxCenter(box) {
|
|
76
|
+
return {
|
|
77
|
+
x: (box.x1 + box.x2) / 2,
|
|
78
|
+
y: (box.y1 + box.y2) / 2
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function getRegionLabel(region) {
|
|
82
|
+
return region.id || region.label || null;
|
|
83
|
+
}
|
|
84
|
+
function clusterMatchesRegion(cluster, region) {
|
|
85
|
+
let clusterBox = normalizeBoundingBox(cluster);
|
|
86
|
+
let regionBox = normalizeBoundingBox(region);
|
|
87
|
+
if (!clusterBox || !regionBox) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let clusterCenter = getBoundingBoxCenter(clusterBox);
|
|
91
|
+
let regionCenter = getBoundingBoxCenter(regionBox);
|
|
92
|
+
let distance = Math.sqrt((clusterCenter.x - regionCenter.x) ** 2 + (clusterCenter.y - regionCenter.y) ** 2);
|
|
93
|
+
return distance <= REGION_CENTER_TOLERANCE;
|
|
94
|
+
}
|
|
95
|
+
function estimateClusterPixels(cluster) {
|
|
96
|
+
if (Number.isFinite(cluster?.pixelCount)) {
|
|
97
|
+
return cluster.pixelCount;
|
|
98
|
+
}
|
|
99
|
+
let box = normalizeBoundingBox(cluster);
|
|
100
|
+
if (!box) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
return box.width * box.height * 0.5;
|
|
104
|
+
}
|
|
44
105
|
|
|
45
106
|
/**
|
|
46
|
-
* Calculate what percentage of
|
|
107
|
+
* Calculate what percentage of changed pixels match confirmed regions.
|
|
108
|
+
*
|
|
109
|
+
* This mirrors cloud dynamic-region approval: clusters match confirmed regions
|
|
110
|
+
* by center proximity, then coverage is weighted by changed pixels. A huge
|
|
111
|
+
* unmatched change should not be hidden just because several tiny clusters
|
|
112
|
+
* match confirmed regions.
|
|
47
113
|
*
|
|
48
114
|
* @param {Array} diffClusters - Array of diff clusters from honeydiff
|
|
49
115
|
* @param {Array} regions - Array of confirmed regions { x1, y1, x2, y2 }
|
|
50
|
-
* @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[] }}
|
|
116
|
+
* @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[], pixelsInRegions: number, totalPixels: number }}
|
|
51
117
|
*/
|
|
52
118
|
export function calculateRegionCoverage(diffClusters, regions) {
|
|
53
119
|
if (!diffClusters || diffClusters.length === 0) {
|
|
@@ -55,7 +121,9 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
55
121
|
coverage: 0,
|
|
56
122
|
clustersInRegions: 0,
|
|
57
123
|
totalClusters: 0,
|
|
58
|
-
matchedRegions: []
|
|
124
|
+
matchedRegions: [],
|
|
125
|
+
pixelsInRegions: 0,
|
|
126
|
+
totalPixels: 0
|
|
59
127
|
};
|
|
60
128
|
}
|
|
61
129
|
if (!regions || regions.length === 0) {
|
|
@@ -63,35 +131,41 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
63
131
|
coverage: 0,
|
|
64
132
|
clustersInRegions: 0,
|
|
65
133
|
totalClusters: diffClusters.length,
|
|
66
|
-
matchedRegions: []
|
|
134
|
+
matchedRegions: [],
|
|
135
|
+
pixelsInRegions: 0,
|
|
136
|
+
totalPixels: 0
|
|
67
137
|
};
|
|
68
138
|
}
|
|
69
139
|
let clustersInRegions = 0;
|
|
70
140
|
let matchedRegionIds = new Set();
|
|
141
|
+
let pixelsInRegions = 0;
|
|
142
|
+
let totalPixels = 0;
|
|
71
143
|
for (let cluster of diffClusters) {
|
|
72
|
-
|
|
73
|
-
|
|
144
|
+
let pixelCount = estimateClusterPixels(cluster);
|
|
145
|
+
totalPixels += pixelCount;
|
|
146
|
+
let matchedAnyRegion = false;
|
|
74
147
|
for (let region of regions) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
matchedRegionIds.add(
|
|
80
|
-
} else if (region.label) {
|
|
81
|
-
matchedRegionIds.add(region.label);
|
|
148
|
+
if (clusterMatchesRegion(cluster, region)) {
|
|
149
|
+
matchedAnyRegion = true;
|
|
150
|
+
let regionLabel = getRegionLabel(region);
|
|
151
|
+
if (regionLabel) {
|
|
152
|
+
matchedRegionIds.add(regionLabel);
|
|
82
153
|
}
|
|
83
154
|
}
|
|
84
155
|
}
|
|
85
|
-
if (
|
|
156
|
+
if (matchedAnyRegion) {
|
|
86
157
|
clustersInRegions++;
|
|
158
|
+
pixelsInRegions += pixelCount;
|
|
87
159
|
}
|
|
88
160
|
}
|
|
89
|
-
let coverage =
|
|
161
|
+
let coverage = totalPixels > 0 ? pixelsInRegions / totalPixels : 0;
|
|
90
162
|
return {
|
|
91
163
|
coverage,
|
|
92
164
|
clustersInRegions,
|
|
93
165
|
totalClusters: diffClusters.length,
|
|
94
|
-
matchedRegions: [...matchedRegionIds]
|
|
166
|
+
matchedRegions: [...matchedRegionIds],
|
|
167
|
+
pixelsInRegions,
|
|
168
|
+
totalPixels
|
|
95
169
|
};
|
|
96
170
|
}
|
|
97
171
|
|
|
@@ -99,17 +173,31 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
99
173
|
* Determine if a comparison should auto-pass based on region coverage
|
|
100
174
|
*
|
|
101
175
|
* Unlike hotspots which require confidence scoring, user-defined regions
|
|
102
|
-
* are already confirmed by humans
|
|
176
|
+
* are already confirmed by humans. Cloud still requires strong structural
|
|
177
|
+
* similarity so a layout shift cannot hide inside a confirmed box.
|
|
103
178
|
*
|
|
104
179
|
* @param {Array} regions - Confirmed regions (already filtered to confirmed status)
|
|
105
180
|
* @param {{ coverage: number }} coverageResult - Result from calculateRegionCoverage
|
|
181
|
+
* @param {Object} options - Approval thresholds
|
|
182
|
+
* @param {number|null} options.ssimScore - Honeydiff SSIM/perceptual score
|
|
183
|
+
* @param {number} options.coverageThreshold - Required region coverage
|
|
184
|
+
* @param {number} options.ssimThreshold - Required structural similarity
|
|
106
185
|
* @returns {boolean} True if diff should auto-pass as region-filtered
|
|
107
186
|
*/
|
|
108
|
-
export function shouldAutoApproveFromRegions(regions, coverageResult) {
|
|
187
|
+
export function shouldAutoApproveFromRegions(regions, coverageResult, options = {}) {
|
|
109
188
|
if (!regions || regions.length === 0 || !coverageResult) {
|
|
110
189
|
return false;
|
|
111
190
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
let {
|
|
192
|
+
ssimScore = null,
|
|
193
|
+
coverageThreshold = 0.9,
|
|
194
|
+
ssimThreshold = 0.95
|
|
195
|
+
} = options;
|
|
196
|
+
if (coverageResult.coverage < coverageThreshold) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (ssimScore === null || ssimScore === undefined) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return ssimScore >= ssimThreshold;
|
|
115
203
|
}
|
|
@@ -32,6 +32,9 @@ export async function compareImages(baselinePath, currentPath, diffPath, options
|
|
|
32
32
|
diffPath,
|
|
33
33
|
overwrite: true,
|
|
34
34
|
includeClusters: true,
|
|
35
|
+
includeSSIM: true,
|
|
36
|
+
includeGMSD: true,
|
|
37
|
+
clusterMerge: true,
|
|
35
38
|
minClusterSize
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -141,6 +144,7 @@ export function buildFailedComparison(params) {
|
|
|
141
144
|
regionData
|
|
142
145
|
} = params;
|
|
143
146
|
let diffClusters = honeydiffResult.diffClusters || [];
|
|
147
|
+
let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
|
|
144
148
|
let isFiltered = false;
|
|
145
149
|
let filterReason = 'pixel-diff';
|
|
146
150
|
|
|
@@ -150,7 +154,9 @@ export function buildFailedComparison(params) {
|
|
|
150
154
|
let confirmedRegions = regionData?.confirmed || [];
|
|
151
155
|
if (confirmedRegions.length > 0 && diffClusters.length > 0) {
|
|
152
156
|
regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
|
|
153
|
-
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage
|
|
157
|
+
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
|
|
158
|
+
ssimScore
|
|
159
|
+
})) {
|
|
154
160
|
isRegionFiltered = true;
|
|
155
161
|
isFiltered = true;
|
|
156
162
|
filterReason = 'region-filtered';
|
|
@@ -198,8 +204,11 @@ export function buildFailedComparison(params) {
|
|
|
198
204
|
coverage: regionCoverage.coverage,
|
|
199
205
|
clustersInRegions: regionCoverage.clustersInRegions,
|
|
200
206
|
totalClusters: regionCoverage.totalClusters,
|
|
207
|
+
pixelsInRegions: regionCoverage.pixelsInRegions,
|
|
208
|
+
totalPixels: regionCoverage.totalPixels,
|
|
201
209
|
matchedRegions: regionCoverage.matchedRegions,
|
|
202
210
|
confirmedCount: confirmedRegions.length,
|
|
211
|
+
ssimScore,
|
|
203
212
|
isFiltered: isRegionFiltered
|
|
204
213
|
} : null,
|
|
205
214
|
// Include confirmed regions for visualization in UI
|
package/dist/tdd/tdd-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
12
|
-
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
12
|
+
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getComparisonContext as defaultGetComparisonContext, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
13
13
|
import { NetworkError } from '../errors/vizzly-error.js';
|
|
14
14
|
import { colors as defaultColors } from '../utils/colors.js';
|
|
15
15
|
import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
|
|
@@ -58,6 +58,7 @@ export class TddService {
|
|
|
58
58
|
getTddBaselines: defaultGetTddBaselines,
|
|
59
59
|
getBuilds: defaultGetBuilds,
|
|
60
60
|
getComparison: defaultGetComparison,
|
|
61
|
+
getComparisonContext: defaultGetComparisonContext,
|
|
61
62
|
getBatchHotspots: defaultGetBatchHotspots,
|
|
62
63
|
fetchWithTimeout: defaultFetchWithTimeout,
|
|
63
64
|
getDefaultBranch: defaultGetDefaultBranch
|
|
@@ -169,6 +170,7 @@ export class TddService {
|
|
|
169
170
|
getTddBaselines,
|
|
170
171
|
getBuilds,
|
|
171
172
|
getComparison,
|
|
173
|
+
getComparisonContext,
|
|
172
174
|
clearBaselineData,
|
|
173
175
|
generateScreenshotSignature,
|
|
174
176
|
generateBaselineFilename,
|
|
@@ -221,10 +223,19 @@ export class TddService {
|
|
|
221
223
|
output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
|
|
222
224
|
}
|
|
223
225
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
226
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
224
227
|
} else if (comparisonId) {
|
|
225
228
|
// Handle specific comparison download
|
|
226
229
|
output.info(`Using comparison: ${comparisonId}`);
|
|
227
230
|
let comparison = await getComparison(this.client, comparisonId);
|
|
231
|
+
let comparisonContext = null;
|
|
232
|
+
if (!comparison.dynamic_content && getComparisonContext) {
|
|
233
|
+
try {
|
|
234
|
+
comparisonContext = await getComparisonContext(this.client, comparisonId);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
output.debug('tdd', `comparison context unavailable for ${comparisonId}: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
228
239
|
if (!comparison.baseline_screenshot) {
|
|
229
240
|
throw new Error(`Comparison ${comparisonId} has no baseline screenshot. This comparison may be a "new" screenshot.`);
|
|
230
241
|
}
|
|
@@ -255,6 +266,11 @@ export class TddService {
|
|
|
255
266
|
}
|
|
256
267
|
}
|
|
257
268
|
let screenshotName = comparison.baseline_name || comparison.current_name;
|
|
269
|
+
let dynamicContent = comparison.dynamic_content || {
|
|
270
|
+
hotspot_analysis: comparisonContext?.history?.hotspot_analysis,
|
|
271
|
+
confirmed_regions: comparisonContext?.history?.confirmed_regions
|
|
272
|
+
};
|
|
273
|
+
this.saveDynamicMetadataForScreenshot(screenshotName, dynamicContent);
|
|
258
274
|
let signature = generateScreenshotSignature(screenshotName, screenshotProperties, this.signatureProperties);
|
|
259
275
|
let filename = generateBaselineFilename(screenshotName, signature);
|
|
260
276
|
baselineBuild = {
|
|
@@ -294,16 +310,7 @@ export class TddService {
|
|
|
294
310
|
}
|
|
295
311
|
baselineBuild = apiResponse.build;
|
|
296
312
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
297
|
-
|
|
298
|
-
// Store bundled hotspots and regions from API response
|
|
299
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
300
|
-
this.hotspotData = apiResponse.hotspots;
|
|
301
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
302
|
-
}
|
|
303
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
304
|
-
this.regionData = apiResponse.regions;
|
|
305
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
306
|
-
}
|
|
313
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
307
314
|
}
|
|
308
315
|
let buildDetails = baselineBuild;
|
|
309
316
|
if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
|
|
@@ -488,6 +495,49 @@ export class TddService {
|
|
|
488
495
|
throw error;
|
|
489
496
|
}
|
|
490
497
|
}
|
|
498
|
+
saveDynamicMetadataFromBaselineResponse(apiResponse = {}) {
|
|
499
|
+
let {
|
|
500
|
+
saveHotspotMetadata,
|
|
501
|
+
saveRegionMetadata
|
|
502
|
+
} = this._deps;
|
|
503
|
+
let hasHotspots = Object.keys(apiResponse.hotspots || {}).length > 0;
|
|
504
|
+
let hasRegions = Object.keys(apiResponse.regions || {}).length > 0;
|
|
505
|
+
if (hasHotspots) {
|
|
506
|
+
this.hotspotData = apiResponse.hotspots;
|
|
507
|
+
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
508
|
+
}
|
|
509
|
+
if (hasRegions) {
|
|
510
|
+
this.regionData = apiResponse.regions;
|
|
511
|
+
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
saveDynamicMetadataForScreenshot(screenshotName, dynamicContent = {}) {
|
|
515
|
+
if (!screenshotName) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
let hotspotAnalysis = dynamicContent.hotspot_analysis;
|
|
519
|
+
let confirmedRegions = dynamicContent.confirmed_regions || [];
|
|
520
|
+
let hasHotspotAnalysis = hotspotAnalysis?.total_builds_analyzed > 0;
|
|
521
|
+
let hasConfirmedRegions = confirmedRegions.length > 0;
|
|
522
|
+
let summary = {
|
|
523
|
+
screenshots: 1,
|
|
524
|
+
hotspots: hasHotspotAnalysis ? 1 : 0,
|
|
525
|
+
regions: hasConfirmedRegions ? 1 : 0,
|
|
526
|
+
total_regions: confirmedRegions.length
|
|
527
|
+
};
|
|
528
|
+
this.saveDynamicMetadataFromBaselineResponse({
|
|
529
|
+
hotspots: hasHotspotAnalysis ? {
|
|
530
|
+
[screenshotName]: hotspotAnalysis
|
|
531
|
+
} : {},
|
|
532
|
+
regions: hasConfirmedRegions ? {
|
|
533
|
+
[screenshotName]: {
|
|
534
|
+
confirmed: confirmedRegions,
|
|
535
|
+
candidates: []
|
|
536
|
+
}
|
|
537
|
+
} : {},
|
|
538
|
+
summary
|
|
539
|
+
});
|
|
540
|
+
}
|
|
491
541
|
|
|
492
542
|
/**
|
|
493
543
|
* Process already-fetched baseline data (for use when caller handles auth)
|
|
@@ -526,20 +576,7 @@ export class TddService {
|
|
|
526
576
|
}
|
|
527
577
|
}
|
|
528
578
|
let baselineBuild = apiResponse.build;
|
|
529
|
-
|
|
530
|
-
// Store bundled hotspots and regions from API response
|
|
531
|
-
let {
|
|
532
|
-
saveHotspotMetadata,
|
|
533
|
-
saveRegionMetadata
|
|
534
|
-
} = this._deps;
|
|
535
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
536
|
-
this.hotspotData = apiResponse.hotspots;
|
|
537
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
538
|
-
}
|
|
539
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
540
|
-
this.regionData = apiResponse.regions;
|
|
541
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
542
|
-
}
|
|
579
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
543
580
|
if (baselineBuild.status === 'failed') {
|
|
544
581
|
output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
|
|
545
582
|
return await this.handleLocalBaselines();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Visual review for agents and teams",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -90,13 +90,15 @@
|
|
|
90
90
|
"registry": "https://registry.npmjs.org/"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@vizzly-testing/honeydiff": "^0.10.
|
|
93
|
+
"@vizzly-testing/honeydiff": "^0.10.3",
|
|
94
94
|
"ansis": "^4.2.0",
|
|
95
95
|
"commander": "^14.0.0",
|
|
96
96
|
"cosmiconfig": "^9.0.0",
|
|
97
97
|
"dotenv": "^17.2.1",
|
|
98
98
|
"form-data": "^4.0.0",
|
|
99
99
|
"glob": "^13.0.0",
|
|
100
|
+
"react": "^19.1.1",
|
|
101
|
+
"react-dom": "^19.1.1",
|
|
100
102
|
"zod": "^4.1.12"
|
|
101
103
|
},
|
|
102
104
|
"tsd": {
|
|
@@ -128,8 +130,6 @@
|
|
|
128
130
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
129
131
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
130
132
|
"postcss": "^8.5.6",
|
|
131
|
-
"react": "^19.1.1",
|
|
132
|
-
"react-dom": "^19.1.1",
|
|
133
133
|
"rimraf": "^6.0.1",
|
|
134
134
|
"tailwindcss": "^4.1.13",
|
|
135
135
|
"tsd": "^0.33.0",
|