Skip to content

Commit 4120b65

Browse files
committed
♻️ split shared config utilities
Move config, runtime, and shared utility contract cleanup into the next stacked review slice.
1 parent f08e059 commit 4120b65

15 files changed

Lines changed: 440 additions & 353 deletions

src/config/core.js

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const CONFIG_DEFAULTS = {
2222
batchSize: 10,
2323
timeout: 30000,
2424
},
25-
comparison: { threshold: 2.0 },
25+
comparison: { threshold: 2.0, minClusterSize: 2 },
2626
tdd: { openReport: false },
2727
plugins: [],
2828
};
@@ -81,32 +81,44 @@ export function validateWriteScope(scope) {
8181
// Deep Merge
8282
// ============================================================================
8383

84+
function isPlainConfigObject(value) {
85+
return value && typeof value === 'object' && !Array.isArray(value);
86+
}
87+
88+
function cloneConfigValue(value) {
89+
if (Array.isArray(value)) {
90+
return value.map(cloneConfigValue);
91+
}
92+
93+
if (isPlainConfigObject(value)) {
94+
let cloned = {};
95+
for (let key of Object.keys(value)) {
96+
cloned[key] = cloneConfigValue(value[key]);
97+
}
98+
return cloned;
99+
}
100+
101+
return value;
102+
}
103+
84104
/**
85105
* Deep merge two objects
86106
* @param {Object} target - Target object
87107
* @param {Object} source - Source object
88108
* @returns {Object} Merged object (new object, inputs not mutated)
89109
*/
90110
export function deepMerge(target, source) {
91-
let output = { ...target };
111+
let output = cloneConfigValue(target);
92112

93113
for (let key of Object.keys(source)) {
94-
if (
95-
source[key] &&
96-
typeof source[key] === 'object' &&
97-
!Array.isArray(source[key])
98-
) {
99-
if (
100-
target[key] &&
101-
typeof target[key] === 'object' &&
102-
!Array.isArray(target[key])
103-
) {
114+
if (isPlainConfigObject(source[key])) {
115+
if (isPlainConfigObject(target[key])) {
104116
output[key] = deepMerge(target[key], source[key]);
105117
} else {
106-
output[key] = source[key];
118+
output[key] = cloneConfigValue(source[key]);
107119
}
108120
} else {
109-
output[key] = source[key];
121+
output[key] = cloneConfigValue(source[key]);
110122
}
111123
}
112124

@@ -123,12 +135,22 @@ export function deepMerge(target, source) {
123135
* @returns {Object} The value if it's an object, empty object otherwise
124136
*/
125137
function ensureObject(value) {
126-
if (value && typeof value === 'object' && !Array.isArray(value)) {
138+
if (isPlainConfigObject(value)) {
127139
return value;
128140
}
129141
return {};
130142
}
131143

144+
function applyConfigLayer(config, sources, layer, sourceName) {
145+
let merged = deepMerge(config, layer);
146+
147+
for (let key of Object.keys(layer)) {
148+
sources[key] = sourceName;
149+
}
150+
151+
return merged;
152+
}
153+
132154
/**
133155
* Build merged config from layers with source tracking
134156
* @param {Object} options - Config layers
@@ -147,37 +169,31 @@ export function buildMergedConfig({
147169
let safeGlobalConfig = ensureObject(globalConfig);
148170
let safeEnvOverrides = ensureObject(envOverrides);
149171

150-
let mergedConfig = {};
172+
let mergedConfig = cloneConfigValue(CONFIG_DEFAULTS);
151173
let sources = {};
152174

153-
// Layer 1: Defaults
154175
for (let key of Object.keys(CONFIG_DEFAULTS)) {
155-
mergedConfig[key] = CONFIG_DEFAULTS[key];
156176
sources[key] = 'default';
157177
}
158178

159-
// Layer 2: Global config (auth, project mappings, user preferences)
160-
if (safeGlobalConfig.auth) {
161-
mergedConfig.auth = safeGlobalConfig.auth;
162-
sources.auth = 'global';
163-
}
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(
180+
mergedConfig,
181+
sources,
182+
safeGlobalConfig,
183+
'global'
184+
);
185+
mergedConfig = applyConfigLayer(
186+
mergedConfig,
187+
sources,
188+
safeProjectConfig,
189+
'project'
190+
);
191+
mergedConfig = applyConfigLayer(
192+
mergedConfig,
193+
sources,
194+
safeEnvOverrides,
195+
'env'
196+
);
181197

182198
return { config: mergedConfig, sources };
183199
}

src/services/config-service.js

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,10 @@ import { existsSync } from 'node:fs';
1212
import { writeFile } from 'node:fs/promises';
1313
import { join } from 'node:path';
1414
import { cosmiconfigSync } from 'cosmiconfig';
15+
import { CONFIG_DEFAULTS } from '../config/core.js';
1516
import { loadGlobalConfig, saveGlobalConfig } from '../utils/global-config.js';
1617
import * as output from '../utils/output.js';
1718

18-
/**
19-
* Default configuration values
20-
*/
21-
let DEFAULT_CONFIG = {
22-
comparison: {
23-
threshold: 2.0,
24-
minClusterSize: 2,
25-
},
26-
server: {
27-
port: 47392,
28-
timeout: 30000,
29-
},
30-
build: {
31-
name: 'Build {timestamp}',
32-
environment: 'test',
33-
},
34-
tdd: {
35-
openReport: false,
36-
},
37-
};
38-
3919
/**
4020
* Create a config service instance
4121
* @param {Object} options
@@ -77,7 +57,7 @@ export function createConfigService({ workingDir }) {
7757
* Get merged configuration with source tracking
7858
*/
7959
async function getMergedConfig() {
80-
let config = { ...DEFAULT_CONFIG };
60+
let config = cloneConfigValue(CONFIG_DEFAULTS);
8161
let sources = {};
8262

8363
// Layer 1: Global config
@@ -102,18 +82,17 @@ export function createConfigService({ workingDir }) {
10282

10383
// Layer 3: Environment variables
10484
if (process.env.VIZZLY_THRESHOLD) {
105-
config.comparison.threshold = parseFloat(process.env.VIZZLY_THRESHOLD);
85+
config.comparison.threshold = Number(process.env.VIZZLY_THRESHOLD);
10686
sources.comparison = 'env';
10787
}
10888
if (process.env.VIZZLY_MIN_CLUSTER_SIZE) {
109-
config.comparison.minClusterSize = parseInt(
110-
process.env.VIZZLY_MIN_CLUSTER_SIZE,
111-
10
89+
config.comparison.minClusterSize = Number(
90+
process.env.VIZZLY_MIN_CLUSTER_SIZE
11291
);
11392
sources.comparison = 'env';
11493
}
11594
if (process.env.VIZZLY_PORT) {
116-
config.server.port = parseInt(process.env.VIZZLY_PORT, 10);
95+
config.server.port = Number(process.env.VIZZLY_PORT);
11796
sources.server = 'env';
11897
}
11998

@@ -240,7 +219,11 @@ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
240219
// Validate threshold
241220
if (config.comparison?.threshold !== undefined) {
242221
let threshold = config.comparison.threshold;
243-
if (typeof threshold !== 'number' || threshold < 0) {
222+
if (
223+
typeof threshold !== 'number' ||
224+
!Number.isFinite(threshold) ||
225+
threshold < 0
226+
) {
244227
errors.push('comparison.threshold must be a non-negative number');
245228
} else if (threshold > 100) {
246229
warnings.push(
@@ -299,7 +282,7 @@ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
299282
* Deep merge two objects
300283
*/
301284
function mergeDeep(target, source) {
302-
let result = { ...target };
285+
let result = cloneConfigValue(target);
303286

304287
for (let key in source) {
305288
if (
@@ -308,6 +291,8 @@ function mergeDeep(target, source) {
308291
!Array.isArray(source[key])
309292
) {
310293
result[key] = mergeDeep(result[key] || {}, source[key]);
294+
} else if (Array.isArray(source[key])) {
295+
result[key] = [...source[key]];
311296
} else {
312297
result[key] = source[key];
313298
}
@@ -316,21 +301,45 @@ function mergeDeep(target, source) {
316301
return result;
317302
}
318303

304+
function cloneConfigValue(value) {
305+
if (Array.isArray(value)) {
306+
return value.map(cloneConfigValue);
307+
}
308+
309+
if (value && typeof value === 'object') {
310+
let clone = {};
311+
for (let key in value) {
312+
clone[key] = cloneConfigValue(value[key]);
313+
}
314+
return clone;
315+
}
316+
317+
return value;
318+
}
319+
319320
/**
320321
* Merge config with source tracking
321322
*/
322-
function mergeWithTracking(target, source, sources, sourceName) {
323+
function mergeWithTracking(target, source, sources, sourceName, sectionName) {
323324
for (let key in source) {
325+
let sourceSection = sectionName || key;
326+
324327
if (
325328
source[key] &&
326329
typeof source[key] === 'object' &&
327330
!Array.isArray(source[key])
328331
) {
329332
if (!target[key]) target[key] = {};
330-
mergeWithTracking(target[key], source[key], sources, sourceName);
333+
mergeWithTracking(
334+
target[key],
335+
source[key],
336+
sources,
337+
sourceName,
338+
sourceSection
339+
);
331340
} else {
332341
target[key] = source[key];
333-
sources[key] = sourceName;
342+
sources[sourceSection] = sourceName;
334343
}
335344
}
336345
}

0 commit comments

Comments
 (0)