@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/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()
@@ -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 }