@vizzly-testing/cli 0.29.6 → 0.31.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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- 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 +870 -0
- 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 +18 -7
- 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 +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- 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 +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- 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/hotspot-coverage.js +1 -1
- 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/hotspot-service.js +1 -14
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +47 -7
- 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/colors.js +46 -44
- 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 +22 -3
- 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/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- 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/commands/tdd.js
CHANGED
|
@@ -15,6 +15,9 @@ import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTest
|
|
|
15
15
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
16
16
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
|
|
17
17
|
import * as defaultOutput from '../utils/output.js';
|
|
18
|
+
function buildLocalContextCommand() {
|
|
19
|
+
return 'vizzly context build current --source local --agent';
|
|
20
|
+
}
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* TDD command implementation
|
|
@@ -91,7 +94,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
91
94
|
output.header('tdd', mode);
|
|
92
95
|
|
|
93
96
|
// Show config in verbose mode
|
|
94
|
-
output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold}`);
|
|
97
|
+
output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold} minClusterSize=${config.comparison.minClusterSize}`);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
// Create functional dependencies
|
|
@@ -133,6 +136,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
133
136
|
commit,
|
|
134
137
|
environment: config.build.environment,
|
|
135
138
|
threshold: config.comparison.threshold,
|
|
139
|
+
minClusterSize: config.comparison.minClusterSize,
|
|
136
140
|
allowNoToken: config.allowNoToken || false,
|
|
137
141
|
baselineBuildId: config.baselineBuildId,
|
|
138
142
|
baselineComparisonId: config.baselineComparisonId,
|
|
@@ -231,10 +235,15 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
231
235
|
exitCode,
|
|
232
236
|
comparisons,
|
|
233
237
|
summary,
|
|
234
|
-
reportPath: runResult.reportPath || '.vizzly/report/index.html'
|
|
238
|
+
reportPath: runResult.reportPath || '.vizzly/report/index.html',
|
|
239
|
+
contextCommand: buildLocalContextCommand()
|
|
235
240
|
});
|
|
236
241
|
output.cleanup();
|
|
237
242
|
}
|
|
243
|
+
if (!globalOptions.json && !options.daemon) {
|
|
244
|
+
output.blank?.();
|
|
245
|
+
output.print(` Context ${buildLocalContextCommand()}`);
|
|
246
|
+
}
|
|
238
247
|
return {
|
|
239
248
|
result: {
|
|
240
249
|
success: !hasFailures,
|
|
@@ -287,22 +296,28 @@ export function validateTddOptions(testCommand, options) {
|
|
|
287
296
|
errors.push('Test command is required');
|
|
288
297
|
}
|
|
289
298
|
if (options.port) {
|
|
290
|
-
let port =
|
|
291
|
-
if (Number.
|
|
299
|
+
let port = Number(options.port);
|
|
300
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
292
301
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
if (options.timeout) {
|
|
296
|
-
let timeout =
|
|
297
|
-
if (Number.
|
|
305
|
+
let timeout = Number(options.timeout);
|
|
306
|
+
if (!Number.isInteger(timeout) || timeout < 1000) {
|
|
298
307
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
299
308
|
}
|
|
300
309
|
}
|
|
301
310
|
if (options.threshold !== undefined) {
|
|
302
|
-
let threshold =
|
|
303
|
-
if (Number.
|
|
311
|
+
let threshold = Number(options.threshold);
|
|
312
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
304
313
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
305
314
|
}
|
|
306
315
|
}
|
|
316
|
+
if (options.minClusterSize !== undefined) {
|
|
317
|
+
let minClusterSize = Number(options.minClusterSize);
|
|
318
|
+
if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
|
|
319
|
+
errors.push('Min cluster size must be a positive integer');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
307
322
|
return errors;
|
|
308
323
|
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
2
|
-
import { createUploader as defaultCreateUploader } from '../
|
|
2
|
+
import { createUploader as defaultCreateUploader } from '../uploader/index.js';
|
|
3
|
+
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
3
4
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
4
5
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
5
6
|
import * as defaultOutput from '../utils/output.js';
|
|
@@ -26,7 +27,7 @@ export async function constructBuildUrl(buildId, apiUrl, apiToken, deps = {}) {
|
|
|
26
27
|
command: 'upload'
|
|
27
28
|
});
|
|
28
29
|
let tokenContext = await getTokenContext(client);
|
|
29
|
-
let baseUrl = apiUrl
|
|
30
|
+
let baseUrl = getAppBaseUrl(apiUrl);
|
|
30
31
|
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
31
32
|
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
|
|
32
33
|
}
|
|
@@ -38,7 +39,7 @@ export async function constructBuildUrl(buildId, apiUrl, apiToken, deps = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// Fallback URL construction
|
|
41
|
-
let baseUrl = apiUrl
|
|
42
|
+
let baseUrl = getAppBaseUrl(apiUrl);
|
|
42
43
|
return `${baseUrl}/builds/${buildId}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -375,20 +376,20 @@ export function validateUploadOptions(screenshotsPath, options) {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
if (options.threshold !== undefined) {
|
|
378
|
-
|
|
379
|
-
if (Number.
|
|
379
|
+
let threshold = Number(options.threshold);
|
|
380
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
380
381
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
if (options.batchSize !== undefined) {
|
|
384
|
-
|
|
385
|
-
if (!Number.
|
|
385
|
+
let n = Number(options.batchSize);
|
|
386
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
386
387
|
errors.push('Batch size must be a positive integer');
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
390
|
if (options.uploadTimeout !== undefined) {
|
|
390
|
-
|
|
391
|
-
if (!Number.
|
|
391
|
+
let n = Number(options.uploadTimeout);
|
|
392
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
392
393
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
393
394
|
}
|
|
394
395
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -3,16 +3,63 @@
|
|
|
3
3
|
* Shows current user and authentication status
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
|
|
7
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
8
|
-
import * as
|
|
6
|
+
import { createAuthClient as defaultCreateAuthClient, createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens, whoami as defaultWhoami } from '../auth/index.js';
|
|
7
|
+
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
8
|
+
import * as defaultOutput from '../utils/output.js';
|
|
9
|
+
export function getTokenExpiryStatus(expiresAt, now = new Date()) {
|
|
10
|
+
let expiresDate = new Date(expiresAt);
|
|
11
|
+
let msUntilExpiry = expiresDate.getTime() - now.getTime();
|
|
12
|
+
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
13
|
+
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
14
|
+
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
15
|
+
if (msUntilExpiry <= 0) {
|
|
16
|
+
return {
|
|
17
|
+
level: 'warn',
|
|
18
|
+
message: 'Token has expired',
|
|
19
|
+
refreshHint: 'Run "vizzly login" to refresh your authentication'
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (daysUntilExpiry > 0) {
|
|
23
|
+
return {
|
|
24
|
+
level: 'hint',
|
|
25
|
+
message: `Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresDate.toLocaleDateString()})`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (hoursUntilExpiry > 0) {
|
|
29
|
+
return {
|
|
30
|
+
level: 'hint',
|
|
31
|
+
message: `Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (minutesUntilExpiry > 0) {
|
|
35
|
+
return {
|
|
36
|
+
level: 'hint',
|
|
37
|
+
message: `Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
level: 'warn',
|
|
42
|
+
message: 'Token expires in less than a minute',
|
|
43
|
+
refreshHint: 'Run "vizzly login" to refresh your authentication'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
9
46
|
|
|
10
47
|
/**
|
|
11
48
|
* Whoami command implementation
|
|
12
49
|
* @param {Object} options - Command options
|
|
13
50
|
* @param {Object} globalOptions - Global CLI options
|
|
51
|
+
* @param {Object} deps - Dependencies for testing
|
|
14
52
|
*/
|
|
15
|
-
export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
53
|
+
export async function whoamiCommand(options = {}, globalOptions = {}, deps = {}) {
|
|
54
|
+
let {
|
|
55
|
+
createAuthClient = defaultCreateAuthClient,
|
|
56
|
+
createTokenStore = defaultCreateTokenStore,
|
|
57
|
+
getApiUrl = defaultGetApiUrl,
|
|
58
|
+
getAuthTokens = defaultGetAuthTokens,
|
|
59
|
+
output = defaultOutput,
|
|
60
|
+
whoami = defaultWhoami,
|
|
61
|
+
exit = code => process.exit(code)
|
|
62
|
+
} = deps;
|
|
16
63
|
output.configure({
|
|
17
64
|
json: globalOptions.json,
|
|
18
65
|
verbose: globalOptions.verbose,
|
|
@@ -21,7 +68,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
21
68
|
try {
|
|
22
69
|
// Check if user is logged in
|
|
23
70
|
let auth = await getAuthTokens();
|
|
24
|
-
if (!auth
|
|
71
|
+
if (!auth?.accessToken) {
|
|
25
72
|
if (globalOptions.json) {
|
|
26
73
|
output.data({
|
|
27
74
|
authenticated: false
|
|
@@ -98,27 +145,17 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
98
145
|
// Show token expiry info
|
|
99
146
|
if (auth.expiresAt) {
|
|
100
147
|
output.blank();
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
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));
|
|
107
|
-
if (msUntilExpiry <= 0) {
|
|
108
|
-
output.warn('Token has expired');
|
|
109
|
-
output.hint('Run "vizzly login" to refresh your authentication');
|
|
110
|
-
} else if (daysUntilExpiry > 0) {
|
|
111
|
-
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
|
|
112
|
-
} else if (hoursUntilExpiry > 0) {
|
|
113
|
-
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
|
|
114
|
-
} else if (minutesUntilExpiry > 0) {
|
|
115
|
-
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
|
|
148
|
+
let expiry = getTokenExpiryStatus(auth.expiresAt);
|
|
149
|
+
if (expiry.level === 'warn') {
|
|
150
|
+
output.warn(expiry.message);
|
|
116
151
|
} else {
|
|
117
|
-
output.
|
|
118
|
-
|
|
152
|
+
output.hint(expiry.message);
|
|
153
|
+
}
|
|
154
|
+
if (expiry.refreshHint) {
|
|
155
|
+
output.hint(expiry.refreshHint);
|
|
119
156
|
}
|
|
120
157
|
if (globalOptions.verbose) {
|
|
121
|
-
output.hint(`Token expires at: ${expiresAt.toISOString()}`);
|
|
158
|
+
output.hint(`Token expires at: ${new Date(auth.expiresAt).toISOString()}`);
|
|
122
159
|
}
|
|
123
160
|
}
|
|
124
161
|
output.cleanup();
|
|
@@ -138,10 +175,11 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
138
175
|
output.hint('Run "vizzly login" to authenticate again');
|
|
139
176
|
}
|
|
140
177
|
output.cleanup();
|
|
141
|
-
|
|
178
|
+
exit(1);
|
|
142
179
|
} else {
|
|
143
180
|
output.error('Failed to get user information', error);
|
|
144
|
-
|
|
181
|
+
output.cleanup();
|
|
182
|
+
exit(1);
|
|
145
183
|
}
|
|
146
184
|
}
|
|
147
185
|
}
|
|
@@ -151,7 +189,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
151
189
|
* @param {Object} options - Command options
|
|
152
190
|
*/
|
|
153
191
|
export function validateWhoamiOptions() {
|
|
154
|
-
|
|
192
|
+
let errors = [];
|
|
155
193
|
|
|
156
194
|
// No specific validation needed for whoami command
|
|
157
195
|
|
package/dist/config/core.js
CHANGED
|
@@ -29,7 +29,8 @@ export const CONFIG_DEFAULTS = {
|
|
|
29
29
|
timeout: 30000
|
|
30
30
|
},
|
|
31
31
|
comparison: {
|
|
32
|
-
threshold: 2.0
|
|
32
|
+
threshold: 2.0,
|
|
33
|
+
minClusterSize: 2
|
|
33
34
|
},
|
|
34
35
|
tdd: {
|
|
35
36
|
openReport: false
|
|
@@ -91,6 +92,23 @@ export function validateWriteScope(scope) {
|
|
|
91
92
|
// Deep Merge
|
|
92
93
|
// ============================================================================
|
|
93
94
|
|
|
95
|
+
function isPlainConfigObject(value) {
|
|
96
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function cloneConfigValue(value) {
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.map(cloneConfigValue);
|
|
101
|
+
}
|
|
102
|
+
if (isPlainConfigObject(value)) {
|
|
103
|
+
let cloned = {};
|
|
104
|
+
for (let key of Object.keys(value)) {
|
|
105
|
+
cloned[key] = cloneConfigValue(value[key]);
|
|
106
|
+
}
|
|
107
|
+
return cloned;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
/**
|
|
95
113
|
* Deep merge two objects
|
|
96
114
|
* @param {Object} target - Target object
|
|
@@ -98,18 +116,16 @@ export function validateWriteScope(scope) {
|
|
|
98
116
|
* @returns {Object} Merged object (new object, inputs not mutated)
|
|
99
117
|
*/
|
|
100
118
|
export function deepMerge(target, source) {
|
|
101
|
-
let output =
|
|
102
|
-
...target
|
|
103
|
-
};
|
|
119
|
+
let output = cloneConfigValue(target);
|
|
104
120
|
for (let key of Object.keys(source)) {
|
|
105
|
-
if (
|
|
106
|
-
if (
|
|
121
|
+
if (isPlainConfigObject(source[key])) {
|
|
122
|
+
if (isPlainConfigObject(target[key])) {
|
|
107
123
|
output[key] = deepMerge(target[key], source[key]);
|
|
108
124
|
} else {
|
|
109
|
-
output[key] = source[key];
|
|
125
|
+
output[key] = cloneConfigValue(source[key]);
|
|
110
126
|
}
|
|
111
127
|
} else {
|
|
112
|
-
output[key] = source[key];
|
|
128
|
+
output[key] = cloneConfigValue(source[key]);
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
return output;
|
|
@@ -125,11 +141,18 @@ export function deepMerge(target, source) {
|
|
|
125
141
|
* @returns {Object} The value if it's an object, empty object otherwise
|
|
126
142
|
*/
|
|
127
143
|
function ensureObject(value) {
|
|
128
|
-
if (
|
|
144
|
+
if (isPlainConfigObject(value)) {
|
|
129
145
|
return value;
|
|
130
146
|
}
|
|
131
147
|
return {};
|
|
132
148
|
}
|
|
149
|
+
function applyConfigLayer(config, sources, layer, sourceName) {
|
|
150
|
+
let merged = deepMerge(config, layer);
|
|
151
|
+
for (let key of Object.keys(layer)) {
|
|
152
|
+
sources[key] = sourceName;
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
133
156
|
|
|
134
157
|
/**
|
|
135
158
|
* Build merged config from layers with source tracking
|
|
@@ -148,36 +171,14 @@ export function buildMergedConfig({
|
|
|
148
171
|
let safeProjectConfig = ensureObject(projectConfig);
|
|
149
172
|
let safeGlobalConfig = ensureObject(globalConfig);
|
|
150
173
|
let safeEnvOverrides = ensureObject(envOverrides);
|
|
151
|
-
let mergedConfig =
|
|
174
|
+
let mergedConfig = cloneConfigValue(CONFIG_DEFAULTS);
|
|
152
175
|
let sources = {};
|
|
153
|
-
|
|
154
|
-
// Layer 1: Defaults
|
|
155
176
|
for (let key of Object.keys(CONFIG_DEFAULTS)) {
|
|
156
|
-
mergedConfig[key] = CONFIG_DEFAULTS[key];
|
|
157
177
|
sources[key] = 'default';
|
|
158
178
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}
|
|
179
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeGlobalConfig, 'global');
|
|
180
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeProjectConfig, 'project');
|
|
181
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeEnvOverrides, 'env');
|
|
181
182
|
return {
|
|
182
183
|
config: mergedConfig,
|
|
183
184
|
sources
|
|
@@ -390,7 +391,7 @@ export function serializeConfig(config, filepath) {
|
|
|
390
391
|
* @returns {{ config: Object|null, filepath: string|null }}
|
|
391
392
|
*/
|
|
392
393
|
export function extractCosmiconfigResult(result) {
|
|
393
|
-
if (!result
|
|
394
|
+
if (!result?.config) {
|
|
394
395
|
return {
|
|
395
396
|
config: null,
|
|
396
397
|
filepath: null
|