| 1 | import { existsSync, readFileSync } from 'node:fs'; |
| 2 | import { mkdir, writeFile } from 'node:fs/promises'; |
| 3 | import { homedir } from 'node:os'; |
| 4 | import path from 'node:path'; |
| 5 | // biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension. |
| 6 | import defaultOverrides from './features.json' with { type: 'json' }; |
| 7 | const DEFAULT_CACHE_FILENAME = 'features.json'; |
| 8 | let cachedOverrides = null; |
| 9 | function normalizeFeatureMap(value) { |
| 10 | if (!value || typeof value !== 'object' || Array.isArray(value)) { |
| 11 | return {}; |
| 12 | } |
| 13 | const result = {}; |
| 14 | for (const [key, entry] of Object.entries(value)) { |
| 15 | if (typeof entry === 'boolean') { |
| 16 | result[key] = entry; |
| 17 | } |
| 18 | } |
| 19 | return result; |
| 20 | } |
| 21 | function normalizeOverrides(value) { |
| 22 | if (!value || typeof value !== 'object' || Array.isArray(value)) { |
| 23 | return { global: {}, sets: {} }; |
| 24 | } |
| 25 | const record = value; |
| 26 | const global = normalizeFeatureMap(record.global); |
| 27 | const sets = {}; |
| 28 | const rawSets = record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets) |
| 29 | ? record.sets |
| 30 | : {}; |
| 31 | for (const [setName, setValue] of Object.entries(rawSets)) { |
| 32 | const normalized = normalizeFeatureMap(setValue); |
| 33 | if (Object.keys(normalized).length > 0) { |
| 34 | sets[setName] = normalized; |
| 35 | } |
| 36 | } |
| 37 | return { global, sets }; |
| 38 | } |
| 39 | function mergeOverrides(base, next) { |
| 40 | const sets = { ...base.sets }; |
| 41 | for (const [setName, overrides] of Object.entries(next.sets)) { |
| 42 | const existing = sets[setName]; |
| 43 | sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides }; |
| 44 | } |
| 45 | return { |
| 46 | global: { ...base.global, ...next.global }, |
| 47 | sets, |
| 48 | }; |
| 49 | } |
| 50 | function toFeatureOverrides(overrides) { |
| 51 | const result = {}; |
| 52 | if (Object.keys(overrides.global).length > 0) { |
| 53 | result.global = overrides.global; |
| 54 | } |
| 55 | const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0); |
| 56 | if (setEntries.length > 0) { |
| 57 | result.sets = Object.fromEntries(setEntries); |
| 58 | } |
| 59 | return result; |
| 60 | } |
| 61 | function resolveFeaturesCachePath() { |
| 62 | const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH; |
| 63 | if (override && override.trim().length > 0) { |
| 64 | return path.resolve(override.trim()); |
| 65 | } |
| 66 | return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME); |
| 67 | } |
| 68 | function readOverridesFromFile(cachePath) { |
| 69 | if (!existsSync(cachePath)) { |
| 70 | return null; |
| 71 | } |
| 72 | try { |
| 73 | const raw = readFileSync(cachePath, 'utf8'); |
| 74 | return normalizeOverrides(JSON.parse(raw)); |
| 75 | } |
| 76 | catch { |
| 77 | return null; |
| 78 | } |
| 79 | } |
| 80 | function readOverridesFromEnv() { |
| 81 | const raw = process.env.BIRD_FEATURES_JSON; |
| 82 | if (!raw || raw.trim().length === 0) { |
| 83 | return null; |
| 84 | } |
| 85 | try { |
| 86 | return normalizeOverrides(JSON.parse(raw)); |
| 87 | } |
| 88 | catch { |
| 89 | return null; |
| 90 | } |
| 91 | } |
| 92 | function writeOverridesToDisk(cachePath, overrides) { |
| 93 | const payload = toFeatureOverrides(overrides); |
| 94 | return mkdir(path.dirname(cachePath), { recursive: true }).then(() => writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')); |
| 95 | } |
| 96 | export function loadFeatureOverrides() { |
| 97 | if (cachedOverrides) { |
| 98 | return cachedOverrides; |
| 99 | } |
| 100 | const base = normalizeOverrides(defaultOverrides); |
| 101 | const fromFile = readOverridesFromFile(resolveFeaturesCachePath()); |
| 102 | const fromEnv = readOverridesFromEnv(); |
| 103 | let merged = base; |
| 104 | if (fromFile) { |
| 105 | merged = mergeOverrides(merged, fromFile); |
| 106 | } |
| 107 | if (fromEnv) { |
| 108 | merged = mergeOverrides(merged, fromEnv); |
| 109 | } |
| 110 | cachedOverrides = merged; |
| 111 | return merged; |
| 112 | } |
| 113 | export function getFeatureOverridesSnapshot() { |
| 114 | const overrides = toFeatureOverrides(loadFeatureOverrides()); |
| 115 | return { |
| 116 | cachePath: resolveFeaturesCachePath(), |
| 117 | overrides, |
| 118 | }; |
| 119 | } |
| 120 | export function applyFeatureOverrides(setName, base) { |
| 121 | const overrides = loadFeatureOverrides(); |
| 122 | const globalOverrides = overrides.global; |
| 123 | const setOverrides = overrides.sets[setName]; |
| 124 | if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) { |
| 125 | return base; |
| 126 | } |
| 127 | if (setOverrides) { |
| 128 | return { |
| 129 | ...base, |
| 130 | ...globalOverrides, |
| 131 | ...setOverrides, |
| 132 | }; |
| 133 | } |
| 134 | return { |
| 135 | ...base, |
| 136 | ...globalOverrides, |
| 137 | }; |
| 138 | } |
| 139 | export async function refreshFeatureOverridesCache() { |
| 140 | const cachePath = resolveFeaturesCachePath(); |
| 141 | const base = normalizeOverrides(defaultOverrides); |
| 142 | const fromFile = readOverridesFromFile(cachePath); |
| 143 | const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} }); |
| 144 | await writeOverridesToDisk(cachePath, merged); |
| 145 | cachedOverrides = null; |
| 146 | return { cachePath, overrides: toFeatureOverrides(merged) }; |
| 147 | } |
| 148 | export function clearFeatureOverridesCache() { |
| 149 | cachedOverrides = null; |
| 150 | } |
| 151 | //# sourceMappingURL=runtime-features.js.map |