@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.
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { mergeArrayValues, mergeObjectValues } from './merger.js'
3
+
4
+ describe('mergeObjectValues', () => {
5
+ test('should merge global and local object values', () => {
6
+ const global = {
7
+ lodash: 'Use lodash-es',
8
+ moment: 'Use date-fns',
9
+ }
10
+
11
+ const local = {
12
+ __INCLUDE_GLOBAL_VALUES__: true,
13
+ react: 'Custom restriction',
14
+ }
15
+
16
+ const result = mergeObjectValues(global, local)
17
+
18
+ expect(result).toMatchObject({
19
+ lodash: 'Use lodash-es',
20
+ moment: 'Use date-fns',
21
+ react: 'Custom restriction',
22
+ })
23
+
24
+ expect(result.__INCLUDE_GLOBAL_VALUES__).toBeUndefined()
25
+ })
26
+
27
+ test('should prioritize local values over global', () => {
28
+ const global = {
29
+ lodash: 'Use lodash-es',
30
+ moment: 'Use date-fns',
31
+ }
32
+
33
+ const local = {
34
+ __INCLUDE_GLOBAL_VALUES__: true,
35
+ lodash: 'Lodash is fine actually',
36
+ }
37
+
38
+ const result = mergeObjectValues(global, local)
39
+
40
+ expect(result.lodash).toBe('Lodash is fine actually')
41
+ expect(result.moment).toBe('Use date-fns')
42
+ })
43
+
44
+ test('should handle "off" overrides', () => {
45
+ const global = {
46
+ lodash: 'Use lodash-es',
47
+ moment: 'Use date-fns',
48
+ jquery: 'No jQuery',
49
+ }
50
+
51
+ const local = {
52
+ __INCLUDE_GLOBAL_VALUES__: true,
53
+ jquery: 'off',
54
+ react: 'Custom restriction',
55
+ }
56
+
57
+ const result = mergeObjectValues(global, local)
58
+
59
+ expect(result.lodash).toBe('Use lodash-es')
60
+ expect(result.moment).toBe('Use date-fns')
61
+ expect(result.jquery).toBeUndefined()
62
+ expect(result.react).toBe('Custom restriction')
63
+ })
64
+
65
+ test('should remove marker from result', () => {
66
+ const global = {
67
+ lodash: 'Use lodash-es',
68
+ }
69
+
70
+ const local = {
71
+ __INCLUDE_GLOBAL_VALUES__: true,
72
+ react: 'Custom restriction',
73
+ }
74
+
75
+ const result = mergeObjectValues(global, local)
76
+
77
+ expect(result.__INCLUDE_GLOBAL_VALUES__).toBeUndefined()
78
+ })
79
+
80
+ test('should handle empty global object', () => {
81
+ const global = {}
82
+
83
+ const local = {
84
+ __INCLUDE_GLOBAL_VALUES__: true,
85
+ react: 'Custom restriction',
86
+ }
87
+
88
+ const result = mergeObjectValues(global, local)
89
+
90
+ expect(result).toMatchObject({
91
+ react: 'Custom restriction',
92
+ })
93
+ })
94
+
95
+ test('should handle empty local object (except marker)', () => {
96
+ const global = {
97
+ lodash: 'Use lodash-es',
98
+ }
99
+
100
+ const local = {
101
+ __INCLUDE_GLOBAL_VALUES__: true,
102
+ }
103
+
104
+ const result = mergeObjectValues(global, local)
105
+
106
+ expect(result).toMatchObject({
107
+ lodash: 'Use lodash-es',
108
+ })
109
+ })
110
+ })
111
+
112
+ describe('mergeArrayValues', () => {
113
+ test('should merge global and local arrays', () => {
114
+ const global = ['event', 'name', 'global']
115
+ const local = ['__INCLUDE_GLOBAL_VALUES__', 'localStorage', 'sessionStorage']
116
+
117
+ const result = mergeArrayValues(global, local)
118
+
119
+ expect(result).toEqual(['localStorage', 'sessionStorage', 'event', 'name', 'global'])
120
+ })
121
+
122
+ test('should deduplicate merged arrays', () => {
123
+ const global = ['event', 'name', 'global']
124
+ const local = ['__INCLUDE_GLOBAL_VALUES__', 'event', 'localStorage']
125
+
126
+ const result = mergeArrayValues(global, local)
127
+
128
+ // Should not have duplicates
129
+ expect(result.filter((x) => x === 'event')).toHaveLength(1)
130
+
131
+ // Should maintain local precedence (local items first)
132
+ expect(result).toEqual(['event', 'localStorage', 'name', 'global'])
133
+ })
134
+
135
+ test('should remove marker from result', () => {
136
+ const global = ['event', 'name']
137
+ const local = ['__INCLUDE_GLOBAL_VALUES__', 'localStorage']
138
+
139
+ const result = mergeArrayValues(global, local)
140
+
141
+ expect(result).not.toContain('__INCLUDE_GLOBAL_VALUES__')
142
+ })
143
+
144
+ test('should handle empty global array', () => {
145
+ const global = []
146
+ const local = ['__INCLUDE_GLOBAL_VALUES__', 'localStorage']
147
+
148
+ const result = mergeArrayValues(global, local)
149
+
150
+ expect(result).toEqual(['localStorage'])
151
+ })
152
+
153
+ test('should handle local array with only marker', () => {
154
+ const global = ['event', 'name']
155
+ const local = ['__INCLUDE_GLOBAL_VALUES__']
156
+
157
+ const result = mergeArrayValues(global, local)
158
+
159
+ expect(result).toEqual(['event', 'name'])
160
+ })
161
+
162
+ test('should deduplicate complex objects', () => {
163
+ const global = [{ path: 'lodash', message: 'Use lodash-es' }]
164
+ const local = [
165
+ '__INCLUDE_GLOBAL_VALUES__',
166
+ { path: 'lodash', message: 'Use lodash-es' },
167
+ { path: 'moment', message: 'Use date-fns' },
168
+ { message: 'Use date-fns', path: 'moment' },
169
+ ]
170
+
171
+ const result = mergeArrayValues(global, local)
172
+
173
+ // Should deduplicate based on JSON serialization
174
+ expect(result).toHaveLength(2)
175
+ expect(result[0]).toMatchObject({ path: 'lodash', message: 'Use lodash-es' })
176
+ expect(result[1]).toMatchObject({ path: 'moment', message: 'Use date-fns' })
177
+ })
178
+ })
@@ -0,0 +1,121 @@
1
+ const { mergeObjectValues, mergeArrayValues } = require('./merger')
2
+ const {
3
+ detectUniversePackages,
4
+ generateUniversePackageOverrides,
5
+ getGlobalRestrictedImportPatterns,
6
+ } = require('./universePackages')
7
+
8
+ /**
9
+ * Processes the entire config, resolving markers in all overrides
10
+ * @param {Object} baseConfig - The base configuration with overrides
11
+ * @param {Map<string, any>} globalRules - Map of global rule paths to values
12
+ * @returns {Object} Configuration with markers resolved
13
+ */
14
+ function processConfig(baseConfig, globalRules) {
15
+ // Deep clone to avoid mutating original
16
+ const config = structuredClone(baseConfig)
17
+
18
+ // Process each override section
19
+ if (Array.isArray(config.overrides)) {
20
+ const processedOverrides = []
21
+
22
+ // First pass: expand markers that generate multiple overrides
23
+ for (const override of config.overrides) {
24
+ if (override === '__AUTO_GENERATE_UNIVERSE_OVERRIDES__') {
25
+ const generatedOverrides = expandUniverseOverridesMarker(config)
26
+ processedOverrides.push(...generatedOverrides)
27
+ } else {
28
+ processedOverrides.push(override)
29
+ }
30
+ }
31
+
32
+ // Second pass: process each override to resolve __INCLUDE_GLOBAL_VALUES__ markers
33
+ config.overrides = processedOverrides.map((override) =>
34
+ override.linter?.rules ? resolveIncludeGlobalValuesMarkers(override, globalRules) : override
35
+ )
36
+ } else if (config.overrides) {
37
+ throw new Error('`overrides` must be an array')
38
+ }
39
+
40
+ return config
41
+ }
42
+
43
+ /**
44
+ * Expands __AUTO_GENERATE_UNIVERSE_OVERRIDES__ marker into actual override configurations
45
+ * @param {Object} baseConfig - The base configuration (needed to extract global patterns)
46
+ * @returns {Array<Object>} Array of generated override configurations
47
+ */
48
+ function expandUniverseOverridesMarker(baseConfig) {
49
+ const universePackages = detectUniversePackages()
50
+ const globalPatterns = getGlobalRestrictedImportPatterns(baseConfig)
51
+ const generatedOverrides = generateUniversePackageOverrides(universePackages, globalPatterns)
52
+
53
+ console.log(`✓ Auto-generated ${generatedOverrides.length} override(s) for @luxfi/* packages`)
54
+
55
+ return generatedOverrides
56
+ }
57
+
58
+ /**
59
+ * Resolves __INCLUDE_GLOBAL_VALUES__ markers in an override's rule options
60
+ * @param {Object} override - Override configuration section
61
+ * @param {Map<string, any>} globalRules - Map of global rule paths to values
62
+ * @returns {Object} Processed override with markers resolved
63
+ */
64
+ function resolveIncludeGlobalValuesMarkers(override, globalRules) {
65
+ // Deep clone to avoid mutation
66
+ const processed = structuredClone(override)
67
+
68
+ /**
69
+ * Recursively walks override rules tree to find and resolve markers
70
+ * @param {Object} obj - Current object being walked
71
+ * @param {Array<string>} pathParts - Path components leading to this object
72
+ */
73
+ function walkAndMerge(obj, pathParts) {
74
+ if (!obj || typeof obj !== 'object') {
75
+ return
76
+ }
77
+
78
+ for (const [key, value] of Object.entries(obj)) {
79
+ const currentPath = [...pathParts, key]
80
+
81
+ // Check if this is a rule's options object with potential markers
82
+ if (key === 'options' && value && typeof value === 'object') {
83
+ const rulePath = pathParts.join('.')
84
+ const globalRule = globalRules.get(rulePath)
85
+
86
+ // Process each option key generically
87
+ for (const [optionKey, optionValue] of Object.entries(value)) {
88
+ const globalOptionValue = globalRule?.options?.[optionKey]
89
+
90
+ // Check for marker in object-type options
91
+ const isObjectWithMarker =
92
+ optionValue &&
93
+ typeof optionValue === 'object' &&
94
+ !Array.isArray(optionValue) &&
95
+ optionValue.__INCLUDE_GLOBAL_VALUES__
96
+
97
+ if (isObjectWithMarker) {
98
+ obj[key][optionKey] = mergeObjectValues(globalOptionValue || {}, optionValue)
99
+ }
100
+
101
+ // Check for marker in array-type options
102
+ const isArrayWithMarker = Array.isArray(optionValue) && optionValue.includes('__INCLUDE_GLOBAL_VALUES__')
103
+
104
+ if (isArrayWithMarker) {
105
+ obj[key][optionKey] = mergeArrayValues(globalOptionValue || [], optionValue)
106
+ }
107
+ }
108
+ }
109
+
110
+ // Continue walking nested objects
111
+ if (typeof value === 'object' && value !== null) {
112
+ walkAndMerge(value, currentPath)
113
+ }
114
+ }
115
+ }
116
+
117
+ walkAndMerge(processed.linter.rules, ['linter', 'rules'])
118
+ return processed
119
+ }
120
+
121
+ module.exports = { processConfig }
@@ -0,0 +1,336 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { parse as parseJsonc } from 'jsonc-parser'
5
+ import { extractGlobalRuleValues } from './extractor.js'
6
+ import { processConfig } from './processor.js'
7
+
8
+ /**
9
+ * Helper to load and process a fixture file
10
+ */
11
+ function processFixture(fixtureName) {
12
+ const fixturePath = path.join(__dirname, 'fixtures', fixtureName)
13
+ const content = fs.readFileSync(fixturePath, 'utf8')
14
+ const config = parseJsonc(content)
15
+ const globalRules = extractGlobalRuleValues(config)
16
+ return processConfig(config, globalRules)
17
+ }
18
+
19
+ describe('Biome Config Processor', () => {
20
+ describe('Object Marker Resolution', () => {
21
+ test('should merge global paths with override paths', () => {
22
+ const result = processFixture('simple-config.jsonc')
23
+
24
+ const override = result.overrides[0]
25
+ const paths = override.linter.rules.style.noRestrictedImports.options.paths
26
+
27
+ // Should include both global and override paths
28
+ expect(paths).toMatchObject({
29
+ lodash: 'Use lodash-es instead',
30
+ moment: 'Use date-fns instead',
31
+ react: 'Use preact in tests',
32
+ })
33
+
34
+ // Should not include marker
35
+ expect(paths.__INCLUDE_GLOBAL_VALUES__).toBeUndefined()
36
+ })
37
+
38
+ test('should handle "off" overrides correctly', () => {
39
+ const result = processFixture('off-override-config.jsonc')
40
+
41
+ const override = result.overrides[0]
42
+ const paths = override.linter.rules.style.noRestrictedImports.options.paths
43
+
44
+ // Should include global paths except the one turned off
45
+ expect(paths.lodash).toBe('Use lodash-es instead')
46
+ expect(paths.moment).toBe('Use date-fns instead')
47
+
48
+ // Should not include the "off" path
49
+ expect(paths.jquery).toBeUndefined()
50
+
51
+ // Should include override-specific path
52
+ expect(paths.axios).toBe('Use fetch instead')
53
+
54
+ // Should not include marker
55
+ expect(paths.__INCLUDE_GLOBAL_VALUES__).toBeUndefined()
56
+ })
57
+ })
58
+
59
+ describe('Array Marker Resolution', () => {
60
+ test('should merge global array with override array', () => {
61
+ const result = processFixture('array-merge-config.jsonc')
62
+
63
+ const override = result.overrides[0]
64
+ const patterns = override.linter.rules.style.noRestrictedImports.options.patterns
65
+
66
+ // Should include both global and override items
67
+ expect(patterns).toEqual(
68
+ expect.arrayContaining([
69
+ {
70
+ group: ['global/*'],
71
+ message: 'Please do not import from global',
72
+ },
73
+ {
74
+ group: ['localStorage/*'],
75
+ message: 'Please do not import from localStorage',
76
+ },
77
+ {
78
+ group: ['sessionStorage/*'],
79
+ message: 'Please do not import from sessionStorage',
80
+ },
81
+ ])
82
+ )
83
+
84
+ // Should not include marker
85
+ expect(patterns).not.toContain('__INCLUDE_GLOBAL_VALUES__')
86
+ })
87
+
88
+ test('should deduplicate merged arrays', () => {
89
+ const config = {
90
+ linter: {
91
+ rules: {
92
+ style: {
93
+ noRestrictedImports: {
94
+ level: 'error',
95
+ options: {
96
+ patterns: [
97
+ {
98
+ group: ['event/*'],
99
+ message: 'Do not import from event',
100
+ },
101
+ {
102
+ group: ['name/*'],
103
+ message: 'Do not import from name',
104
+ },
105
+ ],
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ overrides: [
112
+ {
113
+ include: ['src/**'],
114
+ linter: {
115
+ rules: {
116
+ style: {
117
+ noRestrictedImports: {
118
+ level: 'error',
119
+ options: {
120
+ patterns: [
121
+ '__INCLUDE_GLOBAL_VALUES__',
122
+ {
123
+ group: ['event/*'],
124
+ message: 'Do not import from event',
125
+ },
126
+ {
127
+ group: ['localStorage/*'],
128
+ message: 'Do not import from localStorage',
129
+ },
130
+ ],
131
+ },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ },
137
+ ],
138
+ }
139
+
140
+ const globalRules = extractGlobalRuleValues(config)
141
+ const result = processConfig(config, globalRules)
142
+
143
+ const patterns = result.overrides[0].linter.rules.style.noRestrictedImports.options.patterns
144
+
145
+ // Should not have duplicates
146
+ const eventPatterns = patterns.filter((x) => JSON.stringify(x.group) === JSON.stringify(['event/*']))
147
+ expect(eventPatterns).toHaveLength(1)
148
+
149
+ // Should maintain override order (local items first)
150
+ expect(patterns[0]).toMatchObject({
151
+ group: ['event/*'],
152
+ message: 'Do not import from event',
153
+ })
154
+ expect(patterns[1]).toMatchObject({
155
+ group: ['localStorage/*'],
156
+ message: 'Do not import from localStorage',
157
+ })
158
+ expect(patterns[2]).toMatchObject({
159
+ group: ['name/*'],
160
+ message: 'Do not import from name',
161
+ })
162
+ })
163
+ })
164
+
165
+ describe('No Markers', () => {
166
+ test('should not modify overrides without markers', () => {
167
+ const result = processFixture('no-markers-config.jsonc')
168
+
169
+ const override = result.overrides[0]
170
+ const paths = override.linter.rules.style.noRestrictedImports.options.paths
171
+
172
+ // Should only have override path
173
+ expect(paths).toMatchObject({
174
+ react: 'Custom restriction',
175
+ })
176
+
177
+ // Should not include global paths
178
+ expect(paths.lodash).toBeUndefined()
179
+ })
180
+ })
181
+
182
+ describe('Edge Cases', () => {
183
+ test('should handle config with no overrides', () => {
184
+ const config = {
185
+ linter: {
186
+ rules: {
187
+ style: {
188
+ noRestrictedImports: {
189
+ level: 'error',
190
+ options: {
191
+ paths: {
192
+ lodash: 'Use lodash-es',
193
+ },
194
+ },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ }
200
+
201
+ const globalRules = extractGlobalRuleValues(config)
202
+ const result = processConfig(config, globalRules)
203
+
204
+ // Should return unchanged
205
+ expect(result).toEqual(config)
206
+ })
207
+
208
+ test('should handle override without linter rules', () => {
209
+ const config = {
210
+ linter: {
211
+ rules: {
212
+ style: {
213
+ noRestrictedImports: {
214
+ level: 'error',
215
+ options: {
216
+ paths: {
217
+ lodash: 'Use lodash-es',
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ overrides: [
225
+ {
226
+ include: ['src/**'],
227
+ formatter: {
228
+ enabled: false,
229
+ },
230
+ },
231
+ ],
232
+ }
233
+
234
+ const globalRules = extractGlobalRuleValues(config)
235
+ const result = processConfig(config, globalRules)
236
+
237
+ // Should handle override without linter rules gracefully
238
+ expect(result.overrides[0]).toMatchObject({
239
+ include: ['src/**'],
240
+ formatter: {
241
+ enabled: false,
242
+ },
243
+ })
244
+ })
245
+
246
+ test('should handle empty global rules', () => {
247
+ const config = {
248
+ linter: {
249
+ rules: {},
250
+ },
251
+ overrides: [
252
+ {
253
+ include: ['src/**'],
254
+ linter: {
255
+ rules: {
256
+ style: {
257
+ noRestrictedImports: {
258
+ level: 'error',
259
+ options: {
260
+ paths: {
261
+ __INCLUDE_GLOBAL_VALUES__: true,
262
+ react: 'Custom restriction',
263
+ },
264
+ },
265
+ },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ ],
271
+ }
272
+
273
+ const globalRules = extractGlobalRuleValues(config)
274
+ const result = processConfig(config, globalRules)
275
+
276
+ const paths = result.overrides[0].linter.rules.style.noRestrictedImports.options.paths
277
+
278
+ // Should only include override paths
279
+ expect(paths).toMatchObject({
280
+ react: 'Custom restriction',
281
+ })
282
+
283
+ // Should remove marker
284
+ expect(paths.__INCLUDE_GLOBAL_VALUES__).toBeUndefined()
285
+ })
286
+ })
287
+
288
+ describe('Immutability', () => {
289
+ test('should not mutate original config', () => {
290
+ const originalConfig = {
291
+ linter: {
292
+ rules: {
293
+ style: {
294
+ noRestrictedImports: {
295
+ level: 'error',
296
+ options: {
297
+ paths: {
298
+ lodash: 'Use lodash-es',
299
+ },
300
+ },
301
+ },
302
+ },
303
+ },
304
+ },
305
+ overrides: [
306
+ {
307
+ include: ['src/**'],
308
+ linter: {
309
+ rules: {
310
+ style: {
311
+ noRestrictedImports: {
312
+ level: 'error',
313
+ options: {
314
+ paths: {
315
+ __INCLUDE_GLOBAL_VALUES__: true,
316
+ react: 'Custom restriction',
317
+ },
318
+ },
319
+ },
320
+ },
321
+ },
322
+ },
323
+ },
324
+ ],
325
+ }
326
+
327
+ const globalRules = extractGlobalRuleValues(originalConfig)
328
+ processConfig(originalConfig, globalRules)
329
+
330
+ // Original config should still have the marker
331
+ expect(
332
+ originalConfig.overrides[0].linter.rules.style.noRestrictedImports.options.paths.__INCLUDE_GLOBAL_VALUES__
333
+ ).toBe(true)
334
+ })
335
+ })
336
+ })