@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- package/dist/services/tdd-service.js +0 -1437
package/dist/commands/whoami.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Shows current user and authentication status
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
|
|
7
7
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
8
|
-
import { getAuthTokens } from '../utils/global-config.js';
|
|
9
8
|
import * as output from '../utils/output.js';
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -21,16 +20,17 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
21
20
|
});
|
|
22
21
|
try {
|
|
23
22
|
// Check if user is logged in
|
|
24
|
-
|
|
23
|
+
let auth = await getAuthTokens();
|
|
25
24
|
if (!auth || !auth.accessToken) {
|
|
26
25
|
if (globalOptions.json) {
|
|
27
26
|
output.data({
|
|
28
27
|
authenticated: false
|
|
29
28
|
});
|
|
30
29
|
} else {
|
|
31
|
-
output.
|
|
30
|
+
output.header('whoami');
|
|
31
|
+
output.print(' Not logged in');
|
|
32
32
|
output.blank();
|
|
33
|
-
output.
|
|
33
|
+
output.hint('Run "vizzly login" to authenticate');
|
|
34
34
|
}
|
|
35
35
|
output.cleanup();
|
|
36
36
|
return;
|
|
@@ -38,10 +38,11 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
38
38
|
|
|
39
39
|
// Get current user info
|
|
40
40
|
output.startSpinner('Fetching user information...');
|
|
41
|
-
|
|
41
|
+
let client = createAuthClient({
|
|
42
42
|
baseUrl: options.apiUrl || getApiUrl()
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
let tokenStore = createTokenStore();
|
|
45
|
+
let response = await whoami(client, tokenStore);
|
|
45
46
|
output.stopSpinner();
|
|
46
47
|
|
|
47
48
|
// Output in JSON mode
|
|
@@ -57,36 +58,39 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// Human-readable output
|
|
60
|
-
output.
|
|
61
|
-
output.blank();
|
|
61
|
+
output.header('whoami');
|
|
62
62
|
|
|
63
|
-
// Show user info
|
|
63
|
+
// Show user info using keyValue
|
|
64
64
|
if (response.user) {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
let userInfo = {
|
|
66
|
+
User: response.user.name || response.user.username,
|
|
67
|
+
Email: response.user.email
|
|
68
|
+
};
|
|
67
69
|
if (response.user.username) {
|
|
68
|
-
|
|
70
|
+
userInfo.Username = response.user.username;
|
|
69
71
|
}
|
|
70
72
|
if (globalOptions.verbose && response.user.id) {
|
|
71
|
-
|
|
73
|
+
userInfo['User ID'] = response.user.id;
|
|
72
74
|
}
|
|
75
|
+
output.keyValue(userInfo);
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
// Show organizations
|
|
78
|
+
// Show organizations as a list
|
|
76
79
|
if (response.organizations && response.organizations.length > 0) {
|
|
77
80
|
output.blank();
|
|
78
|
-
output.
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
if (org.slug) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
output.labelValue('Organizations', '');
|
|
82
|
+
let orgItems = response.organizations.map(org => {
|
|
83
|
+
let parts = [org.name];
|
|
84
|
+
if (org.slug) parts.push(`@${org.slug}`);
|
|
85
|
+
if (org.role) parts.push(`[${org.role}]`);
|
|
86
|
+
return parts.join(' ');
|
|
87
|
+
});
|
|
88
|
+
output.list(orgItems);
|
|
89
|
+
if (globalOptions.verbose) {
|
|
90
|
+
for (let org of response.organizations) {
|
|
91
|
+
if (org.id) {
|
|
92
|
+
output.hint(` ${org.name} ID: ${org.id}`);
|
|
93
|
+
}
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -94,29 +98,27 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
94
98
|
// Show token expiry info
|
|
95
99
|
if (auth.expiresAt) {
|
|
96
100
|
output.blank();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
let expiresAt = new Date(auth.expiresAt);
|
|
102
|
+
let now = new Date();
|
|
103
|
+
let msUntilExpiry = expiresAt.getTime() - now.getTime();
|
|
104
|
+
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
105
|
+
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
106
|
+
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
103
107
|
if (msUntilExpiry <= 0) {
|
|
104
108
|
output.warn('Token has expired');
|
|
105
|
-
output.
|
|
106
|
-
output.info('Run "vizzly login" to refresh your authentication');
|
|
109
|
+
output.hint('Run "vizzly login" to refresh your authentication');
|
|
107
110
|
} else if (daysUntilExpiry > 0) {
|
|
108
|
-
output.
|
|
111
|
+
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
|
|
109
112
|
} else if (hoursUntilExpiry > 0) {
|
|
110
|
-
output.
|
|
113
|
+
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
|
|
111
114
|
} else if (minutesUntilExpiry > 0) {
|
|
112
|
-
output.
|
|
115
|
+
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
|
|
113
116
|
} else {
|
|
114
117
|
output.warn('Token expires in less than a minute');
|
|
115
|
-
output.
|
|
116
|
-
output.info('Run "vizzly login" to refresh your authentication');
|
|
118
|
+
output.hint('Run "vizzly login" to refresh your authentication');
|
|
117
119
|
}
|
|
118
120
|
if (globalOptions.verbose) {
|
|
119
|
-
output.
|
|
121
|
+
output.hint(`Token expires at: ${expiresAt.toISOString()}`);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
output.cleanup();
|
|
@@ -133,7 +135,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
133
135
|
} else {
|
|
134
136
|
output.error('Authentication token is invalid or expired', error);
|
|
135
137
|
output.blank();
|
|
136
|
-
output.
|
|
138
|
+
output.hint('Run "vizzly login" to authenticate again');
|
|
137
139
|
}
|
|
138
140
|
output.cleanup();
|
|
139
141
|
process.exit(1);
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Core - Pure functions for configuration logic
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no side effects - just data transformations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Default Configuration
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default configuration values
|
|
15
|
+
*/
|
|
16
|
+
export const CONFIG_DEFAULTS = {
|
|
17
|
+
apiUrl: 'https://app.vizzly.dev',
|
|
18
|
+
server: {
|
|
19
|
+
port: 47392,
|
|
20
|
+
timeout: 30000
|
|
21
|
+
},
|
|
22
|
+
build: {
|
|
23
|
+
name: 'Build {timestamp}',
|
|
24
|
+
environment: 'test'
|
|
25
|
+
},
|
|
26
|
+
upload: {
|
|
27
|
+
screenshotsDir: './screenshots',
|
|
28
|
+
batchSize: 10,
|
|
29
|
+
timeout: 30000
|
|
30
|
+
},
|
|
31
|
+
comparison: {
|
|
32
|
+
threshold: 2.0
|
|
33
|
+
},
|
|
34
|
+
tdd: {
|
|
35
|
+
openReport: false
|
|
36
|
+
},
|
|
37
|
+
plugins: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Valid config scopes for reading
|
|
42
|
+
*/
|
|
43
|
+
export const READ_SCOPES = ['project', 'global', 'merged'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Valid config scopes for writing
|
|
47
|
+
*/
|
|
48
|
+
export const WRITE_SCOPES = ['project', 'global'];
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Scope Validation
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate that a scope is valid for reading
|
|
56
|
+
* @param {string} scope - Scope to validate
|
|
57
|
+
* @returns {{ valid: boolean, error: Error|null }}
|
|
58
|
+
*/
|
|
59
|
+
export function validateReadScope(scope) {
|
|
60
|
+
if (!READ_SCOPES.includes(scope)) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
error: new VizzlyError(`Invalid config scope: ${scope}. Must be 'project', 'global', or 'merged'`, 'INVALID_CONFIG_SCOPE')
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: true,
|
|
68
|
+
error: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate that a scope is valid for writing
|
|
74
|
+
* @param {string} scope - Scope to validate
|
|
75
|
+
* @returns {{ valid: boolean, error: Error|null }}
|
|
76
|
+
*/
|
|
77
|
+
export function validateWriteScope(scope) {
|
|
78
|
+
if (!WRITE_SCOPES.includes(scope)) {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
error: new VizzlyError(`Invalid config scope for update: ${scope}. Must be 'project' or 'global'`, 'INVALID_CONFIG_SCOPE')
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
valid: true,
|
|
86
|
+
error: null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Deep Merge
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deep merge two objects
|
|
96
|
+
* @param {Object} target - Target object
|
|
97
|
+
* @param {Object} source - Source object
|
|
98
|
+
* @returns {Object} Merged object (new object, inputs not mutated)
|
|
99
|
+
*/
|
|
100
|
+
export function deepMerge(target, source) {
|
|
101
|
+
let output = {
|
|
102
|
+
...target
|
|
103
|
+
};
|
|
104
|
+
for (let key of Object.keys(source)) {
|
|
105
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
106
|
+
if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
|
|
107
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
108
|
+
} else {
|
|
109
|
+
output[key] = source[key];
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
output[key] = source[key];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Config Merging with Source Tracking
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ensure value is a plain object, return empty object otherwise
|
|
124
|
+
* @param {*} value - Value to check
|
|
125
|
+
* @returns {Object} The value if it's an object, empty object otherwise
|
|
126
|
+
*/
|
|
127
|
+
function ensureObject(value) {
|
|
128
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build merged config from layers with source tracking
|
|
136
|
+
* @param {Object} options - Config layers
|
|
137
|
+
* @param {Object} options.projectConfig - Project config (from vizzly.config.js)
|
|
138
|
+
* @param {Object} options.globalConfig - Global config (from ~/.vizzly/config.json)
|
|
139
|
+
* @param {Object} [options.envOverrides] - Environment variable overrides
|
|
140
|
+
* @returns {{ config: Object, sources: Object }}
|
|
141
|
+
*/
|
|
142
|
+
export function buildMergedConfig({
|
|
143
|
+
projectConfig = {},
|
|
144
|
+
globalConfig = {},
|
|
145
|
+
envOverrides = {}
|
|
146
|
+
} = {}) {
|
|
147
|
+
// Ensure all inputs are plain objects
|
|
148
|
+
let safeProjectConfig = ensureObject(projectConfig);
|
|
149
|
+
let safeGlobalConfig = ensureObject(globalConfig);
|
|
150
|
+
let safeEnvOverrides = ensureObject(envOverrides);
|
|
151
|
+
let mergedConfig = {};
|
|
152
|
+
let sources = {};
|
|
153
|
+
|
|
154
|
+
// Layer 1: Defaults
|
|
155
|
+
for (let key of Object.keys(CONFIG_DEFAULTS)) {
|
|
156
|
+
mergedConfig[key] = CONFIG_DEFAULTS[key];
|
|
157
|
+
sources[key] = 'default';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Layer 2: Global config (auth, project mappings, user preferences)
|
|
161
|
+
if (safeGlobalConfig.auth) {
|
|
162
|
+
mergedConfig.auth = safeGlobalConfig.auth;
|
|
163
|
+
sources.auth = 'global';
|
|
164
|
+
}
|
|
165
|
+
if (safeGlobalConfig.projects) {
|
|
166
|
+
mergedConfig.projects = safeGlobalConfig.projects;
|
|
167
|
+
sources.projects = 'global';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Layer 3: Project config file
|
|
171
|
+
for (let key of Object.keys(safeProjectConfig)) {
|
|
172
|
+
mergedConfig[key] = safeProjectConfig[key];
|
|
173
|
+
sources[key] = 'project';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Layer 4: Environment variables
|
|
177
|
+
for (let key of Object.keys(safeEnvOverrides)) {
|
|
178
|
+
mergedConfig[key] = safeEnvOverrides[key];
|
|
179
|
+
sources[key] = 'env';
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
config: mergedConfig,
|
|
183
|
+
sources
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract environment variable overrides
|
|
189
|
+
* @param {Object} env - Environment variables object (defaults to process.env)
|
|
190
|
+
* @returns {Object} Overrides from environment
|
|
191
|
+
*/
|
|
192
|
+
export function extractEnvOverrides(env = process.env) {
|
|
193
|
+
let overrides = {};
|
|
194
|
+
if (env.VIZZLY_TOKEN) {
|
|
195
|
+
overrides.apiKey = env.VIZZLY_TOKEN;
|
|
196
|
+
}
|
|
197
|
+
if (env.VIZZLY_API_URL) {
|
|
198
|
+
overrides.apiUrl = env.VIZZLY_API_URL;
|
|
199
|
+
}
|
|
200
|
+
return overrides;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Config Result Building
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build a project config result object
|
|
209
|
+
* @param {Object|null} config - Config object or null if not found
|
|
210
|
+
* @param {string|null} filepath - Path to config file or null
|
|
211
|
+
* @returns {{ config: Object, filepath: string|null, isEmpty: boolean }}
|
|
212
|
+
*/
|
|
213
|
+
export function buildProjectConfigResult(config, filepath) {
|
|
214
|
+
if (!config) {
|
|
215
|
+
return {
|
|
216
|
+
config: {},
|
|
217
|
+
filepath: null,
|
|
218
|
+
isEmpty: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
config,
|
|
223
|
+
filepath,
|
|
224
|
+
isEmpty: Object.keys(config).length === 0
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build a global config result object
|
|
230
|
+
* @param {Object} config - Global config object
|
|
231
|
+
* @param {string} filepath - Path to global config file
|
|
232
|
+
* @returns {{ config: Object, filepath: string, isEmpty: boolean }}
|
|
233
|
+
*/
|
|
234
|
+
export function buildGlobalConfigResult(config, filepath) {
|
|
235
|
+
return {
|
|
236
|
+
config,
|
|
237
|
+
filepath,
|
|
238
|
+
isEmpty: Object.keys(config).length === 0
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build a merged config result object
|
|
244
|
+
* @param {Object} options - Build options
|
|
245
|
+
* @returns {{ config: Object, sources: Object, projectFilepath: string|null, globalFilepath: string }}
|
|
246
|
+
*/
|
|
247
|
+
export function buildMergedConfigResult({
|
|
248
|
+
projectConfig,
|
|
249
|
+
globalConfig,
|
|
250
|
+
envOverrides,
|
|
251
|
+
projectFilepath,
|
|
252
|
+
globalFilepath
|
|
253
|
+
}) {
|
|
254
|
+
let {
|
|
255
|
+
config,
|
|
256
|
+
sources
|
|
257
|
+
} = buildMergedConfig({
|
|
258
|
+
projectConfig,
|
|
259
|
+
globalConfig,
|
|
260
|
+
envOverrides
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
config,
|
|
264
|
+
sources,
|
|
265
|
+
projectFilepath,
|
|
266
|
+
globalFilepath
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Config Serialization
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Stringify a value with proper indentation for JavaScript output
|
|
276
|
+
* @param {*} value - Value to stringify
|
|
277
|
+
* @param {number} depth - Current depth for indentation
|
|
278
|
+
* @returns {string} JavaScript representation of value
|
|
279
|
+
*/
|
|
280
|
+
export function stringifyWithIndent(value, depth = 0) {
|
|
281
|
+
let indent = ' '.repeat(depth);
|
|
282
|
+
let prevIndent = depth > 0 ? ' '.repeat(depth - 1) : '';
|
|
283
|
+
if (value === null || value === undefined) {
|
|
284
|
+
return String(value);
|
|
285
|
+
}
|
|
286
|
+
if (typeof value === 'string') {
|
|
287
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
290
|
+
return String(value);
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
if (value.length === 0) return '[]';
|
|
294
|
+
let items = value.map(item => `${indent}${stringifyWithIndent(item, depth + 1)}`);
|
|
295
|
+
return `[\n${items.join(',\n')}\n${prevIndent}]`;
|
|
296
|
+
}
|
|
297
|
+
if (typeof value === 'object') {
|
|
298
|
+
let keys = Object.keys(value);
|
|
299
|
+
if (keys.length === 0) return '{}';
|
|
300
|
+
let items = keys.map(key => {
|
|
301
|
+
let val = stringifyWithIndent(value[key], depth + 1);
|
|
302
|
+
return `${indent}${key}: ${val}`;
|
|
303
|
+
});
|
|
304
|
+
return `{\n${items.join(',\n')}\n${prevIndent}}`;
|
|
305
|
+
}
|
|
306
|
+
return String(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Serialize config to JavaScript module format
|
|
311
|
+
* @param {Object} config - Config object to serialize
|
|
312
|
+
* @returns {string} JavaScript source code
|
|
313
|
+
*/
|
|
314
|
+
export function serializeToJavaScript(config) {
|
|
315
|
+
let lines = ['/**', ' * Vizzly Configuration', ' * @see https://docs.vizzly.dev/cli/configuration', ' */', '', "import { defineConfig } from '@vizzly-testing/cli/config';", '', 'export default defineConfig(', stringifyWithIndent(config, 1), ');', ''];
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Serialize config to JSON format
|
|
321
|
+
* @param {Object} config - Config object to serialize
|
|
322
|
+
* @returns {string} JSON string with 2-space indentation
|
|
323
|
+
*/
|
|
324
|
+
export function serializeToJson(config) {
|
|
325
|
+
return JSON.stringify(config, null, 2);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Determine the serialization format based on filepath
|
|
330
|
+
* @param {string} filepath - Path to config file
|
|
331
|
+
* @returns {'javascript'|'json'|'package'|'unknown'} Format type
|
|
332
|
+
*/
|
|
333
|
+
export function getConfigFormat(filepath) {
|
|
334
|
+
if (filepath.endsWith('.js') || filepath.endsWith('.mjs')) {
|
|
335
|
+
return 'javascript';
|
|
336
|
+
}
|
|
337
|
+
if (filepath.endsWith('.json') && !filepath.endsWith('package.json')) {
|
|
338
|
+
return 'json';
|
|
339
|
+
}
|
|
340
|
+
if (filepath.endsWith('package.json')) {
|
|
341
|
+
return 'package';
|
|
342
|
+
}
|
|
343
|
+
return 'unknown';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Serialize config for writing to file
|
|
348
|
+
* @param {Object} config - Config object to serialize
|
|
349
|
+
* @param {string} filepath - Target file path
|
|
350
|
+
* @returns {{ content: string|null, format: string, error: Error|null }}
|
|
351
|
+
*/
|
|
352
|
+
export function serializeConfig(config, filepath) {
|
|
353
|
+
let format = getConfigFormat(filepath);
|
|
354
|
+
if (format === 'javascript') {
|
|
355
|
+
return {
|
|
356
|
+
content: serializeToJavaScript(config),
|
|
357
|
+
format,
|
|
358
|
+
error: null
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (format === 'json') {
|
|
362
|
+
return {
|
|
363
|
+
content: serializeToJson(config),
|
|
364
|
+
format,
|
|
365
|
+
error: null
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (format === 'package') {
|
|
369
|
+
// Can't serialize standalone, need existing package.json
|
|
370
|
+
return {
|
|
371
|
+
content: null,
|
|
372
|
+
format,
|
|
373
|
+
error: null
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
content: null,
|
|
378
|
+
format,
|
|
379
|
+
error: new VizzlyError(`Unsupported config file format: ${filepath}`, 'UNSUPPORTED_CONFIG_FORMAT')
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Config Extraction
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Extract config from cosmiconfig result (handles .default exports)
|
|
389
|
+
* @param {Object|null} result - Cosmiconfig result
|
|
390
|
+
* @returns {{ config: Object|null, filepath: string|null }}
|
|
391
|
+
*/
|
|
392
|
+
export function extractCosmiconfigResult(result) {
|
|
393
|
+
if (!result || !result.config) {
|
|
394
|
+
return {
|
|
395
|
+
config: null,
|
|
396
|
+
filepath: null
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Handle both `export default` and `module.exports`
|
|
401
|
+
let config = result.config.default || result.config;
|
|
402
|
+
return {
|
|
403
|
+
config,
|
|
404
|
+
filepath: result.filepath
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Validation Result Building
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Build a validation success result
|
|
414
|
+
* @param {Object} validatedConfig - Validated config
|
|
415
|
+
* @returns {{ valid: true, config: Object, errors: [] }}
|
|
416
|
+
*/
|
|
417
|
+
export function buildValidationSuccess(validatedConfig) {
|
|
418
|
+
return {
|
|
419
|
+
valid: true,
|
|
420
|
+
config: validatedConfig,
|
|
421
|
+
errors: []
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Build a validation failure result
|
|
427
|
+
* @param {Error} error - Validation error
|
|
428
|
+
* @returns {{ valid: false, config: null, errors: Array }}
|
|
429
|
+
*/
|
|
430
|
+
export function buildValidationFailure(error) {
|
|
431
|
+
return {
|
|
432
|
+
valid: false,
|
|
433
|
+
config: null,
|
|
434
|
+
errors: error.errors || [{
|
|
435
|
+
message: error.message
|
|
436
|
+
}]
|
|
437
|
+
};
|
|
438
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Module - Public exports
|
|
3
|
+
*
|
|
4
|
+
* Provides functional configuration primitives:
|
|
5
|
+
* - core.js: Pure functions for merging, serialization, validation results
|
|
6
|
+
* - operations.js: Config operations with dependency injection
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core pure functions
|
|
10
|
+
export { buildGlobalConfigResult, buildMergedConfig, buildMergedConfigResult, buildProjectConfigResult, buildValidationFailure, buildValidationSuccess, CONFIG_DEFAULTS, deepMerge, extractCosmiconfigResult, extractEnvOverrides, getConfigFormat, READ_SCOPES, serializeConfig, serializeToJavaScript, serializeToJson, stringifyWithIndent, validateReadScope, validateWriteScope, WRITE_SCOPES } from './core.js';
|
|
11
|
+
|
|
12
|
+
// Config operations (take dependencies as parameters)
|
|
13
|
+
export { getConfig, getConfigSource, getGlobalConfig, getMergedConfig, getProjectConfig, updateConfig, updateGlobalConfig, updateProjectConfig, validateConfig, writeProjectConfigFile } from './operations.js';
|