@yedoma-labs/kulyn-flags 1.0.0 → 1.0.1
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/README.md +13 -1
- package/dist/index.cjs +32 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +32 -8
- package/dist/index.js.map +1 -1
- package/package.json +81 -83
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @yedoma-labs/kulyn-flags
|
|
2
2
|
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(max-width: 640px)" srcset="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized-mobile.png">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized.png" alt="Project Header">
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
8
|
+
[](https://github.com/yedoma-labs/kulyn-flags/actions/workflows/ci.yml)
|
|
9
|
+
[](https://www.npmjs.com/package/@yedoma-labs/kulyn-flags)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
|
|
13
|
+
> **kulyn** (*kulyn*) — Yakutian (Sakha) for (noch zu bestätigen)
|
|
14
|
+
|
|
3
15
|
A flag register with owners and expiry dates, plus hygiene checks you run in CI.
|
|
4
16
|
It answers the question that runtime flag SDKs leave open: **who owns this flag, and is it safe to delete?**
|
|
5
17
|
|
|
@@ -148,7 +160,7 @@ defineFlags(flags, {
|
|
|
148
160
|
- calls with a string literal as the first argument: `isEnabled("key")`, `getFlag('key')`, `client.isEnabled("key", { fallback: false })`
|
|
149
161
|
- property access: `flags.beta`, `flags["new-ui"]`
|
|
150
162
|
|
|
151
|
-
It ignores text in comments
|
|
163
|
+
It ignores text in comments, in template literals (`` `isEnabled("x")` `` is not a call) and in quoted strings (`"flags.old"` is text, not a use). It does scan calls inside `${...}` placeholders. Template literals used as call arguments, such as ``getFlag(`key`)``, are not matched. Keys must be written as literals to be found.
|
|
152
164
|
|
|
153
165
|
The scan runs in linear time and does not use nested quantifiers. Long inputs stay fast.
|
|
154
166
|
|
package/dist/index.cjs
CHANGED
|
@@ -277,6 +277,8 @@ const MAX_LITERAL_LENGTH = 100;
|
|
|
277
277
|
*
|
|
278
278
|
* Text in comments, and template literals (backticks), are ignored, including `${...}`
|
|
279
279
|
* placeholders in template literals that are not code. Calls inside `${...}` are still scanned.
|
|
280
|
+
* Calls and property access written inside a quoted string (`"isEnabled('x')"`) are ignored,
|
|
281
|
+
* because they are text, not code.
|
|
280
282
|
* This function does not read files. The caller passes the text.
|
|
281
283
|
*
|
|
282
284
|
* Limitations: regex literals containing quotes are not understood. A quote inside a
|
|
@@ -284,36 +286,38 @@ const MAX_LITERAL_LENGTH = 100;
|
|
|
284
286
|
*/
|
|
285
287
|
function findFlagKeysInSource(sourceText, options) {
|
|
286
288
|
const functions = options?.functions ?? DEFAULT_FUNCTIONS;
|
|
287
|
-
const code =
|
|
289
|
+
const { code, stringRanges } = maskWithStrings(sourceText);
|
|
288
290
|
const keys = /* @__PURE__ */ new Set();
|
|
289
291
|
if (functions.length > 0) {
|
|
290
292
|
const alternation = functions.map(escapeRegExp).join("|");
|
|
291
293
|
const callPattern = new RegExp(String.raw`\b(?:${alternation})\s*\(\s*(["'])([^"'\\\r\n]{1,${MAX_LITERAL_LENGTH}})\1(?=\s*[,)])`, "g");
|
|
292
294
|
for (const match of code.matchAll(callPattern)) {
|
|
295
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
293
296
|
const key = match[2];
|
|
294
297
|
if (key !== void 0) keys.add(key);
|
|
295
298
|
}
|
|
296
299
|
}
|
|
297
300
|
for (const match of code.matchAll(/\bflags\.([a-z][a-z0-9]*)(?![\w.-]|\s*\()/g)) {
|
|
301
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
298
302
|
const key = match[1];
|
|
299
303
|
if (key !== void 0) keys.add(key);
|
|
300
304
|
}
|
|
301
305
|
const bracketPattern = new RegExp(String.raw`\bflags\s*\[\s*(["'])([^"'\\\r\n]{1,${MAX_LITERAL_LENGTH}})\1\s*\]`, "g");
|
|
302
306
|
for (const match of code.matchAll(bracketPattern)) {
|
|
307
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
303
308
|
const key = match[2];
|
|
304
309
|
if (key !== void 0) keys.add(key);
|
|
305
310
|
}
|
|
306
311
|
return keys;
|
|
307
312
|
}
|
|
308
313
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* Single- and double-quoted strings are kept, because keys are read from them.
|
|
312
|
-
* Runs in linear time: each character is visited a constant number of times.
|
|
314
|
+
* Like `maskNonCode`, and also returns the ranges of quoted string literals as flat
|
|
315
|
+
* `[start, end, start, end, ...]` pairs (end is exclusive), in source order.
|
|
313
316
|
*/
|
|
314
|
-
function
|
|
317
|
+
function maskWithStrings(source) {
|
|
315
318
|
const out = source.split("");
|
|
316
319
|
const n = source.length;
|
|
320
|
+
const stringRanges = [];
|
|
317
321
|
const frames = [{
|
|
318
322
|
kind: "code",
|
|
319
323
|
braces: 0,
|
|
@@ -363,7 +367,9 @@ function maskNonCode(source) {
|
|
|
363
367
|
} else if (ch === "\"" || ch === "'") {
|
|
364
368
|
let j = i + 1;
|
|
365
369
|
while (j < n && source[j] !== ch && source[j] !== "\n") j += source[j] === "\\" ? 2 : 1;
|
|
366
|
-
|
|
370
|
+
const end = source[j] === ch ? j + 1 : j;
|
|
371
|
+
stringRanges.push(i, end);
|
|
372
|
+
i = end;
|
|
367
373
|
} else if (ch === "`") {
|
|
368
374
|
mask(i, i + 1);
|
|
369
375
|
frames.push({ kind: "template" });
|
|
@@ -379,7 +385,25 @@ function maskNonCode(source) {
|
|
|
379
385
|
i++;
|
|
380
386
|
} else i++;
|
|
381
387
|
}
|
|
382
|
-
return
|
|
388
|
+
return {
|
|
389
|
+
code: out.join(""),
|
|
390
|
+
stringRanges
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** True when `index` lies in one of the sorted `[start, end)` pairs of `ranges`. */
|
|
394
|
+
function isInRanges(ranges, index) {
|
|
395
|
+
if (index === void 0) return false;
|
|
396
|
+
let lo = 0;
|
|
397
|
+
let hi = ranges.length / 2 - 1;
|
|
398
|
+
while (lo <= hi) {
|
|
399
|
+
const mid = lo + hi >> 1;
|
|
400
|
+
const start = ranges[mid * 2] ?? 0;
|
|
401
|
+
const end = ranges[mid * 2 + 1] ?? 0;
|
|
402
|
+
if (index < start) hi = mid - 1;
|
|
403
|
+
else if (index >= end) lo = mid + 1;
|
|
404
|
+
else return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
383
407
|
}
|
|
384
408
|
function escapeRegExp(value) {
|
|
385
409
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/dates.ts","../src/lifetimes.ts","../src/hygiene.ts","../src/registry.ts","../src/scan.ts"],"sourcesContent":["import type { HygieneCode, HygieneFinding } from \"./types\";\n\nexport type FlagRegistryErrorCode =\n\t| \"INVALID_DEFINITION\"\n\t| \"INVALID_KEY\"\n\t| \"DUPLICATE_KEY\"\n\t| \"EMPTY_OWNER\"\n\t| \"INVALID_KIND\"\n\t| \"INVALID_DESCRIPTION\"\n\t| \"INVALID_DATE\"\n\t| \"EXPIRY_NOT_AFTER_CREATION\"\n\t| \"MISSING_EXPIRY\"\n\t| \"EXCEEDS_MAX_LIFETIME\"\n\t| \"INVALID_OPTIONS\";\n\n/**\n * Thrown when a flag register or its options are invalid.\n */\nexport class FlagRegistryError extends Error {\n\treadonly code: FlagRegistryErrorCode;\n\t/** The offending flag key, when it is known. */\n\treadonly key: string | undefined;\n\n\tconstructor(code: FlagRegistryErrorCode, message: string, key?: string) {\n\t\tsuper(message);\n\t\tthis.name = \"FlagRegistryError\";\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n\n/**\n * Thrown by `assertFlagHygiene` when findings match the `failOn` codes.\n */\nexport class FlagHygieneError extends Error {\n\treadonly code: HygieneCode;\n\treadonly findings: readonly HygieneFinding[];\n\n\tconstructor(findings: readonly HygieneFinding[]) {\n\t\tconst lines = findings.map((finding) => ` - [${finding.code}] ${finding.message}`);\n\t\tsuper(`Flag hygiene check failed with ${findings.length} finding(s):\\n${lines.join(\"\\n\")}`);\n\t\tthis.name = \"FlagHygieneError\";\n\t\tthis.code = findings[0]?.code ?? \"UNUSED\";\n\t\tthis.findings = findings;\n\t}\n}\n","export const DAY_MS = 86_400_000;\n\n/**\n * `YYYY-MM-DD`, optionally followed by `T HH:MM[:SS[.fraction]]` and a mandatory timezone.\n * Only fixed quantifiers and a single optional group, so matching is linear.\n */\nconst ISO_PATTERN =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}:\\d{2}))?$/;\n\nfunction isLeapYear(year: number): boolean {\n\treturn (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInMonth(year: number, month: number): number {\n\tconst days = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\treturn days[month - 1] ?? 0;\n}\n\n/**\n * Parses a strict ISO 8601 date or date-time and returns epoch milliseconds.\n * Returns `undefined` for anything that is not a real calendar date and time.\n */\nexport function parseIsoDate(value: unknown): number | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst match = ISO_PATTERN.exec(value);\n\tif (match === null) {\n\t\treturn undefined;\n\t}\n\n\tconst year = Number(match[1]);\n\tconst month = Number(match[2]);\n\tconst day = Number(match[3]);\n\tif (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {\n\t\treturn undefined;\n\t}\n\n\tif (match[4] !== undefined) {\n\t\tconst hours = Number(match[4]);\n\t\tconst minutes = Number(match[5]);\n\t\tconst seconds = match[6] === undefined ? 0 : Number(match[6]);\n\t\tif (hours > 23 || minutes > 59 || seconds > 59) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst offset = match[8];\n\t\tif (offset !== undefined && offset !== \"Z\") {\n\t\t\tconst offsetHours = Number(offset.slice(1, 3));\n\t\t\tconst offsetMinutes = Number(offset.slice(4, 6));\n\t\t\tif (offsetHours > 23 || offsetMinutes > 59) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst time = Date.parse(value);\n\treturn Number.isNaN(time) ? undefined : time;\n}\n","import { FlagRegistryError } from \"./errors\";\nimport type { FlagKind, LifetimeOverrides, MaxLifetimeDays } from \"./types\";\n\nexport const FLAG_KINDS: readonly FlagKind[] = [\"release\", \"experiment\", \"ops\", \"permission\"];\n\n/**\n * Default maximum lifetimes. `permission` flags may live without limit.\n */\nexport const DEFAULT_MAX_LIFETIME_DAYS: MaxLifetimeDays = Object.freeze({\n\trelease: 90,\n\texperiment: 30,\n\tops: 365,\n\tpermission: null,\n});\n\nexport function isFlagKind(value: unknown): value is FlagKind {\n\treturn typeof value === \"string\" && (FLAG_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * Merges user overrides into the defaults and validates them.\n */\nexport function resolveMaxLifetimeDays(overrides: LifetimeOverrides | undefined): MaxLifetimeDays {\n\tconst resolved: Record<FlagKind, number | null> = { ...DEFAULT_MAX_LIFETIME_DAYS };\n\tif (overrides === undefined) {\n\t\treturn resolved;\n\t}\n\tif (typeof overrides !== \"object\" || overrides === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"maxLifetimeDays must be an object\");\n\t}\n\n\tfor (const kind of Object.keys(overrides)) {\n\t\tif (!isFlagKind(kind)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`Unknown flag kind in maxLifetimeDays: \"${kind}\"`,\n\t\t\t);\n\t\t}\n\t\tconst value = overrides[kind];\n\t\tif (value === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (value !== null && !(Number.isInteger(value) && value > 0)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`maxLifetimeDays.${kind} must be a positive integer or null, got ${String(value)}`,\n\t\t\t);\n\t\t}\n\t\tresolved[kind] = value;\n\t}\n\treturn resolved;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagHygieneError, FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type {\n\tAssertFlagHygieneOptions,\n\tCheckFlagHygieneOptions,\n\tFlagDefinition,\n\tHygieneCode,\n\tHygieneFinding,\n} from \"./types\";\n\n/** Order of codes in the result when two findings share a key. */\nconst CODE_ORDER: readonly HygieneCode[] = [\n\t\"EXPIRED\",\n\t\"EXCEEDS_MAX_LIFETIME\",\n\t\"UNUSED\",\n\t\"UNREGISTERED_USAGE\",\n];\n\n/**\n * Checks a flag register for flags that should be cleaned up.\n *\n * - `EXPIRED`: `now` is at or after `expiresAt`.\n * - `EXCEEDS_MAX_LIFETIME`: the flag lives longer than the configured maximum for its kind.\n * This catches flags registered before a limit was lowered.\n * - `UNUSED`: only when `usedKeys` is given. The key is registered but not used.\n * - `UNREGISTERED_USAGE`: only when `usedKeys` is given. The key is used but not registered.\n *\n * Findings are sorted by key, then by code.\n */\nexport function checkFlagHygiene(options: CheckFlagHygieneOptions): readonly HygieneFinding[] {\n\tconst nowMs = resolveNow(options.now);\n\tconst lifetimes = resolveMaxLifetimeDays(options.maxLifetimeDays);\n\n\tconst registered = new Set<string>();\n\tconst findings: HygieneFinding[] = [];\n\n\tfor (const flag of options.flags) {\n\t\tif (registered.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tregistered.add(flag.key);\n\t\tcollectLifecycleFindings(flag, nowMs, lifetimes[flag.kind], findings);\n\t}\n\n\tif (options.usedKeys !== undefined) {\n\t\tconst used = new Set(options.usedKeys);\n\t\tfor (const key of registered) {\n\t\t\tif (!used.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNUSED\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is registered but not used`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const key of used) {\n\t\t\tif (!registered.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNREGISTERED_USAGE\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is used but not registered`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn findings.sort(\n\t\t(a, b) =>\n\t\t\tcompareStrings(a.key, b.key) || CODE_ORDER.indexOf(a.code) - CODE_ORDER.indexOf(b.code),\n\t);\n}\n\n/**\n * Throws `FlagHygieneError` if any finding has a code listed in `failOn`.\n * Defaults to all codes. Use it in tests or CI.\n */\nexport function assertFlagHygiene(\n\tfindings: readonly HygieneFinding[],\n\toptions?: AssertFlagHygieneOptions,\n): void {\n\tconst failOn = new Set<HygieneCode>(options?.failOn ?? CODE_ORDER);\n\tconst failing = findings.filter((finding) => failOn.has(finding.code));\n\tif (failing.length > 0) {\n\t\tthrow new FlagHygieneError(failing);\n\t}\n}\n\nfunction collectLifecycleFindings(\n\tflag: FlagDefinition,\n\tnowMs: number,\n\tmaxDays: number | null | undefined,\n\tfindings: HygieneFinding[],\n): void {\n\tif (!isFlagKind(flag.kind)) {\n\t\tthrow new FlagRegistryError(\"INVALID_KIND\", `Flag \"${flag.key}\" has invalid kind`, flag.key);\n\t}\n\tconst createdMs = parseIsoDate(flag.createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid createdAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\tif (flag.expiresAt === undefined) {\n\t\treturn;\n\t}\n\tconst expiresMs = parseIsoDate(flag.expiresAt);\n\tif (expiresMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid expiresAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\n\tif (nowMs >= expiresMs) {\n\t\tfindings.push({\n\t\t\tkey: flag.key,\n\t\t\tcode: \"EXPIRED\",\n\t\t\tmessage: `Flag \"${flag.key}\" expired at ${flag.expiresAt}`,\n\t\t});\n\t}\n\n\tif (maxDays !== null && maxDays !== undefined) {\n\t\tconst lifetimeMs = expiresMs - createdMs;\n\t\tif (lifetimeMs > maxDays * DAY_MS) {\n\t\t\tfindings.push({\n\t\t\t\tkey: flag.key,\n\t\t\t\tcode: \"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\tmessage: `Flag \"${flag.key}\" (${flag.kind}) lives ${lifetimeMs / DAY_MS} days, the maximum is ${maxDays}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction resolveNow(now: Date | undefined): number {\n\tif (now === undefined) {\n\t\treturn Date.now();\n\t}\n\tconst time = now.getTime();\n\tif (Number.isNaN(time)) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"now is an invalid Date\");\n\t}\n\treturn time;\n}\n\nfunction compareStrings(a: string, b: string): number {\n\tif (a === b) {\n\t\treturn 0;\n\t}\n\treturn a < b ? -1 : 1;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type { DefineFlagsOptions, FlagDefinition, MaxLifetimeDays } from \"./types\";\n\nconst KEY_PATTERN = /^[a-z][a-z0-9-.]{1,79}$/;\n\n/**\n * Validates a flag register and returns a frozen, normalized copy.\n *\n * Throws `FlagRegistryError` with a `code` for the first invalid entry.\n * A flag kind with a finite maximum lifetime requires `expiresAt`, so that a flag\n * cannot escape the limit by omitting the date.\n */\nexport function defineFlags(\n\tdefinitions: readonly FlagDefinition[],\n\toptions?: DefineFlagsOptions,\n): readonly FlagDefinition[] {\n\tif (!Array.isArray(definitions)) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", \"defineFlags expects an array of flags\");\n\t}\n\tconst maxLifetimeDays = resolveMaxLifetimeDays(options?.maxLifetimeDays);\n\n\tconst seen = new Set<string>();\n\tconst result: FlagDefinition[] = [];\n\tfor (const [index, raw] of definitions.entries()) {\n\t\tconst flag = validateDefinition(raw, index, maxLifetimeDays);\n\t\tif (seen.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tseen.add(flag.key);\n\t\tresult.push(flag);\n\t}\n\treturn Object.freeze(result);\n}\n\nfunction validateDefinition(\n\traw: unknown,\n\tindex: number,\n\tmaxLifetimeDays: MaxLifetimeDays,\n): FlagDefinition {\n\tif (typeof raw !== \"object\" || raw === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", `Flag at index ${index} is not an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\n\tconst key = record.key;\n\tif (typeof key !== \"string\" || !KEY_PATTERN.test(key)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KEY\",\n\t\t\t`Invalid flag key at index ${index}: ${JSON.stringify(key)} (must match ${KEY_PATTERN.source})`,\n\t\t\ttypeof key === \"string\" ? key : undefined,\n\t\t);\n\t}\n\n\tconst owner = record.owner;\n\tif (typeof owner !== \"string\" || owner.trim() === \"\") {\n\t\tthrow new FlagRegistryError(\"EMPTY_OWNER\", `Flag \"${key}\" has no owner`, key);\n\t}\n\n\tconst kind = record.kind;\n\tif (!isFlagKind(kind)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KIND\",\n\t\t\t`Flag \"${key}\" has invalid kind ${JSON.stringify(kind)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst createdAt = record.createdAt;\n\tconst createdMs = parseIsoDate(createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${key}\" has invalid createdAt ${JSON.stringify(createdAt)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst expiresAt = record.expiresAt;\n\tlet expiresMs: number | undefined;\n\tif (expiresAt !== undefined) {\n\t\texpiresMs = parseIsoDate(expiresAt);\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_DATE\",\n\t\t\t\t`Flag \"${key}\" has invalid expiresAt ${JSON.stringify(expiresAt)}`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs <= createdMs) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXPIRY_NOT_AFTER_CREATION\",\n\t\t\t\t`Flag \"${key}\" expires at ${String(expiresAt)}, which is not after createdAt`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst maxDays = maxLifetimeDays[kind];\n\tif (maxDays !== null) {\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"MISSING_EXPIRY\",\n\t\t\t\t`Flag \"${key}\" (${kind}) needs an expiresAt, the maximum lifetime is ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs - createdMs > maxDays * DAY_MS) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\t`Flag \"${key}\" (${kind}) lives longer than the maximum of ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst description = record.description;\n\tif (description !== undefined && typeof description !== \"string\") {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DESCRIPTION\",\n\t\t\t`Flag \"${key}\" has a non-string description`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst flag: FlagDefinition = { key, owner, kind, createdAt: createdAt as string };\n\tif (expiresAt !== undefined) {\n\t\tflag.expiresAt = expiresAt as string;\n\t}\n\tif (description !== undefined) {\n\t\tflag.description = description;\n\t}\n\treturn flag;\n}\n","import type { FindFlagKeysOptions } from \"./types\";\n\nconst DEFAULT_FUNCTIONS: readonly string[] = [\"isEnabled\", \"getFlag\", \"flag\", \"evaluate\"];\n\n/** Upper bound for a key literal. Longer literals are ignored. */\nconst MAX_LITERAL_LENGTH = 100;\n\n/**\n * Finds flag keys in source text.\n *\n * Recognized forms, with single or double quotes:\n * - `isEnabled(\"key\")`, `getFlag('key')` and the other configured function names\n * - `flags.key` (identifier-style keys only)\n * - `flags[\"key\"]`\n *\n * Text in comments, and template literals (backticks), are ignored, including `${...}`\n * placeholders in template literals that are not code. Calls inside `${...}` are still scanned.\n * This function does not read files. The caller passes the text.\n *\n * Limitations: regex literals containing quotes are not understood. A quote inside a\n * regex literal can hide code on the same line.\n */\nexport function findFlagKeysInSource(\n\tsourceText: string,\n\toptions?: FindFlagKeysOptions,\n): Set<string> {\n\tconst functions = options?.functions ?? DEFAULT_FUNCTIONS;\n\tconst code = maskNonCode(sourceText);\n\tconst keys = new Set<string>();\n\n\tif (functions.length > 0) {\n\t\tconst alternation = functions.map(escapeRegExp).join(\"|\");\n\t\t// The literal must be closed by the same quote and be a whole argument, so that\n\t\t// `isEnabled(\"a\" + b)` is not read as the key \"a\".\n\t\tconst callPattern = new RegExp(\n\t\t\tString.raw`\\b(?:${alternation})\\s*\\(\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1(?=\\s*[,)])`,\n\t\t\t\"g\",\n\t\t);\n\t\tfor (const match of code.matchAll(callPattern)) {\n\t\t\tconst key = match[2];\n\t\t\tif (key !== undefined) {\n\t\t\t\tkeys.add(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t// `flags.key` is only matched for identifier-like keys, and not for method calls like\n\t// `flags.get(` or chained access like `flags.a.b`.\n\tfor (const match of code.matchAll(/\\bflags\\.([a-z][a-z0-9]*)(?![\\w.-]|\\s*\\()/g)) {\n\t\tconst key = match[1];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\tconst bracketPattern = new RegExp(\n\t\tString.raw`\\bflags\\s*\\[\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1\\s*\\]`,\n\t\t\"g\",\n\t);\n\tfor (const match of code.matchAll(bracketPattern)) {\n\t\tconst key = match[2];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\treturn keys;\n}\n\ntype Frame = { kind: \"template\" } | { kind: \"code\"; braces: number; interpolation: boolean };\n\n/**\n * Returns `source` with comments and template-literal text replaced by spaces.\n * Newlines are kept, so the output has the same length and line structure.\n * Single- and double-quoted strings are kept, because keys are read from them.\n * Runs in linear time: each character is visited a constant number of times.\n */\nexport function maskNonCode(source: string): string {\n\tconst out = source.split(\"\");\n\tconst n = source.length;\n\tconst frames: Frame[] = [{ kind: \"code\", braces: 0, interpolation: false }];\n\n\tconst mask = (from: number, to: number): void => {\n\t\tfor (let k = from; k < to; k++) {\n\t\t\tif (out[k] !== \"\\n\" && out[k] !== \"\\r\") {\n\t\t\t\tout[k] = \" \";\n\t\t\t}\n\t\t}\n\t};\n\n\tlet i = 0;\n\twhile (i < n) {\n\t\tconst top = frames[frames.length - 1];\n\t\tif (top === undefined) {\n\t\t\tbreak;\n\t\t}\n\t\tconst ch = source[i];\n\t\tconst next = source[i + 1];\n\n\t\tif (top.kind === \"template\") {\n\t\t\tif (ch === \"\\\\\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\ti += 2;\n\t\t\t} else if (ch === \"`\") {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t\ti++;\n\t\t\t} else if (ch === \"$\" && next === \"{\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\tframes.push({ kind: \"code\", braces: 0, interpolation: true });\n\t\t\t\ti += 2;\n\t\t\t} else {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (ch === \"/\" && next === \"/\") {\n\t\t\tconst newline = source.indexOf(\"\\n\", i);\n\t\t\tconst end = newline === -1 ? n : newline;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"/\" && next === \"*\") {\n\t\t\tconst close = source.indexOf(\"*/\", i + 2);\n\t\t\tconst end = close === -1 ? n : close + 2;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === '\"' || ch === \"'\") {\n\t\t\t// Strings end at the matching quote or at a line break (unterminated string).\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < n && source[j] !== ch && source[j] !== \"\\n\") {\n\t\t\t\tj += source[j] === \"\\\\\" ? 2 : 1;\n\t\t\t}\n\t\t\ti = source[j] === ch ? j + 1 : j;\n\t\t} else if (ch === \"`\") {\n\t\t\tmask(i, i + 1);\n\t\t\tframes.push({ kind: \"template\" });\n\t\t\ti++;\n\t\t} else if (ch === \"{\") {\n\t\t\ttop.braces++;\n\t\t\ti++;\n\t\t} else if (ch === \"}\") {\n\t\t\tif (top.interpolation && top.braces === 0) {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t} else if (top.braces > 0) {\n\t\t\t\ttop.braces--;\n\t\t\t}\n\t\t\ti++;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn out.join(\"\");\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/dates.ts","../src/lifetimes.ts","../src/hygiene.ts","../src/registry.ts","../src/scan.ts"],"sourcesContent":["import type { HygieneCode, HygieneFinding } from \"./types\";\n\nexport type FlagRegistryErrorCode =\n\t| \"INVALID_DEFINITION\"\n\t| \"INVALID_KEY\"\n\t| \"DUPLICATE_KEY\"\n\t| \"EMPTY_OWNER\"\n\t| \"INVALID_KIND\"\n\t| \"INVALID_DESCRIPTION\"\n\t| \"INVALID_DATE\"\n\t| \"EXPIRY_NOT_AFTER_CREATION\"\n\t| \"MISSING_EXPIRY\"\n\t| \"EXCEEDS_MAX_LIFETIME\"\n\t| \"INVALID_OPTIONS\";\n\n/**\n * Thrown when a flag register or its options are invalid.\n */\nexport class FlagRegistryError extends Error {\n\treadonly code: FlagRegistryErrorCode;\n\t/** The offending flag key, when it is known. */\n\treadonly key: string | undefined;\n\n\tconstructor(code: FlagRegistryErrorCode, message: string, key?: string) {\n\t\tsuper(message);\n\t\tthis.name = \"FlagRegistryError\";\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n\n/**\n * Thrown by `assertFlagHygiene` when findings match the `failOn` codes.\n */\nexport class FlagHygieneError extends Error {\n\treadonly code: HygieneCode;\n\treadonly findings: readonly HygieneFinding[];\n\n\tconstructor(findings: readonly HygieneFinding[]) {\n\t\tconst lines = findings.map((finding) => ` - [${finding.code}] ${finding.message}`);\n\t\tsuper(`Flag hygiene check failed with ${findings.length} finding(s):\\n${lines.join(\"\\n\")}`);\n\t\tthis.name = \"FlagHygieneError\";\n\t\tthis.code = findings[0]?.code ?? \"UNUSED\";\n\t\tthis.findings = findings;\n\t}\n}\n","export const DAY_MS = 86_400_000;\n\n/**\n * `YYYY-MM-DD`, optionally followed by `T HH:MM[:SS[.fraction]]` and a mandatory timezone.\n * Only fixed quantifiers and a single optional group, so matching is linear.\n */\nconst ISO_PATTERN =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}:\\d{2}))?$/;\n\nfunction isLeapYear(year: number): boolean {\n\treturn (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInMonth(year: number, month: number): number {\n\tconst days = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\treturn days[month - 1] ?? 0;\n}\n\n/**\n * Parses a strict ISO 8601 date or date-time and returns epoch milliseconds.\n * Returns `undefined` for anything that is not a real calendar date and time.\n */\nexport function parseIsoDate(value: unknown): number | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst match = ISO_PATTERN.exec(value);\n\tif (match === null) {\n\t\treturn undefined;\n\t}\n\n\tconst year = Number(match[1]);\n\tconst month = Number(match[2]);\n\tconst day = Number(match[3]);\n\tif (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {\n\t\treturn undefined;\n\t}\n\n\tif (match[4] !== undefined) {\n\t\tconst hours = Number(match[4]);\n\t\tconst minutes = Number(match[5]);\n\t\tconst seconds = match[6] === undefined ? 0 : Number(match[6]);\n\t\tif (hours > 23 || minutes > 59 || seconds > 59) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst offset = match[8];\n\t\tif (offset !== undefined && offset !== \"Z\") {\n\t\t\tconst offsetHours = Number(offset.slice(1, 3));\n\t\t\tconst offsetMinutes = Number(offset.slice(4, 6));\n\t\t\tif (offsetHours > 23 || offsetMinutes > 59) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst time = Date.parse(value);\n\treturn Number.isNaN(time) ? undefined : time;\n}\n","import { FlagRegistryError } from \"./errors\";\nimport type { FlagKind, LifetimeOverrides, MaxLifetimeDays } from \"./types\";\n\nexport const FLAG_KINDS: readonly FlagKind[] = [\"release\", \"experiment\", \"ops\", \"permission\"];\n\n/**\n * Default maximum lifetimes. `permission` flags may live without limit.\n */\nexport const DEFAULT_MAX_LIFETIME_DAYS: MaxLifetimeDays = Object.freeze({\n\trelease: 90,\n\texperiment: 30,\n\tops: 365,\n\tpermission: null,\n});\n\nexport function isFlagKind(value: unknown): value is FlagKind {\n\treturn typeof value === \"string\" && (FLAG_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * Merges user overrides into the defaults and validates them.\n */\nexport function resolveMaxLifetimeDays(overrides: LifetimeOverrides | undefined): MaxLifetimeDays {\n\tconst resolved: Record<FlagKind, number | null> = { ...DEFAULT_MAX_LIFETIME_DAYS };\n\tif (overrides === undefined) {\n\t\treturn resolved;\n\t}\n\tif (typeof overrides !== \"object\" || overrides === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"maxLifetimeDays must be an object\");\n\t}\n\n\tfor (const kind of Object.keys(overrides)) {\n\t\tif (!isFlagKind(kind)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`Unknown flag kind in maxLifetimeDays: \"${kind}\"`,\n\t\t\t);\n\t\t}\n\t\tconst value = overrides[kind];\n\t\tif (value === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (value !== null && !(Number.isInteger(value) && value > 0)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`maxLifetimeDays.${kind} must be a positive integer or null, got ${String(value)}`,\n\t\t\t);\n\t\t}\n\t\tresolved[kind] = value;\n\t}\n\treturn resolved;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagHygieneError, FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type {\n\tAssertFlagHygieneOptions,\n\tCheckFlagHygieneOptions,\n\tFlagDefinition,\n\tHygieneCode,\n\tHygieneFinding,\n} from \"./types\";\n\n/** Order of codes in the result when two findings share a key. */\nconst CODE_ORDER: readonly HygieneCode[] = [\n\t\"EXPIRED\",\n\t\"EXCEEDS_MAX_LIFETIME\",\n\t\"UNUSED\",\n\t\"UNREGISTERED_USAGE\",\n];\n\n/**\n * Checks a flag register for flags that should be cleaned up.\n *\n * - `EXPIRED`: `now` is at or after `expiresAt`.\n * - `EXCEEDS_MAX_LIFETIME`: the flag lives longer than the configured maximum for its kind.\n * This catches flags registered before a limit was lowered.\n * - `UNUSED`: only when `usedKeys` is given. The key is registered but not used.\n * - `UNREGISTERED_USAGE`: only when `usedKeys` is given. The key is used but not registered.\n *\n * Findings are sorted by key, then by code.\n */\nexport function checkFlagHygiene(options: CheckFlagHygieneOptions): readonly HygieneFinding[] {\n\tconst nowMs = resolveNow(options.now);\n\tconst lifetimes = resolveMaxLifetimeDays(options.maxLifetimeDays);\n\n\tconst registered = new Set<string>();\n\tconst findings: HygieneFinding[] = [];\n\n\tfor (const flag of options.flags) {\n\t\tif (registered.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tregistered.add(flag.key);\n\t\tcollectLifecycleFindings(flag, nowMs, lifetimes[flag.kind], findings);\n\t}\n\n\tif (options.usedKeys !== undefined) {\n\t\tconst used = new Set(options.usedKeys);\n\t\tfor (const key of registered) {\n\t\t\tif (!used.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNUSED\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is registered but not used`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const key of used) {\n\t\t\tif (!registered.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNREGISTERED_USAGE\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is used but not registered`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn findings.sort(\n\t\t(a, b) =>\n\t\t\tcompareStrings(a.key, b.key) || CODE_ORDER.indexOf(a.code) - CODE_ORDER.indexOf(b.code),\n\t);\n}\n\n/**\n * Throws `FlagHygieneError` if any finding has a code listed in `failOn`.\n * Defaults to all codes. Use it in tests or CI.\n */\nexport function assertFlagHygiene(\n\tfindings: readonly HygieneFinding[],\n\toptions?: AssertFlagHygieneOptions,\n): void {\n\tconst failOn = new Set<HygieneCode>(options?.failOn ?? CODE_ORDER);\n\tconst failing = findings.filter((finding) => failOn.has(finding.code));\n\tif (failing.length > 0) {\n\t\tthrow new FlagHygieneError(failing);\n\t}\n}\n\nfunction collectLifecycleFindings(\n\tflag: FlagDefinition,\n\tnowMs: number,\n\tmaxDays: number | null | undefined,\n\tfindings: HygieneFinding[],\n): void {\n\tif (!isFlagKind(flag.kind)) {\n\t\tthrow new FlagRegistryError(\"INVALID_KIND\", `Flag \"${flag.key}\" has invalid kind`, flag.key);\n\t}\n\tconst createdMs = parseIsoDate(flag.createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid createdAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\tif (flag.expiresAt === undefined) {\n\t\treturn;\n\t}\n\tconst expiresMs = parseIsoDate(flag.expiresAt);\n\tif (expiresMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid expiresAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\n\tif (nowMs >= expiresMs) {\n\t\tfindings.push({\n\t\t\tkey: flag.key,\n\t\t\tcode: \"EXPIRED\",\n\t\t\tmessage: `Flag \"${flag.key}\" expired at ${flag.expiresAt}`,\n\t\t});\n\t}\n\n\tif (maxDays !== null && maxDays !== undefined) {\n\t\tconst lifetimeMs = expiresMs - createdMs;\n\t\tif (lifetimeMs > maxDays * DAY_MS) {\n\t\t\tfindings.push({\n\t\t\t\tkey: flag.key,\n\t\t\t\tcode: \"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\tmessage: `Flag \"${flag.key}\" (${flag.kind}) lives ${lifetimeMs / DAY_MS} days, the maximum is ${maxDays}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction resolveNow(now: Date | undefined): number {\n\tif (now === undefined) {\n\t\treturn Date.now();\n\t}\n\tconst time = now.getTime();\n\tif (Number.isNaN(time)) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"now is an invalid Date\");\n\t}\n\treturn time;\n}\n\nfunction compareStrings(a: string, b: string): number {\n\tif (a === b) {\n\t\treturn 0;\n\t}\n\treturn a < b ? -1 : 1;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type { DefineFlagsOptions, FlagDefinition, MaxLifetimeDays } from \"./types\";\n\nconst KEY_PATTERN = /^[a-z][a-z0-9-.]{1,79}$/;\n\n/**\n * Validates a flag register and returns a frozen, normalized copy.\n *\n * Throws `FlagRegistryError` with a `code` for the first invalid entry.\n * A flag kind with a finite maximum lifetime requires `expiresAt`, so that a flag\n * cannot escape the limit by omitting the date.\n */\nexport function defineFlags(\n\tdefinitions: readonly FlagDefinition[],\n\toptions?: DefineFlagsOptions,\n): readonly FlagDefinition[] {\n\tif (!Array.isArray(definitions)) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", \"defineFlags expects an array of flags\");\n\t}\n\tconst maxLifetimeDays = resolveMaxLifetimeDays(options?.maxLifetimeDays);\n\n\tconst seen = new Set<string>();\n\tconst result: FlagDefinition[] = [];\n\tfor (const [index, raw] of definitions.entries()) {\n\t\tconst flag = validateDefinition(raw, index, maxLifetimeDays);\n\t\tif (seen.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tseen.add(flag.key);\n\t\tresult.push(flag);\n\t}\n\treturn Object.freeze(result);\n}\n\nfunction validateDefinition(\n\traw: unknown,\n\tindex: number,\n\tmaxLifetimeDays: MaxLifetimeDays,\n): FlagDefinition {\n\tif (typeof raw !== \"object\" || raw === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", `Flag at index ${index} is not an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\n\tconst key = record.key;\n\tif (typeof key !== \"string\" || !KEY_PATTERN.test(key)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KEY\",\n\t\t\t`Invalid flag key at index ${index}: ${JSON.stringify(key)} (must match ${KEY_PATTERN.source})`,\n\t\t\ttypeof key === \"string\" ? key : undefined,\n\t\t);\n\t}\n\n\tconst owner = record.owner;\n\tif (typeof owner !== \"string\" || owner.trim() === \"\") {\n\t\tthrow new FlagRegistryError(\"EMPTY_OWNER\", `Flag \"${key}\" has no owner`, key);\n\t}\n\n\tconst kind = record.kind;\n\tif (!isFlagKind(kind)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KIND\",\n\t\t\t`Flag \"${key}\" has invalid kind ${JSON.stringify(kind)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst createdAt = record.createdAt;\n\tconst createdMs = parseIsoDate(createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${key}\" has invalid createdAt ${JSON.stringify(createdAt)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst expiresAt = record.expiresAt;\n\tlet expiresMs: number | undefined;\n\tif (expiresAt !== undefined) {\n\t\texpiresMs = parseIsoDate(expiresAt);\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_DATE\",\n\t\t\t\t`Flag \"${key}\" has invalid expiresAt ${JSON.stringify(expiresAt)}`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs <= createdMs) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXPIRY_NOT_AFTER_CREATION\",\n\t\t\t\t`Flag \"${key}\" expires at ${String(expiresAt)}, which is not after createdAt`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst maxDays = maxLifetimeDays[kind];\n\tif (maxDays !== null) {\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"MISSING_EXPIRY\",\n\t\t\t\t`Flag \"${key}\" (${kind}) needs an expiresAt, the maximum lifetime is ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs - createdMs > maxDays * DAY_MS) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\t`Flag \"${key}\" (${kind}) lives longer than the maximum of ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst description = record.description;\n\tif (description !== undefined && typeof description !== \"string\") {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DESCRIPTION\",\n\t\t\t`Flag \"${key}\" has a non-string description`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst flag: FlagDefinition = { key, owner, kind, createdAt: createdAt as string };\n\tif (expiresAt !== undefined) {\n\t\tflag.expiresAt = expiresAt as string;\n\t}\n\tif (description !== undefined) {\n\t\tflag.description = description;\n\t}\n\treturn flag;\n}\n","import type { FindFlagKeysOptions } from \"./types\";\n\nconst DEFAULT_FUNCTIONS: readonly string[] = [\"isEnabled\", \"getFlag\", \"flag\", \"evaluate\"];\n\n/** Upper bound for a key literal. Longer literals are ignored. */\nconst MAX_LITERAL_LENGTH = 100;\n\n/**\n * Finds flag keys in source text.\n *\n * Recognized forms, with single or double quotes:\n * - `isEnabled(\"key\")`, `getFlag('key')` and the other configured function names\n * - `flags.key` (identifier-style keys only)\n * - `flags[\"key\"]`\n *\n * Text in comments, and template literals (backticks), are ignored, including `${...}`\n * placeholders in template literals that are not code. Calls inside `${...}` are still scanned.\n * Calls and property access written inside a quoted string (`\"isEnabled('x')\"`) are ignored,\n * because they are text, not code.\n * This function does not read files. The caller passes the text.\n *\n * Limitations: regex literals containing quotes are not understood. A quote inside a\n * regex literal can hide code on the same line.\n */\nexport function findFlagKeysInSource(\n\tsourceText: string,\n\toptions?: FindFlagKeysOptions,\n): Set<string> {\n\tconst functions = options?.functions ?? DEFAULT_FUNCTIONS;\n\tconst { code, stringRanges } = maskWithStrings(sourceText);\n\tconst keys = new Set<string>();\n\n\tif (functions.length > 0) {\n\t\tconst alternation = functions.map(escapeRegExp).join(\"|\");\n\t\t// The literal must be closed by the same quote and be a whole argument, so that\n\t\t// `isEnabled(\"a\" + b)` is not read as the key \"a\".\n\t\tconst callPattern = new RegExp(\n\t\t\tString.raw`\\b(?:${alternation})\\s*\\(\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1(?=\\s*[,)])`,\n\t\t\t\"g\",\n\t\t);\n\t\tfor (const match of code.matchAll(callPattern)) {\n\t\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst key = match[2];\n\t\t\tif (key !== undefined) {\n\t\t\t\tkeys.add(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t// `flags.key` is only matched for identifier-like keys, and not for method calls like\n\t// `flags.get(` or chained access like `flags.a.b`.\n\tfor (const match of code.matchAll(/\\bflags\\.([a-z][a-z0-9]*)(?![\\w.-]|\\s*\\()/g)) {\n\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst key = match[1];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\tconst bracketPattern = new RegExp(\n\t\tString.raw`\\bflags\\s*\\[\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1\\s*\\]`,\n\t\t\"g\",\n\t);\n\tfor (const match of code.matchAll(bracketPattern)) {\n\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst key = match[2];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\treturn keys;\n}\n\ntype Frame = { kind: \"template\" } | { kind: \"code\"; braces: number; interpolation: boolean };\n\n/**\n * Returns `source` with comments and template-literal text replaced by spaces.\n * Newlines are kept, so the output has the same length and line structure.\n * Single- and double-quoted strings are kept, because keys are read from them.\n * Runs in linear time: each character is visited a constant number of times.\n */\nexport function maskNonCode(source: string): string {\n\treturn maskWithStrings(source).code;\n}\n\n/**\n * Like `maskNonCode`, and also returns the ranges of quoted string literals as flat\n * `[start, end, start, end, ...]` pairs (end is exclusive), in source order.\n */\nfunction maskWithStrings(source: string): { code: string; stringRanges: number[] } {\n\tconst out = source.split(\"\");\n\tconst n = source.length;\n\tconst stringRanges: number[] = [];\n\tconst frames: Frame[] = [{ kind: \"code\", braces: 0, interpolation: false }];\n\n\tconst mask = (from: number, to: number): void => {\n\t\tfor (let k = from; k < to; k++) {\n\t\t\tif (out[k] !== \"\\n\" && out[k] !== \"\\r\") {\n\t\t\t\tout[k] = \" \";\n\t\t\t}\n\t\t}\n\t};\n\n\tlet i = 0;\n\twhile (i < n) {\n\t\tconst top = frames[frames.length - 1];\n\t\tif (top === undefined) {\n\t\t\tbreak;\n\t\t}\n\t\tconst ch = source[i];\n\t\tconst next = source[i + 1];\n\n\t\tif (top.kind === \"template\") {\n\t\t\tif (ch === \"\\\\\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\ti += 2;\n\t\t\t} else if (ch === \"`\") {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t\ti++;\n\t\t\t} else if (ch === \"$\" && next === \"{\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\tframes.push({ kind: \"code\", braces: 0, interpolation: true });\n\t\t\t\ti += 2;\n\t\t\t} else {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (ch === \"/\" && next === \"/\") {\n\t\t\tconst newline = source.indexOf(\"\\n\", i);\n\t\t\tconst end = newline === -1 ? n : newline;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"/\" && next === \"*\") {\n\t\t\tconst close = source.indexOf(\"*/\", i + 2);\n\t\t\tconst end = close === -1 ? n : close + 2;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === '\"' || ch === \"'\") {\n\t\t\t// Strings end at the matching quote or at a line break (unterminated string).\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < n && source[j] !== ch && source[j] !== \"\\n\") {\n\t\t\t\tj += source[j] === \"\\\\\" ? 2 : 1;\n\t\t\t}\n\t\t\tconst end = source[j] === ch ? j + 1 : j;\n\t\t\tstringRanges.push(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"`\") {\n\t\t\tmask(i, i + 1);\n\t\t\tframes.push({ kind: \"template\" });\n\t\t\ti++;\n\t\t} else if (ch === \"{\") {\n\t\t\ttop.braces++;\n\t\t\ti++;\n\t\t} else if (ch === \"}\") {\n\t\t\tif (top.interpolation && top.braces === 0) {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t} else if (top.braces > 0) {\n\t\t\t\ttop.braces--;\n\t\t\t}\n\t\t\ti++;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn { code: out.join(\"\"), stringRanges };\n}\n\n/** True when `index` lies in one of the sorted `[start, end)` pairs of `ranges`. */\nfunction isInRanges(ranges: readonly number[], index: number | undefined): boolean {\n\tif (index === undefined) {\n\t\treturn false;\n\t}\n\tlet lo = 0;\n\tlet hi = ranges.length / 2 - 1;\n\twhile (lo <= hi) {\n\t\tconst mid = (lo + hi) >> 1;\n\t\tconst start = ranges[mid * 2] ?? 0;\n\t\tconst end = ranges[mid * 2 + 1] ?? 0;\n\t\tif (index < start) {\n\t\t\thi = mid - 1;\n\t\t} else if (index >= end) {\n\t\t\tlo = mid + 1;\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -115,6 +115,8 @@ export declare function defineFlags(definitions: readonly FlagDefinition[], opti
|
|
|
115
115
|
*
|
|
116
116
|
* Text in comments, and template literals (backticks), are ignored, including `${...}`
|
|
117
117
|
* placeholders in template literals that are not code. Calls inside `${...}` are still scanned.
|
|
118
|
+
* Calls and property access written inside a quoted string (`"isEnabled('x')"`) are ignored,
|
|
119
|
+
* because they are text, not code.
|
|
118
120
|
* This function does not read files. The caller passes the text.
|
|
119
121
|
*
|
|
120
122
|
* Limitations: regex literals containing quotes are not understood. A quote inside a
|
package/dist/index.d.ts
CHANGED
|
@@ -115,6 +115,8 @@ export declare function defineFlags(definitions: readonly FlagDefinition[], opti
|
|
|
115
115
|
*
|
|
116
116
|
* Text in comments, and template literals (backticks), are ignored, including `${...}`
|
|
117
117
|
* placeholders in template literals that are not code. Calls inside `${...}` are still scanned.
|
|
118
|
+
* Calls and property access written inside a quoted string (`"isEnabled('x')"`) are ignored,
|
|
119
|
+
* because they are text, not code.
|
|
118
120
|
* This function does not read files. The caller passes the text.
|
|
119
121
|
*
|
|
120
122
|
* Limitations: regex literals containing quotes are not understood. A quote inside a
|
package/dist/index.js
CHANGED
|
@@ -276,6 +276,8 @@ const MAX_LITERAL_LENGTH = 100;
|
|
|
276
276
|
*
|
|
277
277
|
* Text in comments, and template literals (backticks), are ignored, including `${...}`
|
|
278
278
|
* placeholders in template literals that are not code. Calls inside `${...}` are still scanned.
|
|
279
|
+
* Calls and property access written inside a quoted string (`"isEnabled('x')"`) are ignored,
|
|
280
|
+
* because they are text, not code.
|
|
279
281
|
* This function does not read files. The caller passes the text.
|
|
280
282
|
*
|
|
281
283
|
* Limitations: regex literals containing quotes are not understood. A quote inside a
|
|
@@ -283,36 +285,38 @@ const MAX_LITERAL_LENGTH = 100;
|
|
|
283
285
|
*/
|
|
284
286
|
function findFlagKeysInSource(sourceText, options) {
|
|
285
287
|
const functions = options?.functions ?? DEFAULT_FUNCTIONS;
|
|
286
|
-
const code =
|
|
288
|
+
const { code, stringRanges } = maskWithStrings(sourceText);
|
|
287
289
|
const keys = /* @__PURE__ */ new Set();
|
|
288
290
|
if (functions.length > 0) {
|
|
289
291
|
const alternation = functions.map(escapeRegExp).join("|");
|
|
290
292
|
const callPattern = new RegExp(String.raw`\b(?:${alternation})\s*\(\s*(["'])([^"'\\\r\n]{1,${MAX_LITERAL_LENGTH}})\1(?=\s*[,)])`, "g");
|
|
291
293
|
for (const match of code.matchAll(callPattern)) {
|
|
294
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
292
295
|
const key = match[2];
|
|
293
296
|
if (key !== void 0) keys.add(key);
|
|
294
297
|
}
|
|
295
298
|
}
|
|
296
299
|
for (const match of code.matchAll(/\bflags\.([a-z][a-z0-9]*)(?![\w.-]|\s*\()/g)) {
|
|
300
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
297
301
|
const key = match[1];
|
|
298
302
|
if (key !== void 0) keys.add(key);
|
|
299
303
|
}
|
|
300
304
|
const bracketPattern = new RegExp(String.raw`\bflags\s*\[\s*(["'])([^"'\\\r\n]{1,${MAX_LITERAL_LENGTH}})\1\s*\]`, "g");
|
|
301
305
|
for (const match of code.matchAll(bracketPattern)) {
|
|
306
|
+
if (isInRanges(stringRanges, match.index)) continue;
|
|
302
307
|
const key = match[2];
|
|
303
308
|
if (key !== void 0) keys.add(key);
|
|
304
309
|
}
|
|
305
310
|
return keys;
|
|
306
311
|
}
|
|
307
312
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* Single- and double-quoted strings are kept, because keys are read from them.
|
|
311
|
-
* Runs in linear time: each character is visited a constant number of times.
|
|
313
|
+
* Like `maskNonCode`, and also returns the ranges of quoted string literals as flat
|
|
314
|
+
* `[start, end, start, end, ...]` pairs (end is exclusive), in source order.
|
|
312
315
|
*/
|
|
313
|
-
function
|
|
316
|
+
function maskWithStrings(source) {
|
|
314
317
|
const out = source.split("");
|
|
315
318
|
const n = source.length;
|
|
319
|
+
const stringRanges = [];
|
|
316
320
|
const frames = [{
|
|
317
321
|
kind: "code",
|
|
318
322
|
braces: 0,
|
|
@@ -362,7 +366,9 @@ function maskNonCode(source) {
|
|
|
362
366
|
} else if (ch === "\"" || ch === "'") {
|
|
363
367
|
let j = i + 1;
|
|
364
368
|
while (j < n && source[j] !== ch && source[j] !== "\n") j += source[j] === "\\" ? 2 : 1;
|
|
365
|
-
|
|
369
|
+
const end = source[j] === ch ? j + 1 : j;
|
|
370
|
+
stringRanges.push(i, end);
|
|
371
|
+
i = end;
|
|
366
372
|
} else if (ch === "`") {
|
|
367
373
|
mask(i, i + 1);
|
|
368
374
|
frames.push({ kind: "template" });
|
|
@@ -378,7 +384,25 @@ function maskNonCode(source) {
|
|
|
378
384
|
i++;
|
|
379
385
|
} else i++;
|
|
380
386
|
}
|
|
381
|
-
return
|
|
387
|
+
return {
|
|
388
|
+
code: out.join(""),
|
|
389
|
+
stringRanges
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** True when `index` lies in one of the sorted `[start, end)` pairs of `ranges`. */
|
|
393
|
+
function isInRanges(ranges, index) {
|
|
394
|
+
if (index === void 0) return false;
|
|
395
|
+
let lo = 0;
|
|
396
|
+
let hi = ranges.length / 2 - 1;
|
|
397
|
+
while (lo <= hi) {
|
|
398
|
+
const mid = lo + hi >> 1;
|
|
399
|
+
const start = ranges[mid * 2] ?? 0;
|
|
400
|
+
const end = ranges[mid * 2 + 1] ?? 0;
|
|
401
|
+
if (index < start) hi = mid - 1;
|
|
402
|
+
else if (index >= end) lo = mid + 1;
|
|
403
|
+
else return true;
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
382
406
|
}
|
|
383
407
|
function escapeRegExp(value) {
|
|
384
408
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/dates.ts","../src/lifetimes.ts","../src/hygiene.ts","../src/registry.ts","../src/scan.ts"],"sourcesContent":["import type { HygieneCode, HygieneFinding } from \"./types\";\n\nexport type FlagRegistryErrorCode =\n\t| \"INVALID_DEFINITION\"\n\t| \"INVALID_KEY\"\n\t| \"DUPLICATE_KEY\"\n\t| \"EMPTY_OWNER\"\n\t| \"INVALID_KIND\"\n\t| \"INVALID_DESCRIPTION\"\n\t| \"INVALID_DATE\"\n\t| \"EXPIRY_NOT_AFTER_CREATION\"\n\t| \"MISSING_EXPIRY\"\n\t| \"EXCEEDS_MAX_LIFETIME\"\n\t| \"INVALID_OPTIONS\";\n\n/**\n * Thrown when a flag register or its options are invalid.\n */\nexport class FlagRegistryError extends Error {\n\treadonly code: FlagRegistryErrorCode;\n\t/** The offending flag key, when it is known. */\n\treadonly key: string | undefined;\n\n\tconstructor(code: FlagRegistryErrorCode, message: string, key?: string) {\n\t\tsuper(message);\n\t\tthis.name = \"FlagRegistryError\";\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n\n/**\n * Thrown by `assertFlagHygiene` when findings match the `failOn` codes.\n */\nexport class FlagHygieneError extends Error {\n\treadonly code: HygieneCode;\n\treadonly findings: readonly HygieneFinding[];\n\n\tconstructor(findings: readonly HygieneFinding[]) {\n\t\tconst lines = findings.map((finding) => ` - [${finding.code}] ${finding.message}`);\n\t\tsuper(`Flag hygiene check failed with ${findings.length} finding(s):\\n${lines.join(\"\\n\")}`);\n\t\tthis.name = \"FlagHygieneError\";\n\t\tthis.code = findings[0]?.code ?? \"UNUSED\";\n\t\tthis.findings = findings;\n\t}\n}\n","export const DAY_MS = 86_400_000;\n\n/**\n * `YYYY-MM-DD`, optionally followed by `T HH:MM[:SS[.fraction]]` and a mandatory timezone.\n * Only fixed quantifiers and a single optional group, so matching is linear.\n */\nconst ISO_PATTERN =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}:\\d{2}))?$/;\n\nfunction isLeapYear(year: number): boolean {\n\treturn (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInMonth(year: number, month: number): number {\n\tconst days = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\treturn days[month - 1] ?? 0;\n}\n\n/**\n * Parses a strict ISO 8601 date or date-time and returns epoch milliseconds.\n * Returns `undefined` for anything that is not a real calendar date and time.\n */\nexport function parseIsoDate(value: unknown): number | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst match = ISO_PATTERN.exec(value);\n\tif (match === null) {\n\t\treturn undefined;\n\t}\n\n\tconst year = Number(match[1]);\n\tconst month = Number(match[2]);\n\tconst day = Number(match[3]);\n\tif (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {\n\t\treturn undefined;\n\t}\n\n\tif (match[4] !== undefined) {\n\t\tconst hours = Number(match[4]);\n\t\tconst minutes = Number(match[5]);\n\t\tconst seconds = match[6] === undefined ? 0 : Number(match[6]);\n\t\tif (hours > 23 || minutes > 59 || seconds > 59) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst offset = match[8];\n\t\tif (offset !== undefined && offset !== \"Z\") {\n\t\t\tconst offsetHours = Number(offset.slice(1, 3));\n\t\t\tconst offsetMinutes = Number(offset.slice(4, 6));\n\t\t\tif (offsetHours > 23 || offsetMinutes > 59) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst time = Date.parse(value);\n\treturn Number.isNaN(time) ? undefined : time;\n}\n","import { FlagRegistryError } from \"./errors\";\nimport type { FlagKind, LifetimeOverrides, MaxLifetimeDays } from \"./types\";\n\nexport const FLAG_KINDS: readonly FlagKind[] = [\"release\", \"experiment\", \"ops\", \"permission\"];\n\n/**\n * Default maximum lifetimes. `permission` flags may live without limit.\n */\nexport const DEFAULT_MAX_LIFETIME_DAYS: MaxLifetimeDays = Object.freeze({\n\trelease: 90,\n\texperiment: 30,\n\tops: 365,\n\tpermission: null,\n});\n\nexport function isFlagKind(value: unknown): value is FlagKind {\n\treturn typeof value === \"string\" && (FLAG_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * Merges user overrides into the defaults and validates them.\n */\nexport function resolveMaxLifetimeDays(overrides: LifetimeOverrides | undefined): MaxLifetimeDays {\n\tconst resolved: Record<FlagKind, number | null> = { ...DEFAULT_MAX_LIFETIME_DAYS };\n\tif (overrides === undefined) {\n\t\treturn resolved;\n\t}\n\tif (typeof overrides !== \"object\" || overrides === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"maxLifetimeDays must be an object\");\n\t}\n\n\tfor (const kind of Object.keys(overrides)) {\n\t\tif (!isFlagKind(kind)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`Unknown flag kind in maxLifetimeDays: \"${kind}\"`,\n\t\t\t);\n\t\t}\n\t\tconst value = overrides[kind];\n\t\tif (value === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (value !== null && !(Number.isInteger(value) && value > 0)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`maxLifetimeDays.${kind} must be a positive integer or null, got ${String(value)}`,\n\t\t\t);\n\t\t}\n\t\tresolved[kind] = value;\n\t}\n\treturn resolved;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagHygieneError, FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type {\n\tAssertFlagHygieneOptions,\n\tCheckFlagHygieneOptions,\n\tFlagDefinition,\n\tHygieneCode,\n\tHygieneFinding,\n} from \"./types\";\n\n/** Order of codes in the result when two findings share a key. */\nconst CODE_ORDER: readonly HygieneCode[] = [\n\t\"EXPIRED\",\n\t\"EXCEEDS_MAX_LIFETIME\",\n\t\"UNUSED\",\n\t\"UNREGISTERED_USAGE\",\n];\n\n/**\n * Checks a flag register for flags that should be cleaned up.\n *\n * - `EXPIRED`: `now` is at or after `expiresAt`.\n * - `EXCEEDS_MAX_LIFETIME`: the flag lives longer than the configured maximum for its kind.\n * This catches flags registered before a limit was lowered.\n * - `UNUSED`: only when `usedKeys` is given. The key is registered but not used.\n * - `UNREGISTERED_USAGE`: only when `usedKeys` is given. The key is used but not registered.\n *\n * Findings are sorted by key, then by code.\n */\nexport function checkFlagHygiene(options: CheckFlagHygieneOptions): readonly HygieneFinding[] {\n\tconst nowMs = resolveNow(options.now);\n\tconst lifetimes = resolveMaxLifetimeDays(options.maxLifetimeDays);\n\n\tconst registered = new Set<string>();\n\tconst findings: HygieneFinding[] = [];\n\n\tfor (const flag of options.flags) {\n\t\tif (registered.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tregistered.add(flag.key);\n\t\tcollectLifecycleFindings(flag, nowMs, lifetimes[flag.kind], findings);\n\t}\n\n\tif (options.usedKeys !== undefined) {\n\t\tconst used = new Set(options.usedKeys);\n\t\tfor (const key of registered) {\n\t\t\tif (!used.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNUSED\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is registered but not used`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const key of used) {\n\t\t\tif (!registered.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNREGISTERED_USAGE\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is used but not registered`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn findings.sort(\n\t\t(a, b) =>\n\t\t\tcompareStrings(a.key, b.key) || CODE_ORDER.indexOf(a.code) - CODE_ORDER.indexOf(b.code),\n\t);\n}\n\n/**\n * Throws `FlagHygieneError` if any finding has a code listed in `failOn`.\n * Defaults to all codes. Use it in tests or CI.\n */\nexport function assertFlagHygiene(\n\tfindings: readonly HygieneFinding[],\n\toptions?: AssertFlagHygieneOptions,\n): void {\n\tconst failOn = new Set<HygieneCode>(options?.failOn ?? CODE_ORDER);\n\tconst failing = findings.filter((finding) => failOn.has(finding.code));\n\tif (failing.length > 0) {\n\t\tthrow new FlagHygieneError(failing);\n\t}\n}\n\nfunction collectLifecycleFindings(\n\tflag: FlagDefinition,\n\tnowMs: number,\n\tmaxDays: number | null | undefined,\n\tfindings: HygieneFinding[],\n): void {\n\tif (!isFlagKind(flag.kind)) {\n\t\tthrow new FlagRegistryError(\"INVALID_KIND\", `Flag \"${flag.key}\" has invalid kind`, flag.key);\n\t}\n\tconst createdMs = parseIsoDate(flag.createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid createdAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\tif (flag.expiresAt === undefined) {\n\t\treturn;\n\t}\n\tconst expiresMs = parseIsoDate(flag.expiresAt);\n\tif (expiresMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid expiresAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\n\tif (nowMs >= expiresMs) {\n\t\tfindings.push({\n\t\t\tkey: flag.key,\n\t\t\tcode: \"EXPIRED\",\n\t\t\tmessage: `Flag \"${flag.key}\" expired at ${flag.expiresAt}`,\n\t\t});\n\t}\n\n\tif (maxDays !== null && maxDays !== undefined) {\n\t\tconst lifetimeMs = expiresMs - createdMs;\n\t\tif (lifetimeMs > maxDays * DAY_MS) {\n\t\t\tfindings.push({\n\t\t\t\tkey: flag.key,\n\t\t\t\tcode: \"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\tmessage: `Flag \"${flag.key}\" (${flag.kind}) lives ${lifetimeMs / DAY_MS} days, the maximum is ${maxDays}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction resolveNow(now: Date | undefined): number {\n\tif (now === undefined) {\n\t\treturn Date.now();\n\t}\n\tconst time = now.getTime();\n\tif (Number.isNaN(time)) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"now is an invalid Date\");\n\t}\n\treturn time;\n}\n\nfunction compareStrings(a: string, b: string): number {\n\tif (a === b) {\n\t\treturn 0;\n\t}\n\treturn a < b ? -1 : 1;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type { DefineFlagsOptions, FlagDefinition, MaxLifetimeDays } from \"./types\";\n\nconst KEY_PATTERN = /^[a-z][a-z0-9-.]{1,79}$/;\n\n/**\n * Validates a flag register and returns a frozen, normalized copy.\n *\n * Throws `FlagRegistryError` with a `code` for the first invalid entry.\n * A flag kind with a finite maximum lifetime requires `expiresAt`, so that a flag\n * cannot escape the limit by omitting the date.\n */\nexport function defineFlags(\n\tdefinitions: readonly FlagDefinition[],\n\toptions?: DefineFlagsOptions,\n): readonly FlagDefinition[] {\n\tif (!Array.isArray(definitions)) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", \"defineFlags expects an array of flags\");\n\t}\n\tconst maxLifetimeDays = resolveMaxLifetimeDays(options?.maxLifetimeDays);\n\n\tconst seen = new Set<string>();\n\tconst result: FlagDefinition[] = [];\n\tfor (const [index, raw] of definitions.entries()) {\n\t\tconst flag = validateDefinition(raw, index, maxLifetimeDays);\n\t\tif (seen.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tseen.add(flag.key);\n\t\tresult.push(flag);\n\t}\n\treturn Object.freeze(result);\n}\n\nfunction validateDefinition(\n\traw: unknown,\n\tindex: number,\n\tmaxLifetimeDays: MaxLifetimeDays,\n): FlagDefinition {\n\tif (typeof raw !== \"object\" || raw === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", `Flag at index ${index} is not an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\n\tconst key = record.key;\n\tif (typeof key !== \"string\" || !KEY_PATTERN.test(key)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KEY\",\n\t\t\t`Invalid flag key at index ${index}: ${JSON.stringify(key)} (must match ${KEY_PATTERN.source})`,\n\t\t\ttypeof key === \"string\" ? key : undefined,\n\t\t);\n\t}\n\n\tconst owner = record.owner;\n\tif (typeof owner !== \"string\" || owner.trim() === \"\") {\n\t\tthrow new FlagRegistryError(\"EMPTY_OWNER\", `Flag \"${key}\" has no owner`, key);\n\t}\n\n\tconst kind = record.kind;\n\tif (!isFlagKind(kind)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KIND\",\n\t\t\t`Flag \"${key}\" has invalid kind ${JSON.stringify(kind)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst createdAt = record.createdAt;\n\tconst createdMs = parseIsoDate(createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${key}\" has invalid createdAt ${JSON.stringify(createdAt)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst expiresAt = record.expiresAt;\n\tlet expiresMs: number | undefined;\n\tif (expiresAt !== undefined) {\n\t\texpiresMs = parseIsoDate(expiresAt);\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_DATE\",\n\t\t\t\t`Flag \"${key}\" has invalid expiresAt ${JSON.stringify(expiresAt)}`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs <= createdMs) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXPIRY_NOT_AFTER_CREATION\",\n\t\t\t\t`Flag \"${key}\" expires at ${String(expiresAt)}, which is not after createdAt`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst maxDays = maxLifetimeDays[kind];\n\tif (maxDays !== null) {\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"MISSING_EXPIRY\",\n\t\t\t\t`Flag \"${key}\" (${kind}) needs an expiresAt, the maximum lifetime is ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs - createdMs > maxDays * DAY_MS) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\t`Flag \"${key}\" (${kind}) lives longer than the maximum of ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst description = record.description;\n\tif (description !== undefined && typeof description !== \"string\") {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DESCRIPTION\",\n\t\t\t`Flag \"${key}\" has a non-string description`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst flag: FlagDefinition = { key, owner, kind, createdAt: createdAt as string };\n\tif (expiresAt !== undefined) {\n\t\tflag.expiresAt = expiresAt as string;\n\t}\n\tif (description !== undefined) {\n\t\tflag.description = description;\n\t}\n\treturn flag;\n}\n","import type { FindFlagKeysOptions } from \"./types\";\n\nconst DEFAULT_FUNCTIONS: readonly string[] = [\"isEnabled\", \"getFlag\", \"flag\", \"evaluate\"];\n\n/** Upper bound for a key literal. Longer literals are ignored. */\nconst MAX_LITERAL_LENGTH = 100;\n\n/**\n * Finds flag keys in source text.\n *\n * Recognized forms, with single or double quotes:\n * - `isEnabled(\"key\")`, `getFlag('key')` and the other configured function names\n * - `flags.key` (identifier-style keys only)\n * - `flags[\"key\"]`\n *\n * Text in comments, and template literals (backticks), are ignored, including `${...}`\n * placeholders in template literals that are not code. Calls inside `${...}` are still scanned.\n * This function does not read files. The caller passes the text.\n *\n * Limitations: regex literals containing quotes are not understood. A quote inside a\n * regex literal can hide code on the same line.\n */\nexport function findFlagKeysInSource(\n\tsourceText: string,\n\toptions?: FindFlagKeysOptions,\n): Set<string> {\n\tconst functions = options?.functions ?? DEFAULT_FUNCTIONS;\n\tconst code = maskNonCode(sourceText);\n\tconst keys = new Set<string>();\n\n\tif (functions.length > 0) {\n\t\tconst alternation = functions.map(escapeRegExp).join(\"|\");\n\t\t// The literal must be closed by the same quote and be a whole argument, so that\n\t\t// `isEnabled(\"a\" + b)` is not read as the key \"a\".\n\t\tconst callPattern = new RegExp(\n\t\t\tString.raw`\\b(?:${alternation})\\s*\\(\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1(?=\\s*[,)])`,\n\t\t\t\"g\",\n\t\t);\n\t\tfor (const match of code.matchAll(callPattern)) {\n\t\t\tconst key = match[2];\n\t\t\tif (key !== undefined) {\n\t\t\t\tkeys.add(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t// `flags.key` is only matched for identifier-like keys, and not for method calls like\n\t// `flags.get(` or chained access like `flags.a.b`.\n\tfor (const match of code.matchAll(/\\bflags\\.([a-z][a-z0-9]*)(?![\\w.-]|\\s*\\()/g)) {\n\t\tconst key = match[1];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\tconst bracketPattern = new RegExp(\n\t\tString.raw`\\bflags\\s*\\[\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1\\s*\\]`,\n\t\t\"g\",\n\t);\n\tfor (const match of code.matchAll(bracketPattern)) {\n\t\tconst key = match[2];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\treturn keys;\n}\n\ntype Frame = { kind: \"template\" } | { kind: \"code\"; braces: number; interpolation: boolean };\n\n/**\n * Returns `source` with comments and template-literal text replaced by spaces.\n * Newlines are kept, so the output has the same length and line structure.\n * Single- and double-quoted strings are kept, because keys are read from them.\n * Runs in linear time: each character is visited a constant number of times.\n */\nexport function maskNonCode(source: string): string {\n\tconst out = source.split(\"\");\n\tconst n = source.length;\n\tconst frames: Frame[] = [{ kind: \"code\", braces: 0, interpolation: false }];\n\n\tconst mask = (from: number, to: number): void => {\n\t\tfor (let k = from; k < to; k++) {\n\t\t\tif (out[k] !== \"\\n\" && out[k] !== \"\\r\") {\n\t\t\t\tout[k] = \" \";\n\t\t\t}\n\t\t}\n\t};\n\n\tlet i = 0;\n\twhile (i < n) {\n\t\tconst top = frames[frames.length - 1];\n\t\tif (top === undefined) {\n\t\t\tbreak;\n\t\t}\n\t\tconst ch = source[i];\n\t\tconst next = source[i + 1];\n\n\t\tif (top.kind === \"template\") {\n\t\t\tif (ch === \"\\\\\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\ti += 2;\n\t\t\t} else if (ch === \"`\") {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t\ti++;\n\t\t\t} else if (ch === \"$\" && next === \"{\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\tframes.push({ kind: \"code\", braces: 0, interpolation: true });\n\t\t\t\ti += 2;\n\t\t\t} else {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (ch === \"/\" && next === \"/\") {\n\t\t\tconst newline = source.indexOf(\"\\n\", i);\n\t\t\tconst end = newline === -1 ? n : newline;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"/\" && next === \"*\") {\n\t\t\tconst close = source.indexOf(\"*/\", i + 2);\n\t\t\tconst end = close === -1 ? n : close + 2;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === '\"' || ch === \"'\") {\n\t\t\t// Strings end at the matching quote or at a line break (unterminated string).\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < n && source[j] !== ch && source[j] !== \"\\n\") {\n\t\t\t\tj += source[j] === \"\\\\\" ? 2 : 1;\n\t\t\t}\n\t\t\ti = source[j] === ch ? j + 1 : j;\n\t\t} else if (ch === \"`\") {\n\t\t\tmask(i, i + 1);\n\t\t\tframes.push({ kind: \"template\" });\n\t\t\ti++;\n\t\t} else if (ch === \"{\") {\n\t\t\ttop.braces++;\n\t\t\ti++;\n\t\t} else if (ch === \"}\") {\n\t\t\tif (top.interpolation && top.braces === 0) {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t} else if (top.braces > 0) {\n\t\t\t\ttop.braces--;\n\t\t\t}\n\t\t\ti++;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn out.join(\"\");\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/dates.ts","../src/lifetimes.ts","../src/hygiene.ts","../src/registry.ts","../src/scan.ts"],"sourcesContent":["import type { HygieneCode, HygieneFinding } from \"./types\";\n\nexport type FlagRegistryErrorCode =\n\t| \"INVALID_DEFINITION\"\n\t| \"INVALID_KEY\"\n\t| \"DUPLICATE_KEY\"\n\t| \"EMPTY_OWNER\"\n\t| \"INVALID_KIND\"\n\t| \"INVALID_DESCRIPTION\"\n\t| \"INVALID_DATE\"\n\t| \"EXPIRY_NOT_AFTER_CREATION\"\n\t| \"MISSING_EXPIRY\"\n\t| \"EXCEEDS_MAX_LIFETIME\"\n\t| \"INVALID_OPTIONS\";\n\n/**\n * Thrown when a flag register or its options are invalid.\n */\nexport class FlagRegistryError extends Error {\n\treadonly code: FlagRegistryErrorCode;\n\t/** The offending flag key, when it is known. */\n\treadonly key: string | undefined;\n\n\tconstructor(code: FlagRegistryErrorCode, message: string, key?: string) {\n\t\tsuper(message);\n\t\tthis.name = \"FlagRegistryError\";\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n\n/**\n * Thrown by `assertFlagHygiene` when findings match the `failOn` codes.\n */\nexport class FlagHygieneError extends Error {\n\treadonly code: HygieneCode;\n\treadonly findings: readonly HygieneFinding[];\n\n\tconstructor(findings: readonly HygieneFinding[]) {\n\t\tconst lines = findings.map((finding) => ` - [${finding.code}] ${finding.message}`);\n\t\tsuper(`Flag hygiene check failed with ${findings.length} finding(s):\\n${lines.join(\"\\n\")}`);\n\t\tthis.name = \"FlagHygieneError\";\n\t\tthis.code = findings[0]?.code ?? \"UNUSED\";\n\t\tthis.findings = findings;\n\t}\n}\n","export const DAY_MS = 86_400_000;\n\n/**\n * `YYYY-MM-DD`, optionally followed by `T HH:MM[:SS[.fraction]]` and a mandatory timezone.\n * Only fixed quantifiers and a single optional group, so matching is linear.\n */\nconst ISO_PATTERN =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}:\\d{2}))?$/;\n\nfunction isLeapYear(year: number): boolean {\n\treturn (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInMonth(year: number, month: number): number {\n\tconst days = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\treturn days[month - 1] ?? 0;\n}\n\n/**\n * Parses a strict ISO 8601 date or date-time and returns epoch milliseconds.\n * Returns `undefined` for anything that is not a real calendar date and time.\n */\nexport function parseIsoDate(value: unknown): number | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst match = ISO_PATTERN.exec(value);\n\tif (match === null) {\n\t\treturn undefined;\n\t}\n\n\tconst year = Number(match[1]);\n\tconst month = Number(match[2]);\n\tconst day = Number(match[3]);\n\tif (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {\n\t\treturn undefined;\n\t}\n\n\tif (match[4] !== undefined) {\n\t\tconst hours = Number(match[4]);\n\t\tconst minutes = Number(match[5]);\n\t\tconst seconds = match[6] === undefined ? 0 : Number(match[6]);\n\t\tif (hours > 23 || minutes > 59 || seconds > 59) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst offset = match[8];\n\t\tif (offset !== undefined && offset !== \"Z\") {\n\t\t\tconst offsetHours = Number(offset.slice(1, 3));\n\t\t\tconst offsetMinutes = Number(offset.slice(4, 6));\n\t\t\tif (offsetHours > 23 || offsetMinutes > 59) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst time = Date.parse(value);\n\treturn Number.isNaN(time) ? undefined : time;\n}\n","import { FlagRegistryError } from \"./errors\";\nimport type { FlagKind, LifetimeOverrides, MaxLifetimeDays } from \"./types\";\n\nexport const FLAG_KINDS: readonly FlagKind[] = [\"release\", \"experiment\", \"ops\", \"permission\"];\n\n/**\n * Default maximum lifetimes. `permission` flags may live without limit.\n */\nexport const DEFAULT_MAX_LIFETIME_DAYS: MaxLifetimeDays = Object.freeze({\n\trelease: 90,\n\texperiment: 30,\n\tops: 365,\n\tpermission: null,\n});\n\nexport function isFlagKind(value: unknown): value is FlagKind {\n\treturn typeof value === \"string\" && (FLAG_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * Merges user overrides into the defaults and validates them.\n */\nexport function resolveMaxLifetimeDays(overrides: LifetimeOverrides | undefined): MaxLifetimeDays {\n\tconst resolved: Record<FlagKind, number | null> = { ...DEFAULT_MAX_LIFETIME_DAYS };\n\tif (overrides === undefined) {\n\t\treturn resolved;\n\t}\n\tif (typeof overrides !== \"object\" || overrides === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"maxLifetimeDays must be an object\");\n\t}\n\n\tfor (const kind of Object.keys(overrides)) {\n\t\tif (!isFlagKind(kind)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`Unknown flag kind in maxLifetimeDays: \"${kind}\"`,\n\t\t\t);\n\t\t}\n\t\tconst value = overrides[kind];\n\t\tif (value === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (value !== null && !(Number.isInteger(value) && value > 0)) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_OPTIONS\",\n\t\t\t\t`maxLifetimeDays.${kind} must be a positive integer or null, got ${String(value)}`,\n\t\t\t);\n\t\t}\n\t\tresolved[kind] = value;\n\t}\n\treturn resolved;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagHygieneError, FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type {\n\tAssertFlagHygieneOptions,\n\tCheckFlagHygieneOptions,\n\tFlagDefinition,\n\tHygieneCode,\n\tHygieneFinding,\n} from \"./types\";\n\n/** Order of codes in the result when two findings share a key. */\nconst CODE_ORDER: readonly HygieneCode[] = [\n\t\"EXPIRED\",\n\t\"EXCEEDS_MAX_LIFETIME\",\n\t\"UNUSED\",\n\t\"UNREGISTERED_USAGE\",\n];\n\n/**\n * Checks a flag register for flags that should be cleaned up.\n *\n * - `EXPIRED`: `now` is at or after `expiresAt`.\n * - `EXCEEDS_MAX_LIFETIME`: the flag lives longer than the configured maximum for its kind.\n * This catches flags registered before a limit was lowered.\n * - `UNUSED`: only when `usedKeys` is given. The key is registered but not used.\n * - `UNREGISTERED_USAGE`: only when `usedKeys` is given. The key is used but not registered.\n *\n * Findings are sorted by key, then by code.\n */\nexport function checkFlagHygiene(options: CheckFlagHygieneOptions): readonly HygieneFinding[] {\n\tconst nowMs = resolveNow(options.now);\n\tconst lifetimes = resolveMaxLifetimeDays(options.maxLifetimeDays);\n\n\tconst registered = new Set<string>();\n\tconst findings: HygieneFinding[] = [];\n\n\tfor (const flag of options.flags) {\n\t\tif (registered.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tregistered.add(flag.key);\n\t\tcollectLifecycleFindings(flag, nowMs, lifetimes[flag.kind], findings);\n\t}\n\n\tif (options.usedKeys !== undefined) {\n\t\tconst used = new Set(options.usedKeys);\n\t\tfor (const key of registered) {\n\t\t\tif (!used.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNUSED\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is registered but not used`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const key of used) {\n\t\t\tif (!registered.has(key)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tkey,\n\t\t\t\t\tcode: \"UNREGISTERED_USAGE\",\n\t\t\t\t\tmessage: `Flag \"${key}\" is used but not registered`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn findings.sort(\n\t\t(a, b) =>\n\t\t\tcompareStrings(a.key, b.key) || CODE_ORDER.indexOf(a.code) - CODE_ORDER.indexOf(b.code),\n\t);\n}\n\n/**\n * Throws `FlagHygieneError` if any finding has a code listed in `failOn`.\n * Defaults to all codes. Use it in tests or CI.\n */\nexport function assertFlagHygiene(\n\tfindings: readonly HygieneFinding[],\n\toptions?: AssertFlagHygieneOptions,\n): void {\n\tconst failOn = new Set<HygieneCode>(options?.failOn ?? CODE_ORDER);\n\tconst failing = findings.filter((finding) => failOn.has(finding.code));\n\tif (failing.length > 0) {\n\t\tthrow new FlagHygieneError(failing);\n\t}\n}\n\nfunction collectLifecycleFindings(\n\tflag: FlagDefinition,\n\tnowMs: number,\n\tmaxDays: number | null | undefined,\n\tfindings: HygieneFinding[],\n): void {\n\tif (!isFlagKind(flag.kind)) {\n\t\tthrow new FlagRegistryError(\"INVALID_KIND\", `Flag \"${flag.key}\" has invalid kind`, flag.key);\n\t}\n\tconst createdMs = parseIsoDate(flag.createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid createdAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\tif (flag.expiresAt === undefined) {\n\t\treturn;\n\t}\n\tconst expiresMs = parseIsoDate(flag.expiresAt);\n\tif (expiresMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${flag.key}\" has invalid expiresAt`,\n\t\t\tflag.key,\n\t\t);\n\t}\n\n\tif (nowMs >= expiresMs) {\n\t\tfindings.push({\n\t\t\tkey: flag.key,\n\t\t\tcode: \"EXPIRED\",\n\t\t\tmessage: `Flag \"${flag.key}\" expired at ${flag.expiresAt}`,\n\t\t});\n\t}\n\n\tif (maxDays !== null && maxDays !== undefined) {\n\t\tconst lifetimeMs = expiresMs - createdMs;\n\t\tif (lifetimeMs > maxDays * DAY_MS) {\n\t\t\tfindings.push({\n\t\t\t\tkey: flag.key,\n\t\t\t\tcode: \"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\tmessage: `Flag \"${flag.key}\" (${flag.kind}) lives ${lifetimeMs / DAY_MS} days, the maximum is ${maxDays}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction resolveNow(now: Date | undefined): number {\n\tif (now === undefined) {\n\t\treturn Date.now();\n\t}\n\tconst time = now.getTime();\n\tif (Number.isNaN(time)) {\n\t\tthrow new FlagRegistryError(\"INVALID_OPTIONS\", \"now is an invalid Date\");\n\t}\n\treturn time;\n}\n\nfunction compareStrings(a: string, b: string): number {\n\tif (a === b) {\n\t\treturn 0;\n\t}\n\treturn a < b ? -1 : 1;\n}\n","import { DAY_MS, parseIsoDate } from \"./dates\";\nimport { FlagRegistryError } from \"./errors\";\nimport { isFlagKind, resolveMaxLifetimeDays } from \"./lifetimes\";\nimport type { DefineFlagsOptions, FlagDefinition, MaxLifetimeDays } from \"./types\";\n\nconst KEY_PATTERN = /^[a-z][a-z0-9-.]{1,79}$/;\n\n/**\n * Validates a flag register and returns a frozen, normalized copy.\n *\n * Throws `FlagRegistryError` with a `code` for the first invalid entry.\n * A flag kind with a finite maximum lifetime requires `expiresAt`, so that a flag\n * cannot escape the limit by omitting the date.\n */\nexport function defineFlags(\n\tdefinitions: readonly FlagDefinition[],\n\toptions?: DefineFlagsOptions,\n): readonly FlagDefinition[] {\n\tif (!Array.isArray(definitions)) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", \"defineFlags expects an array of flags\");\n\t}\n\tconst maxLifetimeDays = resolveMaxLifetimeDays(options?.maxLifetimeDays);\n\n\tconst seen = new Set<string>();\n\tconst result: FlagDefinition[] = [];\n\tfor (const [index, raw] of definitions.entries()) {\n\t\tconst flag = validateDefinition(raw, index, maxLifetimeDays);\n\t\tif (seen.has(flag.key)) {\n\t\t\tthrow new FlagRegistryError(\"DUPLICATE_KEY\", `Duplicate flag key \"${flag.key}\"`, flag.key);\n\t\t}\n\t\tseen.add(flag.key);\n\t\tresult.push(flag);\n\t}\n\treturn Object.freeze(result);\n}\n\nfunction validateDefinition(\n\traw: unknown,\n\tindex: number,\n\tmaxLifetimeDays: MaxLifetimeDays,\n): FlagDefinition {\n\tif (typeof raw !== \"object\" || raw === null) {\n\t\tthrow new FlagRegistryError(\"INVALID_DEFINITION\", `Flag at index ${index} is not an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\n\tconst key = record.key;\n\tif (typeof key !== \"string\" || !KEY_PATTERN.test(key)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KEY\",\n\t\t\t`Invalid flag key at index ${index}: ${JSON.stringify(key)} (must match ${KEY_PATTERN.source})`,\n\t\t\ttypeof key === \"string\" ? key : undefined,\n\t\t);\n\t}\n\n\tconst owner = record.owner;\n\tif (typeof owner !== \"string\" || owner.trim() === \"\") {\n\t\tthrow new FlagRegistryError(\"EMPTY_OWNER\", `Flag \"${key}\" has no owner`, key);\n\t}\n\n\tconst kind = record.kind;\n\tif (!isFlagKind(kind)) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_KIND\",\n\t\t\t`Flag \"${key}\" has invalid kind ${JSON.stringify(kind)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst createdAt = record.createdAt;\n\tconst createdMs = parseIsoDate(createdAt);\n\tif (createdMs === undefined) {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DATE\",\n\t\t\t`Flag \"${key}\" has invalid createdAt ${JSON.stringify(createdAt)}`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst expiresAt = record.expiresAt;\n\tlet expiresMs: number | undefined;\n\tif (expiresAt !== undefined) {\n\t\texpiresMs = parseIsoDate(expiresAt);\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"INVALID_DATE\",\n\t\t\t\t`Flag \"${key}\" has invalid expiresAt ${JSON.stringify(expiresAt)}`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs <= createdMs) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXPIRY_NOT_AFTER_CREATION\",\n\t\t\t\t`Flag \"${key}\" expires at ${String(expiresAt)}, which is not after createdAt`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst maxDays = maxLifetimeDays[kind];\n\tif (maxDays !== null) {\n\t\tif (expiresMs === undefined) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"MISSING_EXPIRY\",\n\t\t\t\t`Flag \"${key}\" (${kind}) needs an expiresAt, the maximum lifetime is ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t\tif (expiresMs - createdMs > maxDays * DAY_MS) {\n\t\t\tthrow new FlagRegistryError(\n\t\t\t\t\"EXCEEDS_MAX_LIFETIME\",\n\t\t\t\t`Flag \"${key}\" (${kind}) lives longer than the maximum of ${maxDays} days`,\n\t\t\t\tkey,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst description = record.description;\n\tif (description !== undefined && typeof description !== \"string\") {\n\t\tthrow new FlagRegistryError(\n\t\t\t\"INVALID_DESCRIPTION\",\n\t\t\t`Flag \"${key}\" has a non-string description`,\n\t\t\tkey,\n\t\t);\n\t}\n\n\tconst flag: FlagDefinition = { key, owner, kind, createdAt: createdAt as string };\n\tif (expiresAt !== undefined) {\n\t\tflag.expiresAt = expiresAt as string;\n\t}\n\tif (description !== undefined) {\n\t\tflag.description = description;\n\t}\n\treturn flag;\n}\n","import type { FindFlagKeysOptions } from \"./types\";\n\nconst DEFAULT_FUNCTIONS: readonly string[] = [\"isEnabled\", \"getFlag\", \"flag\", \"evaluate\"];\n\n/** Upper bound for a key literal. Longer literals are ignored. */\nconst MAX_LITERAL_LENGTH = 100;\n\n/**\n * Finds flag keys in source text.\n *\n * Recognized forms, with single or double quotes:\n * - `isEnabled(\"key\")`, `getFlag('key')` and the other configured function names\n * - `flags.key` (identifier-style keys only)\n * - `flags[\"key\"]`\n *\n * Text in comments, and template literals (backticks), are ignored, including `${...}`\n * placeholders in template literals that are not code. Calls inside `${...}` are still scanned.\n * Calls and property access written inside a quoted string (`\"isEnabled('x')\"`) are ignored,\n * because they are text, not code.\n * This function does not read files. The caller passes the text.\n *\n * Limitations: regex literals containing quotes are not understood. A quote inside a\n * regex literal can hide code on the same line.\n */\nexport function findFlagKeysInSource(\n\tsourceText: string,\n\toptions?: FindFlagKeysOptions,\n): Set<string> {\n\tconst functions = options?.functions ?? DEFAULT_FUNCTIONS;\n\tconst { code, stringRanges } = maskWithStrings(sourceText);\n\tconst keys = new Set<string>();\n\n\tif (functions.length > 0) {\n\t\tconst alternation = functions.map(escapeRegExp).join(\"|\");\n\t\t// The literal must be closed by the same quote and be a whole argument, so that\n\t\t// `isEnabled(\"a\" + b)` is not read as the key \"a\".\n\t\tconst callPattern = new RegExp(\n\t\t\tString.raw`\\b(?:${alternation})\\s*\\(\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1(?=\\s*[,)])`,\n\t\t\t\"g\",\n\t\t);\n\t\tfor (const match of code.matchAll(callPattern)) {\n\t\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst key = match[2];\n\t\t\tif (key !== undefined) {\n\t\t\t\tkeys.add(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t// `flags.key` is only matched for identifier-like keys, and not for method calls like\n\t// `flags.get(` or chained access like `flags.a.b`.\n\tfor (const match of code.matchAll(/\\bflags\\.([a-z][a-z0-9]*)(?![\\w.-]|\\s*\\()/g)) {\n\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst key = match[1];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\tconst bracketPattern = new RegExp(\n\t\tString.raw`\\bflags\\s*\\[\\s*([\"'])([^\"'\\\\\\r\\n]{1,${MAX_LITERAL_LENGTH}})\\1\\s*\\]`,\n\t\t\"g\",\n\t);\n\tfor (const match of code.matchAll(bracketPattern)) {\n\t\tif (isInRanges(stringRanges, match.index)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst key = match[2];\n\t\tif (key !== undefined) {\n\t\t\tkeys.add(key);\n\t\t}\n\t}\n\n\treturn keys;\n}\n\ntype Frame = { kind: \"template\" } | { kind: \"code\"; braces: number; interpolation: boolean };\n\n/**\n * Returns `source` with comments and template-literal text replaced by spaces.\n * Newlines are kept, so the output has the same length and line structure.\n * Single- and double-quoted strings are kept, because keys are read from them.\n * Runs in linear time: each character is visited a constant number of times.\n */\nexport function maskNonCode(source: string): string {\n\treturn maskWithStrings(source).code;\n}\n\n/**\n * Like `maskNonCode`, and also returns the ranges of quoted string literals as flat\n * `[start, end, start, end, ...]` pairs (end is exclusive), in source order.\n */\nfunction maskWithStrings(source: string): { code: string; stringRanges: number[] } {\n\tconst out = source.split(\"\");\n\tconst n = source.length;\n\tconst stringRanges: number[] = [];\n\tconst frames: Frame[] = [{ kind: \"code\", braces: 0, interpolation: false }];\n\n\tconst mask = (from: number, to: number): void => {\n\t\tfor (let k = from; k < to; k++) {\n\t\t\tif (out[k] !== \"\\n\" && out[k] !== \"\\r\") {\n\t\t\t\tout[k] = \" \";\n\t\t\t}\n\t\t}\n\t};\n\n\tlet i = 0;\n\twhile (i < n) {\n\t\tconst top = frames[frames.length - 1];\n\t\tif (top === undefined) {\n\t\t\tbreak;\n\t\t}\n\t\tconst ch = source[i];\n\t\tconst next = source[i + 1];\n\n\t\tif (top.kind === \"template\") {\n\t\t\tif (ch === \"\\\\\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\ti += 2;\n\t\t\t} else if (ch === \"`\") {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t\ti++;\n\t\t\t} else if (ch === \"$\" && next === \"{\") {\n\t\t\t\tmask(i, i + 2);\n\t\t\t\tframes.push({ kind: \"code\", braces: 0, interpolation: true });\n\t\t\t\ti += 2;\n\t\t\t} else {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (ch === \"/\" && next === \"/\") {\n\t\t\tconst newline = source.indexOf(\"\\n\", i);\n\t\t\tconst end = newline === -1 ? n : newline;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"/\" && next === \"*\") {\n\t\t\tconst close = source.indexOf(\"*/\", i + 2);\n\t\t\tconst end = close === -1 ? n : close + 2;\n\t\t\tmask(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === '\"' || ch === \"'\") {\n\t\t\t// Strings end at the matching quote or at a line break (unterminated string).\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < n && source[j] !== ch && source[j] !== \"\\n\") {\n\t\t\t\tj += source[j] === \"\\\\\" ? 2 : 1;\n\t\t\t}\n\t\t\tconst end = source[j] === ch ? j + 1 : j;\n\t\t\tstringRanges.push(i, end);\n\t\t\ti = end;\n\t\t} else if (ch === \"`\") {\n\t\t\tmask(i, i + 1);\n\t\t\tframes.push({ kind: \"template\" });\n\t\t\ti++;\n\t\t} else if (ch === \"{\") {\n\t\t\ttop.braces++;\n\t\t\ti++;\n\t\t} else if (ch === \"}\") {\n\t\t\tif (top.interpolation && top.braces === 0) {\n\t\t\t\tmask(i, i + 1);\n\t\t\t\tframes.pop();\n\t\t\t} else if (top.braces > 0) {\n\t\t\t\ttop.braces--;\n\t\t\t}\n\t\t\ti++;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn { code: out.join(\"\"), stringRanges };\n}\n\n/** True when `index` lies in one of the sorted `[start, end)` pairs of `ranges`. */\nfunction isInRanges(ranges: readonly number[], index: number | undefined): boolean {\n\tif (index === undefined) {\n\t\treturn false;\n\t}\n\tlet lo = 0;\n\tlet hi = ranges.length / 2 - 1;\n\twhile (lo <= hi) {\n\t\tconst mid = (lo + hi) >> 1;\n\t\tconst start = ranges[mid * 2] ?? 0;\n\t\tconst end = ranges[mid * 2 + 1] ?? 0;\n\t\tif (index < start) {\n\t\t\thi = mid - 1;\n\t\t} else if (index >= end) {\n\t\t\tlo = mid + 1;\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,84 +1,82 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
2
|
+
"name": "@yedoma-labs/kulyn-flags",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Feature flag lifecycle registry with owners, expiry dates and CI hygiene checks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"feature-flags",
|
|
29
|
+
"feature-toggles",
|
|
30
|
+
"flag-lifecycle",
|
|
31
|
+
"tech-debt",
|
|
32
|
+
"dead-flags",
|
|
33
|
+
"ci"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/yedoma-labs/kulyn-flags"
|
|
38
|
+
},
|
|
39
|
+
"author": "",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"simple-git-hooks": {
|
|
42
|
+
"pre-commit": "pnpm exec lint-staged",
|
|
43
|
+
"pre-push": "pnpm run typecheck && pnpm run test",
|
|
44
|
+
"commit-msg": "pnpm exec commitlint --edit $1"
|
|
45
|
+
},
|
|
46
|
+
"lint-staged": {
|
|
47
|
+
"*.ts": [
|
|
48
|
+
"biome check --write"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@biomejs/biome": "2.6.3",
|
|
53
|
+
"@commitlint/cli": "^21.2.4",
|
|
54
|
+
"@commitlint/config-conventional": "^21.2.4",
|
|
55
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
56
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
57
|
+
"@semantic-release/exec": "^7.1.0",
|
|
58
|
+
"@semantic-release/git": "^11.0.1",
|
|
59
|
+
"@semantic-release/github": "^12.0.10",
|
|
60
|
+
"@semantic-release/npm": "^13.2.0",
|
|
61
|
+
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
62
|
+
"@types/node": "26.6.5",
|
|
63
|
+
"@typescript/typescript6": "^6.0.2",
|
|
64
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
65
|
+
"lint-staged": "^17.6.0",
|
|
66
|
+
"semantic-release": "^25.0.9",
|
|
67
|
+
"simple-git-hooks": "^2.14.0",
|
|
68
|
+
"tsdown": "^0.23.2",
|
|
69
|
+
"typescript": "7.0.2",
|
|
70
|
+
"vitest": "5.0.5"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsdown",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"test:coverage": "vitest run --coverage",
|
|
77
|
+
"lint": "biome check src tests",
|
|
78
|
+
"lint:fix": "biome check --write src tests",
|
|
79
|
+
"format": "biome format --write src tests",
|
|
80
|
+
"typecheck": "tsc --noEmit"
|
|
81
|
+
}
|
|
82
|
+
}
|