@luxfi/biome-config 1.0.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/base.jsonc +1047 -0
- package/package.json +25 -0
- package/project.json +27 -0
- package/scripts/generate.js +81 -0
- package/src/extractor.js +44 -0
- package/src/extractor.test.js +138 -0
- package/src/fixtures/array-merge-config.jsonc +52 -0
- package/src/fixtures/no-markers-config.jsonc +37 -0
- package/src/fixtures/off-override-config.jsonc +41 -0
- package/src/fixtures/simple-config.jsonc +45 -0
- package/src/merger.js +100 -0
- package/src/merger.test.js +178 -0
- package/src/processor.js +121 -0
- package/src/processor.test.js +336 -0
- package/src/universePackages.js +144 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luxfi/biome-config",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared Biome configuration for Lux Universe",
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"jsonc-parser": "3.2.0"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./compiled/base.json"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepare": "nx prepare biome-config",
|
|
13
|
+
"test": "nx test biome-config"
|
|
14
|
+
},
|
|
15
|
+
"nx": {
|
|
16
|
+
"includedScripts": []
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"biome",
|
|
20
|
+
"config",
|
|
21
|
+
"linting",
|
|
22
|
+
"formatting"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tags": ["scope:biome-config", "type:tooling"],
|
|
3
|
+
"targets": {
|
|
4
|
+
"prepare": {
|
|
5
|
+
"command": "bun ./scripts/generate.js && biome check --write",
|
|
6
|
+
"options": {
|
|
7
|
+
"cwd": "{projectRoot}"
|
|
8
|
+
},
|
|
9
|
+
"cache": true,
|
|
10
|
+
"inputs": [
|
|
11
|
+
"{projectRoot}/base.jsonc",
|
|
12
|
+
"{projectRoot}/scripts/**",
|
|
13
|
+
"{projectRoot}/src/**",
|
|
14
|
+
"{projectRoot}/package.json"
|
|
15
|
+
],
|
|
16
|
+
"outputs": ["{projectRoot}/compiled/base.json"]
|
|
17
|
+
},
|
|
18
|
+
"test": {
|
|
19
|
+
"command": "bun test",
|
|
20
|
+
"options": {
|
|
21
|
+
"cwd": "{projectRoot}"
|
|
22
|
+
},
|
|
23
|
+
"cache": true,
|
|
24
|
+
"inputs": ["{projectRoot}/src/**", "{projectRoot}/scripts/**"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { parse: parseJsonc } = require('jsonc-parser')
|
|
6
|
+
const { extractGlobalRuleValues } = require('../src/extractor')
|
|
7
|
+
const { processConfig } = require('../src/processor')
|
|
8
|
+
|
|
9
|
+
const BASE_FILE = path.join(__dirname, '..', 'base.jsonc')
|
|
10
|
+
const OUTPUT_DIR = path.join(__dirname, '..', 'compiled')
|
|
11
|
+
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'base.json')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generic Biome Config Marker System
|
|
15
|
+
*
|
|
16
|
+
* This script processes base.jsonc and resolves __INCLUDE_GLOBAL_VALUES__ markers
|
|
17
|
+
* by merging values from the main linter config into override sections.
|
|
18
|
+
*
|
|
19
|
+
* Key features:
|
|
20
|
+
* - Generic rule extraction: walks main linter.rules tree to build lookup map
|
|
21
|
+
* - Generic marker detection: finds __INCLUDE_GLOBAL_VALUES__ in any option key
|
|
22
|
+
* - First-level merging: merges top-level keys only, not nested properties
|
|
23
|
+
* - Works with any option key: paths, patterns, deniedGlobals, etc.
|
|
24
|
+
*
|
|
25
|
+
* Process:
|
|
26
|
+
* 1. Parse base.jsonc with global restrictions in main linter config
|
|
27
|
+
* 2. Extract all rule values from main linter.rules into a lookup map
|
|
28
|
+
* 3. For each override with __INCLUDE_GLOBAL_VALUES__ markers:
|
|
29
|
+
* - Look up the corresponding global rule value
|
|
30
|
+
* - Merge using the appropriate strategy based on value type (object/array)
|
|
31
|
+
* - Handle special cases: "off" overrides for objects, deduplication for arrays
|
|
32
|
+
* 4. Write compiled output to compiled/base.json
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Main entry point - generates compiled config with markers resolved
|
|
37
|
+
*/
|
|
38
|
+
function generate() {
|
|
39
|
+
// Validate base.jsonc exists
|
|
40
|
+
if (!fs.existsSync(BASE_FILE)) {
|
|
41
|
+
console.error('Base config file not found:', BASE_FILE)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Clean and create output directory
|
|
46
|
+
if (fs.existsSync(OUTPUT_DIR)) {
|
|
47
|
+
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true })
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true })
|
|
50
|
+
|
|
51
|
+
// Read and parse base.jsonc
|
|
52
|
+
const baseContent = fs.readFileSync(BASE_FILE, 'utf8')
|
|
53
|
+
let baseConfig
|
|
54
|
+
try {
|
|
55
|
+
baseConfig = parseJsonc(baseContent)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Failed to parse base config file:', BASE_FILE, error.message)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let processedConfig
|
|
62
|
+
try {
|
|
63
|
+
// Extract global rule values from main linter config
|
|
64
|
+
const globalRules = extractGlobalRuleValues(baseConfig)
|
|
65
|
+
// Process all overrides to resolve markers
|
|
66
|
+
processedConfig = processConfig(baseConfig, globalRules)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Failed to generate biome config file:', BASE_FILE, error.message)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
// Write compiled output
|
|
72
|
+
try {
|
|
73
|
+
fs.writeFileSync(OUTPUT_FILE, `${JSON.stringify(processedConfig, null, 2)}\n`)
|
|
74
|
+
console.log('✓ Generated biome.json')
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Failed to write compiled config:', OUTPUT_FILE, error.message)
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
generate()
|
package/src/extractor.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts all rule values from main linter config into a path-keyed lookup map
|
|
3
|
+
* @param {Object} mainConfig - The main linter configuration object
|
|
4
|
+
* @returns {Map<string, any>} Map of rule paths (e.g., "linter.rules.style.noRestrictedImports") to their values
|
|
5
|
+
*/
|
|
6
|
+
function extractGlobalRuleValues(mainConfig) {
|
|
7
|
+
const ruleMap = new Map()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively walks rules tree to extract all rule configurations
|
|
11
|
+
* @param {Object} obj - Current object being walked
|
|
12
|
+
* @param {Array<string>} pathParts - Path components leading to this object
|
|
13
|
+
*/
|
|
14
|
+
function walkRules(obj, pathParts) {
|
|
15
|
+
if (!obj || typeof obj !== 'object') {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
20
|
+
const currentPath = [...pathParts, key]
|
|
21
|
+
const pathString = currentPath.join('.')
|
|
22
|
+
|
|
23
|
+
// Store this rule value if it looks like a rule configuration
|
|
24
|
+
// Rules have a "level" property, or are "off"/"error"/"warn" strings
|
|
25
|
+
if (value && typeof value === 'object' && (value.level || value.options)) {
|
|
26
|
+
ruleMap.set(pathString, value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Continue walking nested objects (but not arrays)
|
|
30
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
31
|
+
walkRules(value, currentPath)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Start walking from linter.rules
|
|
37
|
+
if (mainConfig.linter?.rules) {
|
|
38
|
+
walkRules(mainConfig.linter.rules, ['linter', 'rules'])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return ruleMap
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { extractGlobalRuleValues }
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { extractGlobalRuleValues } from './extractor.js'
|
|
3
|
+
|
|
4
|
+
describe('extractGlobalRuleValues', () => {
|
|
5
|
+
test('should extract simple rule values', () => {
|
|
6
|
+
const config = {
|
|
7
|
+
linter: {
|
|
8
|
+
rules: {
|
|
9
|
+
style: {
|
|
10
|
+
noRestrictedImports: {
|
|
11
|
+
level: 'error',
|
|
12
|
+
options: {
|
|
13
|
+
paths: {
|
|
14
|
+
lodash: 'Use lodash-es',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = extractGlobalRuleValues(config)
|
|
24
|
+
|
|
25
|
+
expect(result.has('linter.rules.style.noRestrictedImports')).toBe(true)
|
|
26
|
+
expect(result.get('linter.rules.style.noRestrictedImports')).toMatchObject({
|
|
27
|
+
level: 'error',
|
|
28
|
+
options: {
|
|
29
|
+
paths: {
|
|
30
|
+
lodash: 'Use lodash-es',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('should extract nested rule values', () => {
|
|
37
|
+
const config = {
|
|
38
|
+
linter: {
|
|
39
|
+
rules: {
|
|
40
|
+
complexity: {
|
|
41
|
+
noExtraBooleanCast: {
|
|
42
|
+
level: 'error',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
style: {
|
|
46
|
+
noNegationElse: {
|
|
47
|
+
level: 'warn',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = extractGlobalRuleValues(config)
|
|
55
|
+
|
|
56
|
+
expect(result.size).toBe(2)
|
|
57
|
+
expect(result.has('linter.rules.complexity.noExtraBooleanCast')).toBe(true)
|
|
58
|
+
expect(result.has('linter.rules.style.noNegationElse')).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('should handle config without linter rules', () => {
|
|
62
|
+
const config = {
|
|
63
|
+
formatter: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = extractGlobalRuleValues(config)
|
|
69
|
+
|
|
70
|
+
expect(result.size).toBe(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('should handle empty linter rules', () => {
|
|
74
|
+
const config = {
|
|
75
|
+
linter: {
|
|
76
|
+
rules: {},
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = extractGlobalRuleValues(config)
|
|
81
|
+
|
|
82
|
+
expect(result.size).toBe(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('should only extract objects with level or options', () => {
|
|
86
|
+
const config = {
|
|
87
|
+
linter: {
|
|
88
|
+
rules: {
|
|
89
|
+
style: {
|
|
90
|
+
noRestrictedImports: {
|
|
91
|
+
level: 'error',
|
|
92
|
+
options: {
|
|
93
|
+
paths: {
|
|
94
|
+
lodash: 'Use lodash-es',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
someOtherKey: {
|
|
99
|
+
randomProperty: 'value',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = extractGlobalRuleValues(config)
|
|
107
|
+
|
|
108
|
+
// Should only extract the rule with level/options
|
|
109
|
+
expect(result.size).toBe(1)
|
|
110
|
+
expect(result.has('linter.rules.style.noRestrictedImports')).toBe(true)
|
|
111
|
+
expect(result.has('linter.rules.style.someOtherKey')).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('should not walk into arrays', () => {
|
|
115
|
+
const config = {
|
|
116
|
+
linter: {
|
|
117
|
+
rules: {
|
|
118
|
+
style: {
|
|
119
|
+
noRestrictedGlobals: {
|
|
120
|
+
level: 'error',
|
|
121
|
+
options: {
|
|
122
|
+
deniedGlobals: ['event', 'name'],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = extractGlobalRuleValues(config)
|
|
131
|
+
|
|
132
|
+
expect(result.size).toBe(1)
|
|
133
|
+
expect(result.has('linter.rules.style.noRestrictedGlobals')).toBe(true)
|
|
134
|
+
|
|
135
|
+
const rule = result.get('linter.rules.style.noRestrictedGlobals')
|
|
136
|
+
expect(Array.isArray(rule.options.deniedGlobals)).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"linter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"rules": {
|
|
6
|
+
"style": {
|
|
7
|
+
"noRestrictedImports": {
|
|
8
|
+
"level": "error",
|
|
9
|
+
"options": {
|
|
10
|
+
"patterns": [
|
|
11
|
+
{
|
|
12
|
+
"group": ["localStorage/*"],
|
|
13
|
+
"message": "Please do not import from localStorage"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"group": ["global/*"],
|
|
17
|
+
"message": "Please do not import from global"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"overrides": [
|
|
26
|
+
{
|
|
27
|
+
"include": ["src/legacy/**"],
|
|
28
|
+
"linter": {
|
|
29
|
+
"rules": {
|
|
30
|
+
"style": {
|
|
31
|
+
"noRestrictedImports": {
|
|
32
|
+
"level": "error",
|
|
33
|
+
"options": {
|
|
34
|
+
"patterns": [
|
|
35
|
+
"__INCLUDE_GLOBAL_VALUES__",
|
|
36
|
+
{
|
|
37
|
+
"group": ["localStorage/*"],
|
|
38
|
+
"message": "Please do not import from localStorage"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"group": ["sessionStorage/*"],
|
|
42
|
+
"message": "Please do not import from sessionStorage"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"linter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"rules": {
|
|
6
|
+
"style": {
|
|
7
|
+
"noRestrictedImports": {
|
|
8
|
+
"level": "error",
|
|
9
|
+
"options": {
|
|
10
|
+
"paths": {
|
|
11
|
+
"lodash": "Use lodash-es instead"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"overrides": [
|
|
19
|
+
{
|
|
20
|
+
"include": ["src/custom/**"],
|
|
21
|
+
"linter": {
|
|
22
|
+
"rules": {
|
|
23
|
+
"style": {
|
|
24
|
+
"noRestrictedImports": {
|
|
25
|
+
"level": "error",
|
|
26
|
+
"options": {
|
|
27
|
+
"paths": {
|
|
28
|
+
"react": "Custom restriction"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"linter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"rules": {
|
|
6
|
+
"style": {
|
|
7
|
+
"noRestrictedImports": {
|
|
8
|
+
"level": "error",
|
|
9
|
+
"options": {
|
|
10
|
+
"paths": {
|
|
11
|
+
"lodash": "Use lodash-es instead",
|
|
12
|
+
"moment": "Use date-fns instead",
|
|
13
|
+
"jquery": "No jQuery allowed"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"overrides": [
|
|
21
|
+
{
|
|
22
|
+
"include": ["src/vendor/**"],
|
|
23
|
+
"linter": {
|
|
24
|
+
"rules": {
|
|
25
|
+
"style": {
|
|
26
|
+
"noRestrictedImports": {
|
|
27
|
+
"level": "error",
|
|
28
|
+
"options": {
|
|
29
|
+
"paths": {
|
|
30
|
+
"__INCLUDE_GLOBAL_VALUES__": true,
|
|
31
|
+
"jquery": "off",
|
|
32
|
+
"axios": "Use fetch instead"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"linter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"rules": {
|
|
6
|
+
"style": {
|
|
7
|
+
"noRestrictedImports": {
|
|
8
|
+
"level": "error",
|
|
9
|
+
"options": {
|
|
10
|
+
"paths": {
|
|
11
|
+
"lodash": "Use lodash-es instead",
|
|
12
|
+
"moment": "Use date-fns instead"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"noRestrictedGlobals": {
|
|
17
|
+
"level": "error",
|
|
18
|
+
"options": {
|
|
19
|
+
"deniedGlobals": ["event", "name"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"overrides": [
|
|
26
|
+
{
|
|
27
|
+
"include": ["src/test/**"],
|
|
28
|
+
"linter": {
|
|
29
|
+
"rules": {
|
|
30
|
+
"style": {
|
|
31
|
+
"noRestrictedImports": {
|
|
32
|
+
"level": "error",
|
|
33
|
+
"options": {
|
|
34
|
+
"paths": {
|
|
35
|
+
"__INCLUDE_GLOBAL_VALUES__": true,
|
|
36
|
+
"react": "Use preact in tests"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
package/src/merger.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges global object values into local object values (generic first-level merge)
|
|
3
|
+
*
|
|
4
|
+
* Performs first-level merging only - merges top-level keys but does not merge
|
|
5
|
+
* nested properties within each key's value. When both global and local define
|
|
6
|
+
* the same key, the local version completely replaces the global version.
|
|
7
|
+
*
|
|
8
|
+
* Works with any object-type option (paths, deniedGlobals, elements, etc.)
|
|
9
|
+
*
|
|
10
|
+
* Special handling:
|
|
11
|
+
* - "off" values: removes that global key from the result
|
|
12
|
+
* - Local precedence: local value completely replaces global for the same key
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} globalValues - Global values from main config
|
|
15
|
+
* @param {Object} localValues - Local values from override (includes marker)
|
|
16
|
+
* @returns {Object} Merged object (marker removed)
|
|
17
|
+
*/
|
|
18
|
+
function mergeObjectValues(globalValues, localValues) {
|
|
19
|
+
const merged = {}
|
|
20
|
+
|
|
21
|
+
// Add all global keys (except those explicitly turned off or overridden)
|
|
22
|
+
for (const [key, globalValue] of Object.entries(globalValues)) {
|
|
23
|
+
const localValue = localValues[key]
|
|
24
|
+
|
|
25
|
+
// Skip if explicitly disabled
|
|
26
|
+
if (localValue === 'off') {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Use local value if present, otherwise use global
|
|
31
|
+
merged[key] = localValue !== undefined ? localValue : globalValue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add local-only keys (except marker and "off" values)
|
|
35
|
+
for (const [key, value] of Object.entries(localValues)) {
|
|
36
|
+
const shouldSkip = key === '__INCLUDE_GLOBAL_VALUES__' || key in globalValues || value === 'off'
|
|
37
|
+
if (!shouldSkip) {
|
|
38
|
+
merged[key] = value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return merged
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalizes an item for comparison by sorting object keys
|
|
47
|
+
* This ensures objects with the same properties but different order are treated as equal
|
|
48
|
+
*
|
|
49
|
+
* @param {*} item - Item to normalize (object, primitive, etc.)
|
|
50
|
+
* @returns {string} Normalized JSON string representation
|
|
51
|
+
*/
|
|
52
|
+
function normalizeForComparison(item) {
|
|
53
|
+
if (typeof item !== 'object' || item === null) {
|
|
54
|
+
return JSON.stringify(item)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort object keys to ensure consistent serialization
|
|
58
|
+
const sortedKeys = Object.keys(item).sort()
|
|
59
|
+
const normalized = {}
|
|
60
|
+
for (const key of sortedKeys) {
|
|
61
|
+
normalized[key] = item[key]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return JSON.stringify(normalized)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merges global array values into local array values (generic array merge)
|
|
69
|
+
*
|
|
70
|
+
* Concatenates global and local arrays, removing duplicates based on JSON serialization.
|
|
71
|
+
* Works with any array-type option (patterns, etc.)
|
|
72
|
+
*
|
|
73
|
+
* @param {Array} globalValues - Global array from main config
|
|
74
|
+
* @param {Array} localValues - Local array from override (includes marker)
|
|
75
|
+
* @returns {Array} Merged array (marker removed)
|
|
76
|
+
*/
|
|
77
|
+
function mergeArrayValues(globalValues, localValues) {
|
|
78
|
+
// Filter out marker and deduplicate local values
|
|
79
|
+
const seen = new Set()
|
|
80
|
+
const cleanLocal = []
|
|
81
|
+
|
|
82
|
+
for (const item of localValues) {
|
|
83
|
+
if (item === '__INCLUDE_GLOBAL_VALUES__') {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalized = normalizeForComparison(item)
|
|
88
|
+
if (!seen.has(normalized)) {
|
|
89
|
+
seen.add(normalized)
|
|
90
|
+
cleanLocal.push(item)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add global items that don't already exist in local
|
|
95
|
+
const newItems = globalValues.filter((item) => !seen.has(normalizeForComparison(item)))
|
|
96
|
+
|
|
97
|
+
return [...cleanLocal, ...newItems]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { mergeObjectValues, mergeArrayValues }
|