@vijayhardaha/dev-config 2.2.0 → 2.3.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.
Files changed (45) hide show
  1. package/README.md +39 -0
  2. package/package.json +17 -20
  3. package/src/__tests__/config-constants.test.js +111 -0
  4. package/src/{index.test.js → __tests__/index.test.js} +1 -1
  5. package/src/commitlint/{index.test.js → __tests__/index.test.js} +5 -5
  6. package/src/commitlint/index.js +3 -1
  7. package/src/config-constants.js +172 -0
  8. package/src/eslint/{index.test.js → __tests__/index.test.js} +3 -3
  9. package/src/eslint/{next.test.js → __tests__/next.test.js} +3 -3
  10. package/src/eslint/{react.test.js → __tests__/react.test.js} +3 -3
  11. package/src/eslint/{typescript.test.js → __tests__/typescript.test.js} +3 -3
  12. package/src/eslint/lib/{build-config.test.js → __tests__/build-config.test.js} +11 -11
  13. package/src/eslint/lib/{files.test.js → __tests__/files.test.js} +2 -2
  14. package/src/eslint/lib/{ignores.test.js → __tests__/ignores.test.js} +3 -3
  15. package/src/eslint/lib/{index.test.js → __tests__/index.test.js} +2 -2
  16. package/src/eslint/lib/{language-options.test.js → __tests__/language-options.test.js} +2 -2
  17. package/src/eslint/lib/{rules.test.js → __tests__/rules.test.js} +1 -1
  18. package/src/eslint/lib/{setup.test.js → __tests__/setup.test.js} +1 -1
  19. package/src/eslint/lib/build-config.js +11 -110
  20. package/src/eslint/lib/files.js +3 -1
  21. package/src/eslint/lib/plugin-helpers/__tests__/enabled-plugins.test.js +49 -0
  22. package/src/eslint/lib/plugin-helpers/__tests__/fixup.test.js +48 -0
  23. package/src/eslint/lib/plugin-helpers/__tests__/flatten.test.js +51 -0
  24. package/src/eslint/lib/plugin-helpers/__tests__/parser.test.js +78 -0
  25. package/src/eslint/lib/plugin-helpers/__tests__/strip.test.js +70 -0
  26. package/src/eslint/lib/plugin-helpers/enabled-plugins.js +26 -0
  27. package/src/eslint/lib/plugin-helpers/fixup.js +34 -0
  28. package/src/eslint/lib/plugin-helpers/flatten.js +31 -0
  29. package/src/eslint/lib/plugin-helpers/index.js +18 -0
  30. package/src/eslint/lib/plugin-helpers/parser.js +33 -0
  31. package/src/eslint/lib/plugin-helpers/strip.js +45 -0
  32. package/src/gulp-smacss/{index.test.js → __tests__/index.test.js} +2 -2
  33. package/src/gulp-smacss/index.js +5 -0
  34. package/src/jsconfig/{index.test.js → __tests__/index.test.js} +3 -3
  35. package/src/lib/__tests__/config-utils.test.js +145 -0
  36. package/src/lib/__tests__/validators.test.js +229 -0
  37. package/src/lib/config-utils.js +187 -0
  38. package/src/lib/validators.js +225 -0
  39. package/src/next-sitemap/{index.test.js → __tests__/index.test.js} +31 -5
  40. package/src/next-sitemap/index.js +22 -8
  41. package/src/prettier/{index.test.js → __tests__/index.test.js} +4 -4
  42. package/src/prettier/index.js +11 -47
  43. package/src/stylelint/{index.test.js → __tests__/index.test.js} +5 -5
  44. package/src/stylelint/index.js +4 -2
  45. package/src/tsconfig/{index.test.js → __tests__/index.test.js} +5 -5
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Removes the parser from individual configs when main config provides one.
3
+ *
4
+ * Prevents parser conflicts (e.g., eslint-config-next/parser vs \@typescript-eslint/parser).
5
+ * Cleans up empty languageOptions objects after parser removal.
6
+ *
7
+ * @param {Array<object>} flatConfigs - Flat config array.
8
+ *
9
+ * @returns {Array<object>} Config array with parsers removed.
10
+ *
11
+ * @example
12
+ * const stripped = stripParser([
13
+ * {
14
+ * languageOptions: {
15
+ * parser: typescriptParser,
16
+ * sourceType: 'module'
17
+ * }
18
+ * }
19
+ * ]);
20
+ * // Returns: [{ languageOptions: { sourceType: 'module' } }]
21
+ */
22
+ export function stripParser(flatConfigs) {
23
+ return flatConfigs.map((config) => {
24
+ if (!config.languageOptions?.parser) return config;
25
+
26
+ const languageOptions = { ...config.languageOptions };
27
+ delete languageOptions.parser;
28
+
29
+ return Object.keys(languageOptions).length > 0
30
+ ? { ...config, languageOptions }
31
+ : { ...config, languageOptions: undefined };
32
+ });
33
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Removes centrally-registered plugins from individual configs.
3
+ *
4
+ * Prevents "Cannot redefine plugin" errors when plugins are registered
5
+ * at the root config level. Removes plugins by name from nested configs.
6
+ *
7
+ * @param {Array<object>} flatConfigs - Flat config array.
8
+ * @param {Array<string>} pluginNames - Plugin names to strip from configs.
9
+ *
10
+ * @returns {Array<object>} Config array with specified plugins removed.
11
+ *
12
+ * @example
13
+ * const stripped = stripPlugins(
14
+ * [
15
+ * { plugins: { prettier: prettierPlugin, eslint: eslintPlugin } },
16
+ * { plugins: { prettier: prettierPlugin } }
17
+ * ],
18
+ * ['prettier']
19
+ * );
20
+ * // prettier plugin removed from both configs
21
+ */
22
+ export function stripPlugins(flatConfigs, pluginNames) {
23
+ if (pluginNames.length === 0) return flatConfigs;
24
+
25
+ const skip = new Set(pluginNames);
26
+
27
+ return flatConfigs.map((config) => {
28
+ if (!config.plugins) return config;
29
+
30
+ const plugins = { ...config.plugins };
31
+
32
+ for (const name of skip) {
33
+ delete plugins[name];
34
+ }
35
+
36
+ if (Object.keys(plugins).length === 0) {
37
+ const rest = { ...config };
38
+ delete rest.plugins;
39
+
40
+ return rest;
41
+ }
42
+
43
+ return { ...config, plugins };
44
+ });
45
+ }
@@ -3,13 +3,13 @@ import { describe, it, expect } from 'vitest';
3
3
  // Test suite for the gulp-smacss module.
4
4
  describe('gulp-smacss/index.js', () => {
5
5
  it('should export smacssOrder array', async () => {
6
- const module = await import('./index.js');
6
+ const module = await import('../index.js');
7
7
 
8
8
  expect(Array.isArray(module.smacssOrder)).toBe(true);
9
9
  });
10
10
 
11
11
  it('should include expected SMACSS properties in the order', async () => {
12
- const module = await import('./index.js');
12
+ const module = await import('../index.js');
13
13
 
14
14
  expect(module.smacssOrder).toContain('position');
15
15
  expect(module.smacssOrder).toContain('display');
@@ -249,6 +249,11 @@ const other = [
249
249
  ];
250
250
 
251
251
  // ── Final merged order (SMACSS) ──
252
+ /**
253
+ * Ordered CSS property groups following the SMACSS methodology.
254
+ *
255
+ * @type {string[]}
256
+ */
252
257
  export const smacssOrder = [
253
258
  ...positioning,
254
259
  ...displayVisibility,
@@ -5,7 +5,7 @@ describe('jsconfig/index.json', () => {
5
5
  // Test that the jsconfig file is valid JSON and exports an object.
6
6
  it('should be a valid JSON file', async () => {
7
7
  // Dynamically import the JSON file with JSON assertion.
8
- const module = await import('./index.json', { assert: { type: 'json' } });
8
+ const module = await import('../index.json', { assert: { type: 'json' } });
9
9
 
10
10
  // Verify that the default export is an object (valid JSON object).
11
11
  expect(typeof module.default).toBe('object');
@@ -14,7 +14,7 @@ describe('jsconfig/index.json', () => {
14
14
  // Test that the jsconfig contains compilerOptions.
15
15
  it('should have compilerOptions', async () => {
16
16
  // Dynamically import the JSON file with JSON assertion.
17
- const module = await import('./index.json', { assert: { type: 'json' } });
17
+ const module = await import('../index.json', { assert: { type: 'json' } });
18
18
 
19
19
  // Verify that compilerOptions property exists on the config object.
20
20
  expect(module.default.compilerOptions).toBeDefined();
@@ -26,7 +26,7 @@ describe('jsconfig/index.json', () => {
26
26
  // Test that the jsconfig has exclude patterns defined as an array.
27
27
  it('should have exclude patterns', async () => {
28
28
  // Dynamically import the JSON file with JSON assertion.
29
- const module = await import('./index.json', { assert: { type: 'json' } });
29
+ const module = await import('../index.json', { assert: { type: 'json' } });
30
30
 
31
31
  // Verify that exclude property exists and is an array (list of file patterns to exclude).
32
32
  expect(Array.isArray(module.default.exclude)).toBe(true);
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import {
4
+ mergeDeep,
5
+ filterObjectEntries,
6
+ createFileOverride,
7
+ getNestedValue,
8
+ setNestedValue,
9
+ flattenArray,
10
+ compactArray,
11
+ isPlainObject,
12
+ } from '../config-utils.js';
13
+
14
+ describe('Config Utils', () => {
15
+ describe('mergeDeep', () => {
16
+ it('should merge objects deeply', () => {
17
+ const result = mergeDeep({ a: { b: 1 } }, { a: { c: 2 } });
18
+ expect(result).toEqual({ a: { b: 1, c: 2 } });
19
+ });
20
+
21
+ it('should override values', () => {
22
+ const result = mergeDeep({ a: 1 }, { a: 2 });
23
+ expect(result).toEqual({ a: 2 });
24
+ });
25
+
26
+ it('should handle multiple sources', () => {
27
+ const result = mergeDeep({ a: 1 }, { b: 2 }, { c: 3 });
28
+ expect(result).toEqual({ a: 1, b: 2, c: 3 });
29
+ });
30
+
31
+ it('should replace arrays instead of merging', () => {
32
+ const result = mergeDeep({ arr: [1, 2] }, { arr: [3] });
33
+ expect(result).toEqual({ arr: [3] });
34
+ });
35
+ });
36
+
37
+ describe('filterObjectEntries', () => {
38
+ it('should filter object entries', () => {
39
+ const obj = { a: 1, b: 2, c: 3 };
40
+ const result = filterObjectEntries(obj, ([key]) => key !== 'b');
41
+ expect(result).toEqual({ a: 1, c: 3 });
42
+ });
43
+
44
+ it('should filter by value', () => {
45
+ const obj = { a: 1, b: 2, c: 3 };
46
+ const result = filterObjectEntries(obj, ([, value]) => value > 1);
47
+ expect(result).toEqual({ b: 2, c: 3 });
48
+ });
49
+ });
50
+
51
+ describe('createFileOverride', () => {
52
+ it('should create file override config', () => {
53
+ const result = createFileOverride(['*.py'], { tabWidth: 4 });
54
+ expect(result).toEqual({ files: ['*.py'], options: { tabWidth: 4 } });
55
+ });
56
+ });
57
+
58
+ describe('getNestedValue', () => {
59
+ it('should get nested value with dot notation', () => {
60
+ const obj = { a: { b: { c: 42 } } };
61
+ expect(getNestedValue(obj, 'a.b.c')).toBe(42);
62
+ });
63
+
64
+ it('should return default if path not found', () => {
65
+ const obj = { a: { b: 1 } };
66
+ expect(getNestedValue(obj, 'a.b.c.d', 'default')).toBe('default');
67
+ });
68
+
69
+ it('should handle undefined as default', () => {
70
+ const obj = {};
71
+ expect(getNestedValue(obj, 'a.b')).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe('setNestedValue', () => {
76
+ it('should set nested value with dot notation', () => {
77
+ const obj = {};
78
+ setNestedValue(obj, 'a.b.c', 42);
79
+ expect(obj).toEqual({ a: { b: { c: 42 } } });
80
+ });
81
+
82
+ it('should create intermediate objects', () => {
83
+ const obj = { a: {} };
84
+ setNestedValue(obj, 'a.b.c.d', 'value');
85
+ expect(obj.a.b.c.d).toBe('value');
86
+ });
87
+
88
+ it('should return modified object', () => {
89
+ const obj = {};
90
+ const result = setNestedValue(obj, 'x', 1);
91
+ expect(result).toBe(obj);
92
+ });
93
+ });
94
+
95
+ describe('flattenArray', () => {
96
+ it('should flatten arrays', () => {
97
+ const result = flattenArray([1, [2, 3], 4]);
98
+ expect(result).toEqual([1, 2, 3, 4]);
99
+ });
100
+
101
+ it('should handle mixed arrays and objects', () => {
102
+ const result = flattenArray([{ a: 1 }, [{ b: 2 }, { c: 3 }]]);
103
+ expect(result).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]);
104
+ });
105
+ });
106
+
107
+ describe('compactArray', () => {
108
+ it('should remove falsy values', () => {
109
+ const result = compactArray([1, null, 2, undefined, 3, false, 0, '', 4]);
110
+ expect(result).toEqual([1, 2, 3, 4]);
111
+ });
112
+
113
+ it('should keep truthy values', () => {
114
+ const result = compactArray([true, 'string', {}, [], 1]);
115
+ expect(result).toEqual([true, 'string', {}, [], 1]);
116
+ });
117
+ });
118
+
119
+ describe('isPlainObject', () => {
120
+ it('should identify plain objects', () => {
121
+ expect(isPlainObject({})).toBe(true);
122
+ expect(isPlainObject({ a: 1 })).toBe(true);
123
+ });
124
+
125
+ it('should reject arrays', () => {
126
+ expect(isPlainObject([])).toBe(false);
127
+ });
128
+
129
+ it('should reject null', () => {
130
+ expect(isPlainObject(null)).toBe(false);
131
+ });
132
+
133
+ it('should reject special objects', () => {
134
+ expect(isPlainObject(new Date())).toBe(false);
135
+ expect(isPlainObject(/regex/)).toBe(false);
136
+ expect(isPlainObject(new Map())).toBe(false);
137
+ });
138
+
139
+ it('should reject primitives', () => {
140
+ expect(isPlainObject('string')).toBe(false);
141
+ expect(isPlainObject(42)).toBe(false);
142
+ expect(isPlainObject(true)).toBe(false);
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import {
4
+ validateUrl,
5
+ validateArray,
6
+ validateNonEmptyArray,
7
+ validateObject,
8
+ validateString,
9
+ validateNonEmptyString,
10
+ validateBoolean,
11
+ validateNumber,
12
+ validateNumberInRange,
13
+ validateStringArray,
14
+ validateObjectKeys,
15
+ validateRules,
16
+ validateFilePatterns,
17
+ } from '../validators.js';
18
+
19
+ describe('Validators', () => {
20
+ describe('validateUrl', () => {
21
+ it('should accept valid URLs', () => {
22
+ expect(() => validateUrl('https://example.com')).not.toThrow();
23
+ expect(() => validateUrl('http://localhost:3000')).not.toThrow();
24
+ });
25
+
26
+ it('should reject non-string URLs', () => {
27
+ expect(() => validateUrl(123)).toThrow(TypeError);
28
+ expect(() => validateUrl(null)).toThrow(TypeError);
29
+ });
30
+
31
+ it('should reject empty URLs', () => {
32
+ expect(() => validateUrl('')).toThrow(Error);
33
+ expect(() => validateUrl(' ')).toThrow(Error);
34
+ });
35
+
36
+ it('should reject invalid URLs', () => {
37
+ expect(() => validateUrl('not a url')).toThrow(Error);
38
+ expect(() => validateUrl('ht!@#tp://invalid')).toThrow(Error);
39
+ });
40
+
41
+ it('should use custom field name in error', () => {
42
+ expect(() => validateUrl('invalid', 'Sitemap URL')).toThrow(/Sitemap URL/);
43
+ });
44
+ });
45
+
46
+ describe('validateArray', () => {
47
+ it('should accept arrays', () => {
48
+ expect(() => validateArray([])).not.toThrow();
49
+ expect(() => validateArray([1, 2, 3])).not.toThrow();
50
+ });
51
+
52
+ it('should reject non-arrays', () => {
53
+ expect(() => validateArray('not an array')).toThrow(TypeError);
54
+ expect(() => validateArray({ array: true })).toThrow(TypeError);
55
+ });
56
+ });
57
+
58
+ describe('validateNonEmptyArray', () => {
59
+ it('should accept non-empty arrays', () => {
60
+ expect(() => validateNonEmptyArray([1])).not.toThrow();
61
+ });
62
+
63
+ it('should reject empty arrays', () => {
64
+ expect(() => validateNonEmptyArray([])).toThrow(Error);
65
+ });
66
+
67
+ it('should reject non-arrays', () => {
68
+ expect(() => validateNonEmptyArray('not an array')).toThrow(TypeError);
69
+ });
70
+ });
71
+
72
+ describe('validateObject', () => {
73
+ it('should accept objects', () => {
74
+ expect(() => validateObject({})).not.toThrow();
75
+ expect(() => validateObject({ key: 'value' })).not.toThrow();
76
+ });
77
+
78
+ it('should reject non-objects', () => {
79
+ expect(() => validateObject('string')).toThrow(TypeError);
80
+ expect(() => validateObject(123)).toThrow(TypeError);
81
+ });
82
+
83
+ it('should reject arrays', () => {
84
+ expect(() => validateObject([1, 2, 3])).toThrow(TypeError);
85
+ });
86
+
87
+ it('should reject null', () => {
88
+ expect(() => validateObject(null)).toThrow(TypeError);
89
+ });
90
+ });
91
+
92
+ describe('validateString', () => {
93
+ it('should accept strings', () => {
94
+ expect(() => validateString('hello')).not.toThrow();
95
+ expect(() => validateString('')).not.toThrow();
96
+ });
97
+
98
+ it('should reject non-strings', () => {
99
+ expect(() => validateString(123)).toThrow(TypeError);
100
+ expect(() => validateString(null)).toThrow(TypeError);
101
+ });
102
+ });
103
+
104
+ describe('validateNonEmptyString', () => {
105
+ it('should accept non-empty strings', () => {
106
+ expect(() => validateNonEmptyString('hello')).not.toThrow();
107
+ });
108
+
109
+ it('should reject empty strings', () => {
110
+ expect(() => validateNonEmptyString('')).toThrow(Error);
111
+ expect(() => validateNonEmptyString(' ')).toThrow(Error);
112
+ });
113
+
114
+ it('should reject non-strings', () => {
115
+ expect(() => validateNonEmptyString(123)).toThrow(TypeError);
116
+ });
117
+ });
118
+
119
+ describe('validateBoolean', () => {
120
+ it('should accept booleans', () => {
121
+ expect(() => validateBoolean(true)).not.toThrow();
122
+ expect(() => validateBoolean(false)).not.toThrow();
123
+ });
124
+
125
+ it('should reject non-booleans', () => {
126
+ expect(() => validateBoolean(1)).toThrow(TypeError);
127
+ expect(() => validateBoolean('true')).toThrow(TypeError);
128
+ });
129
+ });
130
+
131
+ describe('validateNumber', () => {
132
+ it('should accept valid numbers', () => {
133
+ expect(() => validateNumber(42)).not.toThrow();
134
+ expect(() => validateNumber(0)).not.toThrow();
135
+ expect(() => validateNumber(-5)).not.toThrow();
136
+ });
137
+
138
+ it('should reject non-numbers', () => {
139
+ expect(() => validateNumber('42')).toThrow(TypeError);
140
+ expect(() => validateNumber(null)).toThrow(TypeError);
141
+ });
142
+
143
+ it('should reject NaN and Infinity', () => {
144
+ expect(() => validateNumber(NaN)).toThrow(TypeError);
145
+ expect(() => validateNumber(Infinity)).toThrow(TypeError);
146
+ });
147
+ });
148
+
149
+ describe('validateNumberInRange', () => {
150
+ it('should accept numbers within range', () => {
151
+ expect(() => validateNumberInRange(0.5, 0, 1)).not.toThrow();
152
+ expect(() => validateNumberInRange(0, 0, 1)).not.toThrow();
153
+ expect(() => validateNumberInRange(1, 0, 1)).not.toThrow();
154
+ });
155
+
156
+ it('should reject numbers outside range', () => {
157
+ expect(() => validateNumberInRange(-0.5, 0, 1)).toThrow(Error);
158
+ expect(() => validateNumberInRange(1.5, 0, 1)).toThrow(Error);
159
+ });
160
+
161
+ it('should reject non-numbers', () => {
162
+ expect(() => validateNumberInRange('0.5', 0, 1)).toThrow(TypeError);
163
+ });
164
+ });
165
+
166
+ describe('validateStringArray', () => {
167
+ it('should accept arrays of strings', () => {
168
+ expect(() => validateStringArray(['a', 'b', 'c'])).not.toThrow();
169
+ expect(() => validateStringArray([])).not.toThrow();
170
+ });
171
+
172
+ it('should reject arrays with non-strings', () => {
173
+ expect(() => validateStringArray(['a', 123])).toThrow(TypeError);
174
+ });
175
+
176
+ it('should reject non-arrays', () => {
177
+ expect(() => validateStringArray('not an array')).toThrow(TypeError);
178
+ });
179
+ });
180
+
181
+ describe('validateObjectKeys', () => {
182
+ it('should accept objects with required keys', () => {
183
+ expect(() => validateObjectKeys({ name: 'test' }, ['name'])).not.toThrow();
184
+ expect(() => validateObjectKeys({ name: 'test', age: 30 }, ['name', 'age'])).not.toThrow();
185
+ });
186
+
187
+ it('should accept objects with extra keys', () => {
188
+ expect(() => validateObjectKeys({ name: 'test', age: 30 }, ['name'])).not.toThrow();
189
+ });
190
+
191
+ it('should reject objects missing required keys', () => {
192
+ expect(() => validateObjectKeys({ name: 'test' }, ['name', 'age'])).toThrow(Error);
193
+ });
194
+
195
+ it('should reject non-objects', () => {
196
+ expect(() => validateObjectKeys('not an object', ['key'])).toThrow(TypeError);
197
+ });
198
+ });
199
+
200
+ describe('validateRules', () => {
201
+ it('should accept rules objects', () => {
202
+ expect(() => validateRules({ 'rule-name': 'error' })).not.toThrow();
203
+ expect(() => validateRules({})).not.toThrow();
204
+ });
205
+
206
+ it('should reject non-objects', () => {
207
+ expect(() => validateRules('not an object')).toThrow(TypeError);
208
+ });
209
+
210
+ it('should reject arrays', () => {
211
+ expect(() => validateRules(['rule1', 'rule2'])).toThrow(TypeError);
212
+ });
213
+ });
214
+
215
+ describe('validateFilePatterns', () => {
216
+ it('should accept valid file patterns', () => {
217
+ expect(() => validateFilePatterns(['**/*.js'])).not.toThrow();
218
+ expect(() => validateFilePatterns(['**/*.js', '**/*.ts', '**/*.jsx'])).not.toThrow();
219
+ });
220
+
221
+ it('should reject arrays with non-strings', () => {
222
+ expect(() => validateFilePatterns(['**/*.js', 123])).toThrow(TypeError);
223
+ });
224
+
225
+ it('should reject non-arrays', () => {
226
+ expect(() => validateFilePatterns('**/*.js')).toThrow(TypeError);
227
+ });
228
+ });
229
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * =====================================================================
3
+ * Configuration Utilities
4
+ * =====================================================================
5
+ * Purpose: Shared utility functions for configuration builders.
6
+ * Common patterns used across multiple config modules.
7
+ * Usage: Import and use in config builders
8
+ * =====================================================================
9
+ */
10
+
11
+ /**
12
+ * Deep merge two objects with override capability.
13
+ *
14
+ * Later values override earlier values. Arrays are replaced, not merged.
15
+ *
16
+ * @param {object} target - Target object.
17
+ * @param {...object} sources - Source objects to merge.
18
+ *
19
+ * @returns {object} Merged object.
20
+ *
21
+ * @example
22
+ * const merged = mergeDeep(
23
+ * { a: 1, nested: { b: 2 } },
24
+ * { nested: { c: 3 } }
25
+ * );
26
+ * // Returns: { a: 1, nested: { b: 2, c: 3 } }
27
+ */
28
+ export function mergeDeep(target, ...sources) {
29
+ if (sources.length === 0) return target;
30
+
31
+ const source = sources.shift();
32
+
33
+ if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) {
34
+ return mergeDeep(target, ...sources);
35
+ }
36
+
37
+ for (const [key, value] of Object.entries(source)) {
38
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
39
+ if (!(key in target)) target[key] = {};
40
+ mergeDeep(target[key], value);
41
+ continue;
42
+ }
43
+
44
+ target[key] = value;
45
+ }
46
+
47
+ return mergeDeep(target, ...sources);
48
+ }
49
+
50
+ /**
51
+ * Filter object entries by a predicate function.
52
+ *
53
+ * @param {object} obj - Object to filter.
54
+ * @param {(entry: [string, unknown], index: number, entries: [string, unknown][]) => boolean} predicate - Function that returns true to keep entries.
55
+ *
56
+ * @returns {object} Filtered object.
57
+ *
58
+ * @example
59
+ * const filtered = filterObjectEntries(
60
+ * { a: 1, b: 2, c: 3 },
61
+ * ([key]) => key !== 'b'
62
+ * );
63
+ * // Returns: { a: 1, c: 3 }
64
+ */
65
+ export function filterObjectEntries(obj, predicate) {
66
+ return Object.fromEntries(Object.entries(obj).filter(predicate));
67
+ }
68
+
69
+ /**
70
+ * Create a configuration override for specific file patterns.
71
+ *
72
+ * @param {string[]} files - File patterns to match.
73
+ * @param {object} options - Override options.
74
+ *
75
+ * @returns {object} Override configuration object.
76
+ *
77
+ * @example
78
+ * const override = createFileOverride(['*.py'], { tabWidth: 4 });
79
+ * // Returns: { files: ['*.py'], options: { tabWidth: 4 } }
80
+ */
81
+ export function createFileOverride(files, options) {
82
+ return { files, options };
83
+ }
84
+
85
+ /**
86
+ * Get a nested property from an object using dot notation.
87
+ *
88
+ * @param {object} obj - Object to search.
89
+ * @param {string} path - Dot-notated path (e.g., 'a.b.c').
90
+ * @param {(object | Array | string | number | boolean | null | undefined)} [defaultValue] - Value if path not found.
91
+ *
92
+ * @returns {(object | Array | string | number | boolean | null | undefined)} Property value or default.
93
+ *
94
+ * @example
95
+ * const value = getNestedValue({ a: { b: { c: 42 } } }, 'a.b.c');
96
+ * // Returns: 42
97
+ */
98
+ export function getNestedValue(obj, path, defaultValue = undefined) {
99
+ const keys = path.split('.');
100
+ let current = obj;
101
+
102
+ for (const key of keys) {
103
+ if (typeof current === 'object' && current !== null && key in current) {
104
+ current = current[key];
105
+ } else {
106
+ return defaultValue;
107
+ }
108
+ }
109
+
110
+ return current;
111
+ }
112
+
113
+ /**
114
+ * Set a nested property in an object using dot notation.
115
+ *
116
+ * @param {object} obj - Object to modify.
117
+ * @param {string} path - Dot-notated path (e.g., 'a.b.c').
118
+ * @param {(object | Array | string | number | boolean | null)} value - Value to set.
119
+ *
120
+ * @returns {object} Modified object.
121
+ *
122
+ * @example
123
+ * const obj = {};
124
+ * setNestedValue(obj, 'a.b.c', 42);
125
+ * // obj is now: { a: { b: { c: 42 } } }
126
+ */
127
+ export function setNestedValue(obj, path, value) {
128
+ const keys = path.split('.');
129
+ let current = obj;
130
+
131
+ for (let i = 0; i < keys.length - 1; i++) {
132
+ const key = keys[i];
133
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
134
+ current[key] = {};
135
+ }
136
+ current = current[key];
137
+ }
138
+
139
+ current[keys[keys.length - 1]] = value;
140
+ return obj;
141
+ }
142
+
143
+ /**
144
+ * Flatten an array of mixed arrays and objects.
145
+ *
146
+ * @param {Array} arr - Array to flatten (shallow).
147
+ *
148
+ * @returns {Array} Flattened array.
149
+ *
150
+ * @example
151
+ * const flat = flattenArray([1, [2, 3], 4]);
152
+ * // Returns: [1, 2, 3, 4]
153
+ */
154
+ export function flattenArray(arr) {
155
+ return arr.reduce((acc, item) => (Array.isArray(item) ? acc.concat(item) : acc.concat([item])), []);
156
+ }
157
+
158
+ /**
159
+ * Remove falsy values from an array.
160
+ *
161
+ * @param {Array} arr - Array to clean.
162
+ *
163
+ * @returns {Array} Array without falsy values.
164
+ *
165
+ * @example
166
+ * const cleaned = compactArray([1, null, 2, undefined, 3, false]);
167
+ * // Returns: [1, 2, 3]
168
+ */
169
+ export function compactArray(arr) {
170
+ return arr.filter(Boolean);
171
+ }
172
+
173
+ /**
174
+ * Check if a value is a plain object (not array, Date, etc.).
175
+ *
176
+ * @param {(object | Array | string | number | boolean | null)} value - Value to check.
177
+ *
178
+ * @returns {boolean} True if plain object.
179
+ *
180
+ * @example
181
+ * isPlainObject({}) // true
182
+ * isPlainObject([]) // false
183
+ * isPlainObject(new Date()) // false
184
+ */
185
+ export function isPlainObject(value) {
186
+ return typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;
187
+ }