@vizzly-testing/cli 0.10.0 → 0.10.1
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/cli.js +4 -2
- package/dist/commands/init.js +120 -8
- package/dist/plugin-loader.js +32 -11
- package/dist/types/commands/init.d.ts +20 -1
- package/dist/types/utils/config-schema.d.ts +6 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -34,8 +34,9 @@ let logger = createComponentLogger('CLI', {
|
|
|
34
34
|
verbose: verboseMode || false
|
|
35
35
|
});
|
|
36
36
|
let container = await createServiceContainer(config);
|
|
37
|
+
let plugins = [];
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
plugins = await loadPlugins(configPath, config, logger);
|
|
39
40
|
for (let plugin of plugins) {
|
|
40
41
|
try {
|
|
41
42
|
// Add timeout protection for plugin registration (5 seconds)
|
|
@@ -58,7 +59,8 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
58
59
|
const globalOptions = program.opts();
|
|
59
60
|
await init({
|
|
60
61
|
...globalOptions,
|
|
61
|
-
...options
|
|
62
|
+
...options,
|
|
63
|
+
plugins
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
|
package/dist/commands/init.js
CHANGED
|
@@ -3,15 +3,19 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
5
5
|
import { createComponentLogger } from '../utils/logger-factory.js';
|
|
6
|
+
import { loadPlugins } from '../plugin-loader.js';
|
|
7
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
8
|
+
import { z } from 'zod';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Simple configuration setup for Vizzly CLI
|
|
9
12
|
*/
|
|
10
13
|
export class InitCommand {
|
|
11
|
-
constructor(logger) {
|
|
14
|
+
constructor(logger, plugins = []) {
|
|
12
15
|
this.logger = logger || createComponentLogger('INIT', {
|
|
13
16
|
level: 'info'
|
|
14
17
|
});
|
|
18
|
+
this.plugins = plugins;
|
|
15
19
|
}
|
|
16
20
|
async run(options = {}) {
|
|
17
21
|
this.logger.info('🎯 Initializing Vizzly configuration...\n');
|
|
@@ -37,7 +41,7 @@ export class InitCommand {
|
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
async generateConfigFile(configPath) {
|
|
40
|
-
|
|
44
|
+
let coreConfig = `export default {
|
|
41
45
|
// Server configuration (for run command)
|
|
42
46
|
server: {
|
|
43
47
|
port: 47392,
|
|
@@ -65,11 +69,103 @@ export class InitCommand {
|
|
|
65
69
|
// TDD configuration
|
|
66
70
|
tdd: {
|
|
67
71
|
openReport: false // Whether to auto-open HTML report in browser
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
}`;
|
|
73
|
+
|
|
74
|
+
// Add plugin configurations
|
|
75
|
+
let pluginConfigs = this.generatePluginConfigs();
|
|
76
|
+
if (pluginConfigs) {
|
|
77
|
+
coreConfig += ',\n\n' + pluginConfigs;
|
|
78
|
+
}
|
|
79
|
+
coreConfig += '\n};\n';
|
|
80
|
+
await fs.writeFile(configPath, coreConfig, 'utf8');
|
|
72
81
|
this.logger.info(`📄 Created vizzly.config.js`);
|
|
82
|
+
|
|
83
|
+
// Log discovered plugins
|
|
84
|
+
let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
|
|
85
|
+
if (pluginsWithConfig.length > 0) {
|
|
86
|
+
this.logger.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
87
|
+
pluginsWithConfig.forEach(p => {
|
|
88
|
+
this.logger.info(` - ${p.name}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate configuration sections for plugins
|
|
95
|
+
* @returns {string} Plugin config sections as formatted string
|
|
96
|
+
*/
|
|
97
|
+
generatePluginConfigs() {
|
|
98
|
+
let sections = [];
|
|
99
|
+
for (let plugin of this.plugins) {
|
|
100
|
+
if (plugin.configSchema) {
|
|
101
|
+
let configStr = this.formatPluginConfig(plugin);
|
|
102
|
+
if (configStr) {
|
|
103
|
+
sections.push(configStr);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return sections.length > 0 ? sections.join(',\n\n') : '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format a plugin's config schema as JavaScript code
|
|
112
|
+
* @param {Object} plugin - Plugin with configSchema
|
|
113
|
+
* @returns {string} Formatted config string
|
|
114
|
+
*/
|
|
115
|
+
formatPluginConfig(plugin) {
|
|
116
|
+
try {
|
|
117
|
+
// Validate config schema structure with Zod (defensive check)
|
|
118
|
+
let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
|
|
119
|
+
let configSchemaValidator = z.record(configValueSchema);
|
|
120
|
+
configSchemaValidator.parse(plugin.configSchema);
|
|
121
|
+
let configEntries = [];
|
|
122
|
+
for (let [key, value] of Object.entries(plugin.configSchema)) {
|
|
123
|
+
let formattedValue = this.formatValue(value, 1);
|
|
124
|
+
configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
|
|
125
|
+
}
|
|
126
|
+
return configEntries.join(',\n\n');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof z.ZodError) {
|
|
129
|
+
let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
130
|
+
this.logger.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
131
|
+
} else {
|
|
132
|
+
this.logger.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format a JavaScript value with proper indentation
|
|
140
|
+
* @param {*} value - Value to format
|
|
141
|
+
* @param {number} depth - Current indentation depth
|
|
142
|
+
* @returns {string} Formatted value
|
|
143
|
+
*/
|
|
144
|
+
formatValue(value, depth = 0) {
|
|
145
|
+
let indent = ' '.repeat(depth);
|
|
146
|
+
let nextIndent = ' '.repeat(depth + 1);
|
|
147
|
+
if (value === null) return 'null';
|
|
148
|
+
if (value === undefined) return 'undefined';
|
|
149
|
+
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
|
|
150
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
if (value.length === 0) return '[]';
|
|
153
|
+
let items = value.map(item => {
|
|
154
|
+
let formatted = this.formatValue(item, depth + 1);
|
|
155
|
+
return `${nextIndent}${formatted}`;
|
|
156
|
+
});
|
|
157
|
+
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
158
|
+
}
|
|
159
|
+
if (typeof value === 'object') {
|
|
160
|
+
let entries = Object.entries(value);
|
|
161
|
+
if (entries.length === 0) return '{}';
|
|
162
|
+
let props = entries.map(([k, v]) => {
|
|
163
|
+
let formatted = this.formatValue(v, depth + 1);
|
|
164
|
+
return `${nextIndent}${k}: ${formatted}`;
|
|
165
|
+
});
|
|
166
|
+
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
167
|
+
}
|
|
168
|
+
return String(value);
|
|
73
169
|
}
|
|
74
170
|
showNextSteps() {
|
|
75
171
|
this.logger.info('\n📚 Next steps:');
|
|
@@ -92,12 +188,28 @@ export class InitCommand {
|
|
|
92
188
|
|
|
93
189
|
// Export factory function for CLI
|
|
94
190
|
export function createInitCommand(options) {
|
|
95
|
-
const command = new InitCommand(options.logger);
|
|
191
|
+
const command = new InitCommand(options.logger, options.plugins);
|
|
96
192
|
return () => command.run(options);
|
|
97
193
|
}
|
|
98
194
|
|
|
99
195
|
// Simple export for direct CLI usage
|
|
100
196
|
export async function init(options = {}) {
|
|
101
|
-
|
|
197
|
+
let plugins = [];
|
|
198
|
+
|
|
199
|
+
// Try to load plugins if not provided
|
|
200
|
+
if (!options.plugins) {
|
|
201
|
+
try {
|
|
202
|
+
let config = await loadConfig(options.config, {});
|
|
203
|
+
let logger = createComponentLogger('INIT', {
|
|
204
|
+
level: 'debug'
|
|
205
|
+
});
|
|
206
|
+
plugins = await loadPlugins(options.config, config, logger);
|
|
207
|
+
} catch {
|
|
208
|
+
// Silent fail - plugins are optional for init
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
plugins = options.plugins;
|
|
212
|
+
}
|
|
213
|
+
const command = new InitCommand(null, plugins);
|
|
102
214
|
return await command.run(options);
|
|
103
215
|
}
|
package/dist/plugin-loader.js
CHANGED
|
@@ -2,6 +2,7 @@ import { glob } from 'glob';
|
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { resolve, dirname } from 'path';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
|
+
import { z } from 'zod';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Load and register plugins from node_modules and config
|
|
@@ -128,23 +129,43 @@ async function loadPlugin(pluginPath) {
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Zod schema for validating plugin structure
|
|
134
|
+
*/
|
|
135
|
+
const pluginSchema = z.object({
|
|
136
|
+
name: z.string().min(1, 'Plugin name is required'),
|
|
137
|
+
version: z.string().optional(),
|
|
138
|
+
register: z.function(z.tuple([z.any(), z.any()]), z.void()),
|
|
139
|
+
configSchema: z.record(z.any()).optional()
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Zod schema for validating plugin config values
|
|
144
|
+
* Supports: string, number, boolean, null, arrays, and nested objects
|
|
145
|
+
*/
|
|
146
|
+
const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
|
|
147
|
+
|
|
131
148
|
/**
|
|
132
149
|
* Validate plugin has required structure
|
|
133
150
|
* @param {Object} plugin - Plugin object
|
|
134
151
|
* @throws {Error} If plugin structure is invalid
|
|
135
152
|
*/
|
|
136
153
|
function validatePluginStructure(plugin) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
try {
|
|
155
|
+
// Validate basic plugin structure
|
|
156
|
+
pluginSchema.parse(plugin);
|
|
157
|
+
|
|
158
|
+
// If configSchema exists, validate it contains valid config values
|
|
159
|
+
if (plugin.configSchema) {
|
|
160
|
+
let configSchemaValidator = z.record(configValueSchema);
|
|
161
|
+
configSchemaValidator.parse(plugin.configSchema);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof z.ZodError) {
|
|
165
|
+
let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
166
|
+
throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
148
169
|
}
|
|
149
170
|
}
|
|
150
171
|
|
|
@@ -5,10 +5,29 @@ export function init(options?: {}): Promise<void>;
|
|
|
5
5
|
* Simple configuration setup for Vizzly CLI
|
|
6
6
|
*/
|
|
7
7
|
export class InitCommand {
|
|
8
|
-
constructor(logger: any);
|
|
8
|
+
constructor(logger: any, plugins?: any[]);
|
|
9
9
|
logger: any;
|
|
10
|
+
plugins: any[];
|
|
10
11
|
run(options?: {}): Promise<void>;
|
|
11
12
|
generateConfigFile(configPath: any): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Generate configuration sections for plugins
|
|
15
|
+
* @returns {string} Plugin config sections as formatted string
|
|
16
|
+
*/
|
|
17
|
+
generatePluginConfigs(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Format a plugin's config schema as JavaScript code
|
|
20
|
+
* @param {Object} plugin - Plugin with configSchema
|
|
21
|
+
* @returns {string} Formatted config string
|
|
22
|
+
*/
|
|
23
|
+
formatPluginConfig(plugin: any): string;
|
|
24
|
+
/**
|
|
25
|
+
* Format a JavaScript value with proper indentation
|
|
26
|
+
* @param {*} value - Value to format
|
|
27
|
+
* @param {number} depth - Current indentation depth
|
|
28
|
+
* @returns {string} Formatted value
|
|
29
|
+
*/
|
|
30
|
+
formatValue(value: any, depth?: number): string;
|
|
12
31
|
showNextSteps(): void;
|
|
13
32
|
fileExists(filePath: any): Promise<boolean>;
|
|
14
33
|
}
|
|
@@ -35,14 +35,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
|
|
|
35
35
|
commit: z.ZodOptional<z.ZodString>;
|
|
36
36
|
message: z.ZodOptional<z.ZodString>;
|
|
37
37
|
}, "strip", z.ZodTypeAny, {
|
|
38
|
-
message?: string;
|
|
39
38
|
name?: string;
|
|
39
|
+
message?: string;
|
|
40
40
|
environment?: string;
|
|
41
41
|
branch?: string;
|
|
42
42
|
commit?: string;
|
|
43
43
|
}, {
|
|
44
|
-
message?: string;
|
|
45
44
|
name?: string;
|
|
45
|
+
message?: string;
|
|
46
46
|
environment?: string;
|
|
47
47
|
branch?: string;
|
|
48
48
|
commit?: string;
|
|
@@ -101,14 +101,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
|
|
|
101
101
|
commit: z.ZodOptional<z.ZodString>;
|
|
102
102
|
message: z.ZodOptional<z.ZodString>;
|
|
103
103
|
}, "strip", z.ZodTypeAny, {
|
|
104
|
-
message?: string;
|
|
105
104
|
name?: string;
|
|
105
|
+
message?: string;
|
|
106
106
|
environment?: string;
|
|
107
107
|
branch?: string;
|
|
108
108
|
commit?: string;
|
|
109
109
|
}, {
|
|
110
|
-
message?: string;
|
|
111
110
|
name?: string;
|
|
111
|
+
message?: string;
|
|
112
112
|
environment?: string;
|
|
113
113
|
branch?: string;
|
|
114
114
|
commit?: string;
|
|
@@ -167,14 +167,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
|
|
|
167
167
|
commit: z.ZodOptional<z.ZodString>;
|
|
168
168
|
message: z.ZodOptional<z.ZodString>;
|
|
169
169
|
}, "strip", z.ZodTypeAny, {
|
|
170
|
-
message?: string;
|
|
171
170
|
name?: string;
|
|
171
|
+
message?: string;
|
|
172
172
|
environment?: string;
|
|
173
173
|
branch?: string;
|
|
174
174
|
commit?: string;
|
|
175
175
|
}, {
|
|
176
|
-
message?: string;
|
|
177
176
|
name?: string;
|
|
177
|
+
message?: string;
|
|
178
178
|
environment?: string;
|
|
179
179
|
branch?: string;
|
|
180
180
|
commit?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Visual review platform for UI developers and designers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visual-testing",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"form-data": "^4.0.0",
|
|
85
85
|
"glob": "^11.0.3",
|
|
86
86
|
"odiff-bin": "^3.2.1",
|
|
87
|
-
"zod": "^3.
|
|
87
|
+
"zod": "^3.25.76"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@babel/cli": "^7.28.0",
|