@vizzly-testing/cli 0.22.1 → 0.22.2

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.
@@ -188,7 +188,9 @@ function createSimpleClient(serverUrl) {
188
188
  try {
189
189
  // If it's a string, assume it's a file path and send directly
190
190
  // Otherwise it's a Buffer, so convert to base64
191
- const image = typeof imageBuffer === 'string' ? imageBuffer : imageBuffer.toString('base64');
191
+ let isFilePath = typeof imageBuffer === 'string';
192
+ let image = isFilePath ? imageBuffer : imageBuffer.toString('base64');
193
+ let type = isFilePath ? 'file-path' : 'base64';
192
194
  const {
193
195
  status,
194
196
  json
@@ -196,6 +198,7 @@ function createSimpleClient(serverUrl) {
196
198
  buildId: getBuildId(),
197
199
  name,
198
200
  image,
201
+ type,
199
202
  properties: options,
200
203
  fullPage: options.fullPage || false
201
204
  }, DEFAULT_TIMEOUT_MS);
package/dist/sdk/index.js CHANGED
@@ -247,6 +247,7 @@ export class VizzlySDK extends EventEmitter {
247
247
  buildId,
248
248
  name,
249
249
  image: imageBase64,
250
+ type: 'base64',
250
251
  properties: options.properties || {}
251
252
  };
252
253
 
@@ -47,7 +47,7 @@ export const createApiHandler = (client, {
47
47
  let vizzlyDisabled = false;
48
48
  let screenshotCount = 0;
49
49
  let uploadPromises = [];
50
- const handleScreenshot = async (buildId, name, image, properties = {}) => {
50
+ const handleScreenshot = async (buildId, name, image, properties = {}, type) => {
51
51
  if (vizzlyDisabled) {
52
52
  output.debug('upload', `${name} (disabled)`);
53
53
  return {
@@ -73,8 +73,11 @@ export const createApiHandler = (client, {
73
73
  }
74
74
 
75
75
  // Support both base64 encoded images and file paths
76
+ // Use explicit type from client if provided (fast path), otherwise detect (slow path)
77
+ // Only accept valid type values to prevent invalid types from bypassing detection
76
78
  let imageBuffer;
77
- const inputType = detectImageInputType(image);
79
+ let validTypes = ['base64', 'file-path'];
80
+ const inputType = type && validTypes.includes(type) ? type : detectImageInputType(image);
78
81
  if (inputType === 'file-path') {
79
82
  // It's a file path - resolve and read the file
80
83
  const filePath = resolve(image.replace('file://', ''));
@@ -267,7 +267,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
267
267
  output.debug('tdd', `baseline: ${baseline.buildName}`);
268
268
  }
269
269
  };
270
- const handleScreenshot = async (_buildId, name, image, properties = {}) => {
270
+ const handleScreenshot = async (_buildId, name, image, properties = {}, type) => {
271
271
  // Validate and sanitize screenshot name
272
272
  let sanitizedName;
273
273
  try {
@@ -306,8 +306,11 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
306
306
 
307
307
  // Support both base64 encoded images and file paths
308
308
  // Vitest browser mode returns file paths, so we need to handle both
309
+ // Use explicit type from client if provided (fast path), otherwise detect (slow path)
310
+ // Only accept valid type values to prevent invalid types from bypassing detection
309
311
  let imageBuffer;
310
- const inputType = detectImageInputType(image);
312
+ let validTypes = ['base64', 'file-path'];
313
+ const inputType = type && validTypes.includes(type) ? type : detectImageInputType(image);
311
314
  if (inputType === 'file-path') {
312
315
  // It's a file path - resolve and read the file
313
316
  const filePath = resolve(image.replace('file://', ''));
@@ -31,7 +31,8 @@ export function createScreenshotRouter({
31
31
  buildId,
32
32
  name,
33
33
  properties,
34
- image
34
+ image,
35
+ type
35
36
  } = body;
36
37
  if (!name || !image) {
37
38
  sendError(res, 400, 'name and image are required');
@@ -40,7 +41,7 @@ export function createScreenshotRouter({
40
41
 
41
42
  // Use buildId from request body, or fall back to server's buildId
42
43
  const effectiveBuildId = buildId || defaultBuildId;
43
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
44
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type);
44
45
  sendJson(res, result.statusCode, result.body);
45
46
  return true;
46
47
  } catch (error) {
@@ -81,41 +81,46 @@ export function looksLikeFilePath(str) {
81
81
  return false;
82
82
  }
83
83
 
84
- // 0. Explicitly reject data URIs first (they contain : and / which would match path patterns)
84
+ // 0. Length check - file paths are short, base64 screenshots are huge
85
+ // Even the longest realistic file path is < 500 chars
86
+ // This makes detection O(1) for large base64 strings
87
+ // Use same threshold (1000) as detectImageInputType for consistency
88
+ if (str.length > 1000) {
89
+ return false;
90
+ }
91
+
92
+ // 1. Explicitly reject data URIs (they contain : and / which would match path patterns)
85
93
  if (str.startsWith('data:')) {
86
94
  return false;
87
95
  }
88
96
 
89
- // 1. Check for file:// URI scheme
97
+ // 2. Check for file:// URI scheme
90
98
  if (str.startsWith('file://')) {
91
99
  return true;
92
100
  }
93
101
 
94
- // 2. Check for absolute paths (Unix or Windows)
95
- // Unix: starts with /
96
- // Windows: starts with drive letter like C:\ or C:/
97
- if (str.startsWith('/') || /^[A-Za-z]:[/\\]/.test(str)) {
102
+ // 3. Windows absolute paths (C:\ or C:/) - base64 never starts with drive letter
103
+ if (/^[A-Za-z]:[/\\]/.test(str)) {
98
104
  return true;
99
105
  }
100
106
 
101
- // 3. Check for relative path indicators
102
- // ./ or ../ or .\ or ..\
107
+ // 4. Relative path indicators (./ or ../) - base64 never starts with dot
103
108
  if (/^\.\.?[/\\]/.test(str)) {
104
109
  return true;
105
110
  }
106
111
 
107
- // 4. Check for path separators (forward or back slash)
108
- // This catches paths like: subdirectory/file.png or subdirectory\file.png
109
- if (/[/\\]/.test(str)) {
110
- return true;
111
- }
112
-
113
112
  // 5. Check for common image file extensions
114
- // This catches simple filenames like: screenshot.png
115
- // Common extensions: png, jpg, jpeg, gif, webp, bmp, svg, tiff, ico
113
+ // This is the safest check - base64 never ends with .png/.jpg/etc
114
+ // Catches: /path/file.png, subdir/file.png, file.png
116
115
  if (/\.(png|jpe?g|gif|webp|bmp|svg|tiff?|ico)$/i.test(str)) {
117
116
  return true;
118
117
  }
118
+
119
+ // Note: We intentionally don't check for bare "/" prefix or "/" anywhere
120
+ // because JPEG base64 starts with "/9j/" which would false-positive
121
+ // File paths without extensions are rare for images and will fall through
122
+ // to base64 detection, which is acceptable for backwards compat
123
+
119
124
  return false;
120
125
  }
121
126
 
@@ -127,14 +132,13 @@ export function looksLikeFilePath(str) {
127
132
  * - 'file-path': A file path (relative or absolute)
128
133
  * - 'unknown': Cannot determine (ambiguous or invalid)
129
134
  *
130
- * Strategy:
131
- * 1. First check if it's valid base64 (can contain / which might look like paths)
132
- * 2. Then check if it looks like a file path (more specific patterns)
133
- * 3. Otherwise return 'unknown'
135
+ * Strategy (optimized for performance):
136
+ * 1. Check for data URI prefix first (O(1), definitive)
137
+ * 2. Check file path patterns (O(1) prefix/suffix checks)
138
+ * 3. For large non-path strings, assume base64 (skip expensive validation)
139
+ * 4. Only run full base64 validation on small ambiguous strings
134
140
  *
135
- * This order prevents base64 strings (which can contain /) from being
136
- * misidentified as file paths. Base64 validation is stricter and should
137
- * be checked first.
141
+ * This avoids O(n) regex validation on large screenshot buffers.
138
142
  *
139
143
  * @param {string} str - String to detect
140
144
  * @returns {'base64' | 'file-path' | 'unknown'} Detected input type
@@ -151,15 +155,26 @@ export function detectImageInputType(str) {
151
155
  return 'unknown';
152
156
  }
153
157
 
154
- // Check base64 FIRST - base64 strings can contain / which looks like paths
155
- // Base64 validation is stricter and more deterministic
156
- if (isBase64(str)) {
158
+ // 1. Data URIs are definitively base64 (O(1) check)
159
+ if (str.startsWith('data:')) {
157
160
  return 'base64';
158
161
  }
159
162
 
160
- // Then check file path - catch patterns that aren't valid base64
163
+ // 2. Check file path patterns (O(1) prefix/suffix checks)
161
164
  if (looksLikeFilePath(str)) {
162
165
  return 'file-path';
163
166
  }
167
+
168
+ // 3. For large strings that aren't file paths, assume base64
169
+ // Screenshots are typically 100KB+ as base64, file paths are <1KB
170
+ // Skip expensive O(n) validation for large strings
171
+ if (str.length > 1000) {
172
+ return 'base64';
173
+ }
174
+
175
+ // 4. Full validation only for small ambiguous strings
176
+ if (isBase64(str)) {
177
+ return 'base64';
178
+ }
164
179
  return 'unknown';
165
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -88,7 +88,6 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@vizzly-testing/honeydiff": "^0.8.0",
91
- "@vizzly-testing/static-site": "^0.0.11",
92
91
  "ansis": "^4.2.0",
93
92
  "commander": "^14.0.0",
94
93
  "cosmiconfig": "^9.0.0",