@vizzly-testing/cli 0.30.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.
- package/README.md +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/region-coverage.js +109 -21
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/comparison-service.js +10 -1
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +144 -138
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/dist/plugin-loader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { glob } from 'glob';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -12,14 +12,14 @@ import * as output from './utils/output.js';
|
|
|
12
12
|
* @returns {Promise<Array>} Array of loaded plugins
|
|
13
13
|
*/
|
|
14
14
|
export async function loadPlugins(configPath, config) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let plugins = [];
|
|
16
|
+
let loadedNames = new Set();
|
|
17
17
|
|
|
18
18
|
// 1. Auto-discover plugins from @vizzly-testing/* packages
|
|
19
|
-
|
|
20
|
-
for (
|
|
19
|
+
let discoveredPlugins = await discoverInstalledPlugins();
|
|
20
|
+
for (let pluginInfo of discoveredPlugins) {
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
let plugin = await loadPlugin(pluginInfo.path);
|
|
23
23
|
if (plugin && !loadedNames.has(plugin.name)) {
|
|
24
24
|
plugins.push(plugin);
|
|
25
25
|
loadedNames.add(plugin.name);
|
|
@@ -32,15 +32,15 @@ export async function loadPlugins(configPath, config) {
|
|
|
32
32
|
|
|
33
33
|
// 2. Load explicit plugins from config
|
|
34
34
|
if (config?.plugins && Array.isArray(config.plugins)) {
|
|
35
|
-
for (
|
|
35
|
+
for (let pluginSpec of config.plugins) {
|
|
36
36
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
let pluginPath = resolvePluginPath(pluginSpec, configPath);
|
|
38
|
+
let plugin = await loadPlugin(pluginPath);
|
|
39
39
|
if (plugin && !loadedNames.has(plugin.name)) {
|
|
40
40
|
plugins.push(plugin);
|
|
41
41
|
loadedNames.add(plugin.name);
|
|
42
42
|
} else if (plugin && loadedNames.has(plugin.name)) {
|
|
43
|
-
|
|
43
|
+
let existingPlugin = plugins.find(p => p.name === plugin.name);
|
|
44
44
|
output.warn(`Plugin ${plugin.name} already loaded (v${existingPlugin.version || 'unknown'}), ` + `skipping v${plugin.version || 'unknown'} from config`);
|
|
45
45
|
}
|
|
46
46
|
} catch (error) {
|
|
@@ -55,39 +55,24 @@ export async function loadPlugins(configPath, config) {
|
|
|
55
55
|
* Discover installed plugins from node_modules/@vizzly-testing/*
|
|
56
56
|
* @returns {Promise<Array>} Array of plugin info objects
|
|
57
57
|
*/
|
|
58
|
-
async function discoverInstalledPlugins() {
|
|
59
|
-
|
|
58
|
+
export async function discoverInstalledPlugins() {
|
|
59
|
+
let plugins = [];
|
|
60
60
|
try {
|
|
61
61
|
// Find all @vizzly-testing packages
|
|
62
|
-
|
|
62
|
+
let packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
|
|
63
63
|
cwd: process.cwd(),
|
|
64
64
|
absolute: true
|
|
65
65
|
});
|
|
66
|
-
for (
|
|
66
|
+
for (let pkgPath of packageJsonPaths) {
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
let packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
69
69
|
|
|
70
70
|
// Check if package has a plugin field
|
|
71
71
|
// Support both new `vizzlyPlugin` and legacy `vizzly.plugin` for backwards compatibility
|
|
72
|
-
|
|
72
|
+
let pluginField = getPluginField(packageJson);
|
|
73
73
|
if (pluginField) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Security: Ensure plugin path is relative and doesn't traverse up
|
|
77
|
-
if (pluginRelativePath.startsWith('/') || pluginRelativePath.includes('..')) {
|
|
78
|
-
output.warn(`Invalid plugin path in ${packageJson.name}: path must be relative and cannot traverse directories`);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Resolve plugin path relative to package directory
|
|
83
|
-
const packageDir = dirname(pkgPath);
|
|
84
|
-
const pluginPath = resolve(packageDir, pluginRelativePath);
|
|
85
|
-
|
|
86
|
-
// Additional security: Ensure resolved path is still within package directory
|
|
87
|
-
if (!pluginPath.startsWith(packageDir)) {
|
|
88
|
-
output.warn(`Plugin path escapes package directory: ${packageJson.name}`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
74
|
+
let packageDir = dirname(pkgPath);
|
|
75
|
+
let pluginPath = resolvePackagePluginPath(packageJson.name, packageDir, pluginField);
|
|
91
76
|
plugins.push({
|
|
92
77
|
packageName: packageJson.name,
|
|
93
78
|
path: pluginPath
|
|
@@ -102,28 +87,45 @@ async function discoverInstalledPlugins() {
|
|
|
102
87
|
}
|
|
103
88
|
return plugins;
|
|
104
89
|
}
|
|
90
|
+
function getPluginField(packageJson) {
|
|
91
|
+
return packageJson.vizzlyPlugin || packageJson.vizzly?.plugin;
|
|
92
|
+
}
|
|
93
|
+
export function resolvePackagePluginPath(packageName, packageDir, pluginRelativePath) {
|
|
94
|
+
if (typeof pluginRelativePath !== 'string' || pluginRelativePath === '') {
|
|
95
|
+
throw new Error(`Invalid plugin path in ${packageName}: path must be a non-empty string`);
|
|
96
|
+
}
|
|
97
|
+
if (isAbsolute(pluginRelativePath)) {
|
|
98
|
+
throw new Error(`Invalid plugin path in ${packageName}: path must be relative`);
|
|
99
|
+
}
|
|
100
|
+
let pluginPath = resolve(packageDir, pluginRelativePath);
|
|
101
|
+
let relativePath = relative(packageDir, pluginPath);
|
|
102
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
103
|
+
throw new Error(`Invalid plugin path in ${packageName}: path cannot escape package directory`);
|
|
104
|
+
}
|
|
105
|
+
return pluginPath;
|
|
106
|
+
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
109
|
* Load a plugin from a file path
|
|
108
110
|
* @param {string} pluginPath - Path to plugin file
|
|
109
111
|
* @returns {Promise<Object|null>} Loaded plugin or null
|
|
110
112
|
*/
|
|
111
|
-
async function loadPlugin(pluginPath) {
|
|
113
|
+
export async function loadPlugin(pluginPath) {
|
|
112
114
|
try {
|
|
113
115
|
// Convert to file URL for ESM import
|
|
114
|
-
|
|
116
|
+
let pluginUrl = pathToFileURL(pluginPath).href;
|
|
115
117
|
|
|
116
118
|
// Dynamic import
|
|
117
|
-
|
|
119
|
+
let pluginModule = await import(pluginUrl);
|
|
118
120
|
|
|
119
121
|
// Get the default export
|
|
120
|
-
|
|
122
|
+
let plugin = pluginModule.default || pluginModule;
|
|
121
123
|
|
|
122
124
|
// Validate plugin structure
|
|
123
125
|
validatePluginStructure(plugin);
|
|
124
126
|
return plugin;
|
|
125
127
|
} catch (error) {
|
|
126
|
-
|
|
128
|
+
let newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
|
|
127
129
|
newError.cause = error;
|
|
128
130
|
throw newError;
|
|
129
131
|
}
|
|
@@ -156,7 +158,7 @@ function validatePluginStructure(plugin) {
|
|
|
156
158
|
// configSchema is optional and primarily for documentation
|
|
157
159
|
} catch (error) {
|
|
158
160
|
if (error instanceof z.ZodError) {
|
|
159
|
-
|
|
161
|
+
let messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
160
162
|
throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
|
|
161
163
|
}
|
|
162
164
|
throw error;
|
|
@@ -169,19 +171,23 @@ function validatePluginStructure(plugin) {
|
|
|
169
171
|
* @param {string|null} configPath - Path to config file
|
|
170
172
|
* @returns {string} Resolved plugin path
|
|
171
173
|
*/
|
|
172
|
-
function resolvePluginPath(pluginSpec, configPath) {
|
|
174
|
+
export function resolvePluginPath(pluginSpec, configPath) {
|
|
175
|
+
if (typeof pluginSpec !== 'string' || pluginSpec === '') {
|
|
176
|
+
throw new Error('Plugin spec must be a non-empty string');
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
// If it's a package name (starts with @ or is alphanumeric), try to resolve from node_modules
|
|
174
180
|
if (pluginSpec.startsWith('@') || /^[a-zA-Z0-9-]+$/.test(pluginSpec)) {
|
|
175
181
|
// Try to resolve as a package
|
|
176
182
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
let packageJsonPath = resolve(process.cwd(), 'node_modules', pluginSpec, 'package.json');
|
|
184
|
+
let packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
179
185
|
|
|
180
186
|
// Support both new `vizzlyPlugin` and legacy `vizzly.plugin`
|
|
181
|
-
|
|
187
|
+
let pluginField = getPluginField(packageJson);
|
|
182
188
|
if (pluginField) {
|
|
183
|
-
|
|
184
|
-
return
|
|
189
|
+
let packageDir = dirname(packageJsonPath);
|
|
190
|
+
return resolvePackagePluginPath(packageJson.name || pluginSpec, packageDir, pluginField);
|
|
185
191
|
}
|
|
186
192
|
throw new Error('Package does not specify a vizzlyPlugin or vizzly.plugin field');
|
|
187
193
|
} catch (error) {
|
|
@@ -192,10 +198,10 @@ function resolvePluginPath(pluginSpec, configPath) {
|
|
|
192
198
|
// Otherwise treat as a file path
|
|
193
199
|
if (configPath) {
|
|
194
200
|
// Resolve relative to config file
|
|
195
|
-
|
|
201
|
+
let configDir = dirname(configPath);
|
|
196
202
|
return resolve(configDir, pluginSpec);
|
|
197
|
-
} else {
|
|
198
|
-
// Resolve relative to cwd
|
|
199
|
-
return resolve(process.cwd(), pluginSpec);
|
|
200
203
|
}
|
|
204
|
+
|
|
205
|
+
// Resolve relative to cwd
|
|
206
|
+
return resolve(process.cwd(), pluginSpec);
|
|
201
207
|
}
|
package/dist/project/core.js
CHANGED
|
@@ -67,8 +67,8 @@ export function buildBuildsUrl(projectSlug, options = {}) {
|
|
|
67
67
|
* @param {string} [tokenId] - Optional token ID for specific token operations
|
|
68
68
|
* @returns {string} API URL path
|
|
69
69
|
*/
|
|
70
|
-
export function buildTokensUrl(
|
|
71
|
-
let base = `/api/
|
|
70
|
+
export function buildTokensUrl(_organizationSlug, projectSlug, tokenId) {
|
|
71
|
+
let base = `/api/project/${projectSlug}/tokens`;
|
|
72
72
|
return tokenId ? `${base}/${tokenId}` : base;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -264,7 +264,8 @@ export async function createProjectToken(apiClient, projectSlug, organizationSlu
|
|
|
264
264
|
let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
|
|
265
265
|
method: 'POST',
|
|
266
266
|
headers: {
|
|
267
|
-
'Content-Type': 'application/json'
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
...buildOrgHeader(organizationSlug)
|
|
268
269
|
},
|
|
269
270
|
body: JSON.stringify(tokenData)
|
|
270
271
|
});
|
|
@@ -287,7 +288,8 @@ export async function listProjectTokens(apiClient, projectSlug, organizationSlug
|
|
|
287
288
|
}
|
|
288
289
|
try {
|
|
289
290
|
let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
|
|
290
|
-
method: 'GET'
|
|
291
|
+
method: 'GET',
|
|
292
|
+
headers: buildOrgHeader(organizationSlug)
|
|
291
293
|
});
|
|
292
294
|
return extractTokens(response);
|
|
293
295
|
} catch (error) {
|
|
@@ -309,7 +311,8 @@ export async function revokeProjectToken(apiClient, projectSlug, organizationSlu
|
|
|
309
311
|
}
|
|
310
312
|
try {
|
|
311
313
|
await apiClient.request(buildTokensUrl(organizationSlug, projectSlug, tokenId), {
|
|
312
|
-
method: 'DELETE'
|
|
314
|
+
method: 'DELETE',
|
|
315
|
+
headers: buildOrgHeader(organizationSlug)
|
|
313
316
|
});
|
|
314
317
|
} catch (error) {
|
|
315
318
|
throw buildTokenRevokeError(error);
|