@yukyu30/fluorite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parse.ts","../src/assertion.ts","../src/recorder.ts","../src/check.ts","../src/config.ts","../src/report.ts"],"sourcesContent":["import matter from \"gray-matter\";\n\n/** Outcome of parsing frontmatter out of a Markdown source string. */\nexport interface ParseResult {\n /** Parsed frontmatter data (empty object when none / on error). */\n data: Record<string, unknown>;\n /** The Markdown body following the frontmatter block. */\n content: string;\n /** Whether a frontmatter block was present at all. */\n hasFrontmatter: boolean;\n /** Parse error message, if the frontmatter (YAML) was malformed. */\n error?: string;\n}\n\n/**\n * Extract frontmatter from a Markdown source string.\n *\n * Never throws: malformed YAML is reported via the `error` field so callers\n * can record it as a failure rather than crash.\n */\nexport function parseFrontmatter(source: string): ParseResult {\n const hasFrontmatter = /^?\\s*---\\r?\\n/.test(source);\n try {\n const parsed = matter(source);\n const data = (parsed.data ?? {}) as Record<string, unknown>;\n return {\n data,\n content: parsed.content,\n hasFrontmatter,\n };\n } catch (err) {\n return {\n data: {},\n content: source,\n hasFrontmatter,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { Recorder } from \"./recorder.js\";\nimport type { RuleResult, ValueType } from \"./types.js\";\n\n/** Sentinel meaning \"the key was not present in the frontmatter\". */\nconst MISSING = Symbol(\"missing\");\n\nfunction valueType(value: unknown): ValueType | \"undefined\" {\n if (value === MISSING) return \"undefined\";\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"object\") {\n return t;\n }\n return \"undefined\";\n}\n\nfunction lengthOf(value: unknown): number | undefined {\n if (typeof value === \"string\" || Array.isArray(value)) return value.length;\n return undefined;\n}\n\nfunction display(value: unknown): string {\n if (value === MISSING) return \"undefined\";\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Fluent, chainable assertions for a single frontmatter key.\n *\n * Each terminal matcher records one {@link RuleResult} on the parent\n * {@link Recorder} and returns `this`, so matchers can be chained:\n *\n * ```ts\n * fm.key(\"title\").required().type(\"string\").lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * ```\n *\n * The `.not` modifier negates only the next matcher, then resets.\n */\nexport class KeyAssertion {\n #negated = false;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n private readonly present: boolean,\n ) {}\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n /** The raw value (resolved against the sentinel) used by matchers. */\n private get resolved(): unknown {\n return this.present ? this.value : MISSING;\n }\n\n /**\n * Record a result, applying the pending `.not` negation, then reset it.\n */\n private record(\n rule: string,\n rawPass: boolean,\n describe: (negated: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const ok = negated ? !rawPass : rawPass;\n const result: RuleResult = {\n key: this.key,\n rule,\n ok,\n negated,\n message: describe(negated),\n value: this.present ? this.value : undefined,\n expected,\n };\n this.recorder.push(result);\n return this;\n }\n\n /** Assert the key exists in the frontmatter. */\n required(): this {\n return this.record(\n \"required\",\n this.present,\n (neg) => (neg ? `should not exist` : `is required`),\n );\n }\n\n /** Alias of {@link required}. */\n exists(): this {\n return this.record(\n \"exists\",\n this.present,\n (neg) => (neg ? `should not exist` : `should exist`),\n );\n }\n\n /** Assert the value is of the given type. */\n type(expected: ValueType): this {\n const actual = valueType(this.resolved);\n return this.record(\n \"type\",\n actual === expected,\n (neg) =>\n neg\n ? `should not be of type ${expected}`\n : `should be of type ${expected} (was ${actual})`,\n expected,\n );\n }\n\n /** Assert the value strictly equals `expected` (deep for arrays/objects). */\n eq(expected: unknown): this {\n return this.record(\n \"eq\",\n deepEqual(this.resolved, expected),\n (neg) =>\n neg\n ? `should not equal ${display(expected)}`\n : `should equal ${display(expected)}`,\n expected,\n );\n }\n\n /**\n * Assert the value is an array whose every element is in `allowed` (enum).\n *\n * Designed for catching tag notation drift: define the canonical set once\n * and any stray / mistyped value is reported.\n *\n * ```ts\n * fm.key(\"tags\").subsetOf([\"ok\", \"release\", \"blog\"]);\n * ```\n */\n subsetOf(allowed: readonly unknown[]): this {\n const v = this.resolved;\n const isArray = Array.isArray(v);\n const invalid = isArray\n ? v.filter((el) => !allowed.some((a) => deepEqual(el, a)))\n : [];\n return this.record(\n \"subsetOf\",\n isArray && invalid.length === 0,\n (neg) => {\n if (!isArray) return `should be an array of values from ${display(allowed)}`;\n if (neg) return `should contain values outside ${display(allowed)}`;\n return invalid.length\n ? `all items should be one of ${display(allowed)} (invalid: ${display(invalid)})`\n : `all items should be one of ${display(allowed)}`;\n },\n allowed,\n );\n }\n\n /** Alias of {@link subsetOf}. */\n only(allowed: readonly unknown[]): this {\n return this.subsetOf(allowed);\n }\n\n /**\n * Apply matchers to every element of an array value.\n *\n * ```ts\n * fm.key(\"tags\").each.oneOf([\"ok\", \"release\"]);\n * fm.key(\"tags\").each.matches(/^[a-z0-9-]+$/);\n * ```\n */\n get each(): EachAssertion {\n const negated = this.#negated;\n this.#negated = false;\n return new EachAssertion(this.recorder, this.key, this.resolved, negated);\n }\n\n /** Assert the value is one of `allowed` (enum). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n allowed.some((a) => deepEqual(this.resolved, a)),\n (neg) =>\n neg\n ? `should not be one of ${display(allowed)}`\n : `should be one of ${display(allowed)}`,\n allowed,\n );\n }\n\n /** Assert a string value matches the given regular expression. */\n matches(pattern: RegExp): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && pattern.test(v);\n return this.record(\n \"matches\",\n pass,\n (neg) =>\n neg\n ? `should not match ${pattern}`\n : `should match ${pattern}`,\n pattern.source,\n );\n }\n\n /** Assert an array contains `item`, or a string contains the substring. */\n has(item: unknown): this {\n const v = this.resolved;\n let pass = false;\n if (Array.isArray(v)) pass = v.some((el) => deepEqual(el, item));\n else if (typeof v === \"string\" && typeof item === \"string\")\n pass = v.includes(item);\n return this.record(\n \"has\",\n pass,\n (neg) =>\n neg ? `should not have ${display(item)}` : `should have ${display(item)}`,\n item,\n );\n }\n\n /** Assert an array/string contains all of `items`. */\n hasAll(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.every((item) => contains(v, item));\n return this.record(\n \"hasAll\",\n pass,\n (neg) =>\n neg\n ? `should not have all of ${display(items)}`\n : `should have all of ${display(items)}`,\n items,\n );\n }\n\n /** Assert an array/string contains at least one of `items`. */\n hasAny(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.some((item) => contains(v, item));\n return this.record(\n \"hasAny\",\n pass,\n (neg) =>\n neg\n ? `should not have any of ${display(items)}`\n : `should have any of ${display(items)}`,\n items,\n );\n }\n\n /** Assert the string/array length equals `n`. */\n length(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"length\",\n len === n,\n (neg) =>\n neg\n ? `length should not be ${n} (was ${len ?? \"n/a\"})`\n : `length should be ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at least `n`. */\n lengthMin(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMin\",\n len !== undefined && len >= n,\n (neg) =>\n neg\n ? `length should be < ${n} (was ${len ?? \"n/a\"})`\n : `length should be >= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at most `n`. */\n lengthMax(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMax\",\n len !== undefined && len <= n,\n (neg) =>\n neg\n ? `length should be > ${n} (was ${len ?? \"n/a\"})`\n : `length should be <= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n}\n\n/**\n * Applies matchers to every element of an array frontmatter value.\n *\n * Obtained via {@link KeyAssertion.each}. Each terminal matcher records a\n * single {@link RuleResult}: it passes only when the value is an array and\n * every element satisfies the matcher; failures list the offending elements.\n */\nexport class EachAssertion {\n #negated: boolean;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n negated: boolean,\n ) {\n this.#negated = negated;\n }\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n private record(\n rule: string,\n perElement: (el: unknown) => boolean,\n describe: (negated: boolean, invalid: unknown[], isArray: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const isArray = Array.isArray(this.value);\n const invalid = isArray\n ? (this.value as unknown[]).filter((el) => !perElement(el))\n : [];\n const rawPass = isArray && invalid.length === 0;\n // A non-array can never satisfy a per-element check, even when negated.\n const ok = isArray ? (negated ? !rawPass : rawPass) : false;\n this.recorder.push({\n key: this.key,\n rule: `each.${rule}`,\n ok,\n negated,\n message: describe(negated, invalid, isArray),\n value: this.value === MISSING ? undefined : this.value,\n expected,\n });\n return this;\n }\n\n /** Every element must be one of `allowed` (enum over array contents). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n (el) => allowed.some((a) => deepEqual(el, a)),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of values from ${display(allowed)}`\n : neg\n ? `every item should be outside ${display(allowed)}`\n : `every item should be one of ${display(allowed)}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n allowed,\n );\n }\n\n /** Every element must be of the given type. */\n type(expected: ValueType): this {\n return this.record(\n \"type\",\n (el) => valueType(el) === expected,\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of ${expected}`\n : neg\n ? `every item should not be of type ${expected}`\n : `every item should be of type ${expected}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n expected,\n );\n }\n\n /** Every (string) element must match the pattern. */\n matches(pattern: RegExp): this {\n return this.record(\n \"matches\",\n (el) => typeof el === \"string\" && pattern.test(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of strings matching ${pattern}`\n : neg\n ? `every item should not match ${pattern}`\n : `every item should match ${pattern}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n pattern.source,\n );\n }\n}\n\nfunction contains(container: unknown, item: unknown): boolean {\n if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));\n if (typeof container === \"string\" && typeof item === \"string\")\n return container.includes(item);\n return false;\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((el, i) => deepEqual(el, b[i]));\n }\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n return ak.every((k) => deepEqual(ao[k], bo[k]));\n}\n","import { KeyAssertion } from \"./assertion.js\";\nimport type { RuleResult } from \"./types.js\";\n\n/**\n * The `fm` object passed to a rule set. Holds the parsed frontmatter data,\n * hands out {@link KeyAssertion} instances via {@link key}, and accumulates\n * every {@link RuleResult} the matchers record.\n */\nexport class Recorder {\n readonly results: RuleResult[] = [];\n\n constructor(readonly data: Record<string, unknown>) {}\n\n /** Begin a chain of assertions against the given frontmatter key. */\n key(name: string): KeyAssertion {\n const present = Object.prototype.hasOwnProperty.call(this.data, name);\n return new KeyAssertion(this, name, this.data[name], present);\n }\n\n /** Internal: append a recorded rule result. */\n push(result: RuleResult): void {\n this.results.push(result);\n }\n}\n","import { parseFrontmatter } from \"./parse.js\";\nimport { Recorder } from \"./recorder.js\";\nimport type { CheckResult, RulesFn, RuleResult } from \"./types.js\";\n\n/**\n * Run a rule set against the frontmatter of a Markdown source string.\n *\n * Never throws: malformed YAML or a missing frontmatter block is surfaced as a\n * failing {@link RuleResult} so results can always be collected and reported.\n *\n * ```ts\n * const result = check(markdown, (fm) => {\n * fm.key(\"title\").required().lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * });\n * if (!result.ok) console.error(result.failures);\n * ```\n */\nexport function check(source: string, rules: RulesFn): CheckResult {\n const parsed = parseFrontmatter(source);\n const recorder = new Recorder(parsed.data);\n\n if (parsed.error) {\n recorder.push(parseErrorResult(`invalid frontmatter: ${parsed.error}`));\n } else if (!parsed.hasFrontmatter) {\n recorder.push(parseErrorResult(\"no frontmatter block found\"));\n } else {\n rules(recorder);\n }\n\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return {\n ok: failures.length === 0,\n results,\n failures,\n data: parsed.data,\n };\n}\n\n/** Run a rule set against already-parsed frontmatter data. */\nexport function checkData(\n data: Record<string, unknown>,\n rules: RulesFn,\n): CheckResult {\n const recorder = new Recorder(data);\n rules(recorder);\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return { ok: failures.length === 0, results, failures, data };\n}\n\nfunction parseErrorResult(message: string): RuleResult {\n return {\n key: \"(frontmatter)\",\n rule: \"parse\",\n ok: false,\n negated: false,\n message,\n value: undefined,\n };\n}\n","import { pathToFileURL } from \"node:url\";\nimport { access } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { FluoriteConfig } from \"./types.js\";\n\n/**\n * Identity helper for authoring a config file with full type inference:\n *\n * ```ts\n * import { defineConfig } from \"fluorite\";\n * export default defineConfig({\n * include: [\"docs/**\\/*.md\"],\n * rules: (fm) => fm.key(\"title\").required(),\n * });\n * ```\n */\nexport function defineConfig(config: FluoriteConfig): FluoriteConfig {\n return config;\n}\n\nconst CONFIG_NAMES = [\n \"fluorite.config.js\",\n \"fluorite.config.mjs\",\n \"fluorite.config.cjs\",\n];\n\n/**\n * Locate a config file: an explicit path, or the first conventional name found\n * in `cwd`. Returns `undefined` when none is found.\n */\nexport async function resolveConfigPath(\n explicit: string | undefined,\n cwd = process.cwd(),\n): Promise<string | undefined> {\n if (explicit) return resolve(cwd, explicit);\n for (const name of CONFIG_NAMES) {\n const candidate = resolve(cwd, name);\n if (await fileExists(candidate)) return candidate;\n }\n return undefined;\n}\n\n/** Dynamically import a config file and return its default export. */\nexport async function loadConfig(path: string): Promise<FluoriteConfig> {\n const mod = await import(pathToFileURL(path).href);\n const config = (mod.default ?? mod) as FluoriteConfig;\n if (!config || typeof config.rules !== \"function\") {\n throw new Error(\n `Config at ${path} must export a default object with a \"rules\" function.`,\n );\n }\n return config;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","import pc from \"picocolors\";\nimport type { CheckResult } from \"./types.js\";\n\n/** A check result paired with the file it came from. */\nexport interface FileReport {\n file: string;\n result: CheckResult;\n}\n\nexport interface FormatOptions {\n /** Suppress per-file lines for passing files. */\n quiet?: boolean;\n}\n\nfunction displayValue(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Format file reports into a human-readable, colorized string. */\nexport function formatReports(\n reports: FileReport[],\n options: FormatOptions = {},\n): string {\n const lines: string[] = [];\n let passed = 0;\n let failed = 0;\n let ruleFailures = 0;\n\n for (const { file, result } of reports) {\n if (result.ok) {\n passed++;\n if (!options.quiet) lines.push(`${pc.green(\"✔\")} ${file}`);\n } else {\n failed++;\n lines.push(`${pc.red(\"✘\")} ${file}`);\n for (const failure of result.failures) {\n ruleFailures++;\n const where = pc.dim(`(value: ${displayValue(failure.value)})`);\n lines.push(\n ` ${pc.red(\"✘\")} ${pc.bold(failure.key)}: ${failure.message} ${where}`,\n );\n }\n }\n }\n\n const summary = [\n `${reports.length} files`,\n pc.green(`${passed} passed`),\n failed > 0 ? pc.red(`${failed} failed`) : `${failed} failed`,\n `${ruleFailures} rule failures`,\n ].join(\", \");\n\n lines.push(\"\");\n lines.push(summary);\n return lines.join(\"\\n\");\n}\n"],"mappings":";AAAA,OAAO,YAAY;AAoBZ,SAAS,iBAAiB,QAA6B;AAC5D,QAAM,iBAAiB,iBAAiB,KAAK,MAAM;AACnD,MAAI;AACF,UAAM,SAAS,OAAO,MAAM;AAC5B,UAAM,OAAQ,OAAO,QAAQ,CAAC;AAC9B,WAAO;AAAA,MACL;AAAA,MACA,SAAS,OAAO;AAAA,MAChB;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,MAAM,CAAC;AAAA,MACP,SAAS;AAAA,MACT;AAAA,MACA,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACxD;AAAA,EACF;AACF;;;AClCA,IAAM,UAAU,uBAAO,SAAS;AAEhC,SAAS,UAAU,OAAyC;AAC1D,MAAI,UAAU,QAAS,QAAO;AAC9B,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,aAAa,MAAM,UAAU;AACzE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoC;AACpD,MAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM;AACpE,SAAO;AACT;AAEA,SAAS,QAAQ,OAAwB;AACvC,MAAI,UAAU,QAAS,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;AAAA,EAC9C,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAeO,IAAM,eAAN,MAAmB;AAAA,EAGxB,YACmB,UACA,KACA,OACA,SACjB;AAJiB;AACA;AACA;AACA;AAAA,EAChB;AAAA,EAJgB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EANnB,WAAW;AAAA;AAAA,EAUX,IAAI,MAAY;AACd,SAAK,WAAW;AAChB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAY,WAAoB;AAC9B,WAAO,KAAK,UAAU,KAAK,QAAQ;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAKQ,OACN,MACA,SACA,UACA,UACM;AACN,UAAM,UAAU,KAAK;AACrB,SAAK,WAAW;AAChB,UAAM,KAAK,UAAU,CAAC,UAAU;AAChC,UAAM,SAAqB;AAAA,MACzB,KAAK,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,SAAS,OAAO;AAAA,MACzB,OAAO,KAAK,UAAU,KAAK,QAAQ;AAAA,MACnC;AAAA,IACF;AACA,SAAK,SAAS,KAAK,MAAM;AACzB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,WAAiB;AACf,WAAO,KAAK;AAAA,MACV;AAAA,MACA,KAAK;AAAA,MACL,CAAC,QAAS,MAAM,qBAAqB;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAGA,SAAe;AACb,WAAO,KAAK;AAAA,MACV;AAAA,MACA,KAAK;AAAA,MACL,CAAC,QAAS,MAAM,qBAAqB;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,UAA2B;AAC9B,UAAM,SAAS,UAAU,KAAK,QAAQ;AACtC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,WAAW;AAAA,MACX,CAAC,QACC,MACI,yBAAyB,QAAQ,KACjC,qBAAqB,QAAQ,SAAS,MAAM;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,GAAG,UAAyB;AAC1B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,UAAU,KAAK,UAAU,QAAQ;AAAA,MACjC,CAAC,QACC,MACI,oBAAoB,QAAQ,QAAQ,CAAC,KACrC,gBAAgB,QAAQ,QAAQ,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,SAAS,SAAmC;AAC1C,UAAM,IAAI,KAAK;AACf,UAAM,UAAU,MAAM,QAAQ,CAAC;AAC/B,UAAM,UAAU,UACZ,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,KAAK,CAAC,MAAM,UAAU,IAAI,CAAC,CAAC,CAAC,IACvD,CAAC;AACL,WAAO,KAAK;AAAA,MACV;AAAA,MACA,WAAW,QAAQ,WAAW;AAAA,MAC9B,CAAC,QAAQ;AACP,YAAI,CAAC,QAAS,QAAO,qCAAqC,QAAQ,OAAO,CAAC;AAC1E,YAAI,IAAK,QAAO,iCAAiC,QAAQ,OAAO,CAAC;AACjE,eAAO,QAAQ,SACX,8BAA8B,QAAQ,OAAO,CAAC,cAAc,QAAQ,OAAO,CAAC,MAC5E,8BAA8B,QAAQ,OAAO,CAAC;AAAA,MACpD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,SAAmC;AACtC,WAAO,KAAK,SAAS,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,IAAI,OAAsB;AACxB,UAAM,UAAU,KAAK;AACrB,SAAK,WAAW;AAChB,WAAO,IAAI,cAAc,KAAK,UAAU,KAAK,KAAK,KAAK,UAAU,OAAO;AAAA,EAC1E;AAAA;AAAA,EAGA,MAAM,SAAmC;AACvC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,QAAQ,KAAK,CAAC,MAAM,UAAU,KAAK,UAAU,CAAC,CAAC;AAAA,MAC/C,CAAC,QACC,MACI,wBAAwB,QAAQ,OAAO,CAAC,KACxC,oBAAoB,QAAQ,OAAO,CAAC;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,SAAuB;AAC7B,UAAM,IAAI,KAAK;AACf,UAAM,OAAO,OAAO,MAAM,YAAY,QAAQ,KAAK,CAAC;AACpD,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,CAAC,QACC,MACI,oBAAoB,OAAO,KAC3B,gBAAgB,OAAO;AAAA,MAC7B,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,MAAqB;AACvB,UAAM,IAAI,KAAK;AACf,QAAI,OAAO;AACX,QAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,KAAK,CAAC,OAAO,UAAU,IAAI,IAAI,CAAC;AAAA,aACtD,OAAO,MAAM,YAAY,OAAO,SAAS;AAChD,aAAO,EAAE,SAAS,IAAI;AACxB,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,CAAC,QACC,MAAM,mBAAmB,QAAQ,IAAI,CAAC,KAAK,eAAe,QAAQ,IAAI,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,OAAiC;AACtC,UAAM,IAAI,KAAK;AACf,UAAM,OAAO,MAAM,MAAM,CAAC,SAAS,SAAS,GAAG,IAAI,CAAC;AACpD,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,CAAC,QACC,MACI,0BAA0B,QAAQ,KAAK,CAAC,KACxC,sBAAsB,QAAQ,KAAK,CAAC;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,OAAiC;AACtC,UAAM,IAAI,KAAK;AACf,UAAM,OAAO,MAAM,KAAK,CAAC,SAAS,SAAS,GAAG,IAAI,CAAC;AACnD,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,CAAC,QACC,MACI,0BAA0B,QAAQ,KAAK,CAAC,KACxC,sBAAsB,QAAQ,KAAK,CAAC;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,GAAiB;AACtB,UAAM,MAAM,SAAS,KAAK,QAAQ;AAClC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,MACR,CAAC,QACC,MACI,wBAAwB,CAAC,SAAS,OAAO,KAAK,MAC9C,oBAAoB,CAAC,SAAS,OAAO,KAAK;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,UAAU,GAAiB;AACzB,UAAM,MAAM,SAAS,KAAK,QAAQ;AAClC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,QAAQ,UAAa,OAAO;AAAA,MAC5B,CAAC,QACC,MACI,sBAAsB,CAAC,SAAS,OAAO,KAAK,MAC5C,uBAAuB,CAAC,SAAS,OAAO,KAAK;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,UAAU,GAAiB;AACzB,UAAM,MAAM,SAAS,KAAK,QAAQ;AAClC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,QAAQ,UAAa,OAAO;AAAA,MAC5B,CAAC,QACC,MACI,sBAAsB,CAAC,SAAS,OAAO,KAAK,MAC5C,uBAAuB,CAAC,SAAS,OAAO,KAAK;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;AASO,IAAM,gBAAN,MAAoB;AAAA,EAGzB,YACmB,UACA,KACA,OACjB,SACA;AAJiB;AACA;AACA;AAGjB,SAAK,WAAW;AAAA,EAClB;AAAA,EANmB;AAAA,EACA;AAAA,EACA;AAAA,EALnB;AAAA;AAAA,EAYA,IAAI,MAAY;AACd,SAAK,WAAW;AAChB,WAAO;AAAA,EACT;AAAA,EAEQ,OACN,MACA,YACA,UACA,UACM;AACN,UAAM,UAAU,KAAK;AACrB,SAAK,WAAW;AAChB,UAAM,UAAU,MAAM,QAAQ,KAAK,KAAK;AACxC,UAAM,UAAU,UACX,KAAK,MAAoB,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,IACxD,CAAC;AACL,UAAM,UAAU,WAAW,QAAQ,WAAW;AAE9C,UAAM,KAAK,UAAW,UAAU,CAAC,UAAU,UAAW;AACtD,SAAK,SAAS,KAAK;AAAA,MACjB,KAAK,KAAK;AAAA,MACV,MAAM,QAAQ,IAAI;AAAA,MAClB;AAAA,MACA;AAAA,MACA,SAAS,SAAS,SAAS,SAAS,OAAO;AAAA,MAC3C,OAAO,KAAK,UAAU,UAAU,SAAY,KAAK;AAAA,MACjD;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,SAAmC;AACvC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,CAAC,OAAO,QAAQ,KAAK,CAAC,MAAM,UAAU,IAAI,CAAC,CAAC;AAAA,MAC5C,CAAC,KAAK,SAAS,YACb,CAAC,UACG,qCAAqC,QAAQ,OAAO,CAAC,KACrD,MACE,gCAAgC,QAAQ,OAAO,CAAC,KAChD,+BAA+B,QAAQ,OAAO,CAAC,GAAG,QAAQ,SAAS,cAAc,QAAQ,OAAO,CAAC,MAAM,EAAE;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,UAA2B;AAC9B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,CAAC,OAAO,UAAU,EAAE,MAAM;AAAA,MAC1B,CAAC,KAAK,SAAS,YACb,CAAC,UACG,yBAAyB,QAAQ,KACjC,MACE,oCAAoC,QAAQ,KAC5C,gCAAgC,QAAQ,GAAG,QAAQ,SAAS,cAAc,QAAQ,OAAO,CAAC,MAAM,EAAE;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,SAAuB;AAC7B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,CAAC,OAAO,OAAO,OAAO,YAAY,QAAQ,KAAK,EAAE;AAAA,MACjD,CAAC,KAAK,SAAS,YACb,CAAC,UACG,0CAA0C,OAAO,KACjD,MACE,+BAA+B,OAAO,KACtC,2BAA2B,OAAO,GAAG,QAAQ,SAAS,cAAc,QAAQ,OAAO,CAAC,MAAM,EAAE;AAAA,MACpG,QAAQ;AAAA,IACV;AAAA,EACF;AACF;AAEA,SAAS,SAAS,WAAoB,MAAwB;AAC5D,MAAI,MAAM,QAAQ,SAAS,EAAG,QAAO,UAAU,KAAK,CAAC,OAAO,UAAU,IAAI,IAAI,CAAC;AAC/E,MAAI,OAAO,cAAc,YAAY,OAAO,SAAS;AACnD,WAAO,UAAU,SAAS,IAAI;AAChC,SAAO;AACT;AAEA,SAAS,UAAU,GAAY,GAAqB;AAClD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,QAAQ,MAAM,KAAM,QAAO;AACrC,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO;AAC3D,MAAI,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAClD,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAO,EAAE,MAAM,CAAC,IAAI,MAAM,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC;AAAA,EAC/C;AACA,QAAM,KAAK;AACX,QAAM,KAAK;AACX,QAAM,KAAK,OAAO,KAAK,EAAE;AACzB,QAAM,KAAK,OAAO,KAAK,EAAE;AACzB,MAAI,GAAG,WAAW,GAAG,OAAQ,QAAO;AACpC,SAAO,GAAG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AAChD;;;AC7ZO,IAAM,WAAN,MAAe;AAAA,EAGpB,YAAqB,MAA+B;AAA/B;AAAA,EAAgC;AAAA,EAAhC;AAAA,EAFZ,UAAwB,CAAC;AAAA;AAAA,EAKlC,IAAI,MAA4B;AAC9B,UAAM,UAAU,OAAO,UAAU,eAAe,KAAK,KAAK,MAAM,IAAI;AACpE,WAAO,IAAI,aAAa,MAAM,MAAM,KAAK,KAAK,IAAI,GAAG,OAAO;AAAA,EAC9D;AAAA;AAAA,EAGA,KAAK,QAA0B;AAC7B,SAAK,QAAQ,KAAK,MAAM;AAAA,EAC1B;AACF;;;ACLO,SAAS,MAAM,QAAgB,OAA6B;AACjE,QAAM,SAAS,iBAAiB,MAAM;AACtC,QAAM,WAAW,IAAI,SAAS,OAAO,IAAI;AAEzC,MAAI,OAAO,OAAO;AAChB,aAAS,KAAK,iBAAiB,wBAAwB,OAAO,KAAK,EAAE,CAAC;AAAA,EACxE,WAAW,CAAC,OAAO,gBAAgB;AACjC,aAAS,KAAK,iBAAiB,4BAA4B,CAAC;AAAA,EAC9D,OAAO;AACL,UAAM,QAAQ;AAAA,EAChB;AAEA,QAAM,UAAU,SAAS;AACzB,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AAC5C,SAAO;AAAA,IACL,IAAI,SAAS,WAAW;AAAA,IACxB;AAAA,IACA;AAAA,IACA,MAAM,OAAO;AAAA,EACf;AACF;AAGO,SAAS,UACd,MACA,OACa;AACb,QAAM,WAAW,IAAI,SAAS,IAAI;AAClC,QAAM,QAAQ;AACd,QAAM,UAAU,SAAS;AACzB,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AAC5C,SAAO,EAAE,IAAI,SAAS,WAAW,GAAG,SAAS,UAAU,KAAK;AAC9D;AAEA,SAAS,iBAAiB,SAA6B;AACrD,SAAO;AAAA,IACL,KAAK;AAAA,IACL,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,SAAS;AAAA,IACT;AAAA,IACA,OAAO;AAAA,EACT;AACF;;;AC7DA,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AACvB,SAAS,eAAe;AAcjB,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;AAEA,IAAM,eAAe;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF;AAMA,eAAsB,kBACpB,UACA,MAAM,QAAQ,IAAI,GACW;AAC7B,MAAI,SAAU,QAAO,QAAQ,KAAK,QAAQ;AAC1C,aAAW,QAAQ,cAAc;AAC/B,UAAM,YAAY,QAAQ,KAAK,IAAI;AACnC,QAAI,MAAM,WAAW,SAAS,EAAG,QAAO;AAAA,EAC1C;AACA,SAAO;AACT;AAGA,eAAsB,WAAW,MAAuC;AACtE,QAAM,MAAM,MAAM,OAAO,cAAc,IAAI,EAAE;AAC7C,QAAM,SAAU,IAAI,WAAW;AAC/B,MAAI,CAAC,UAAU,OAAO,OAAO,UAAU,YAAY;AACjD,UAAM,IAAI;AAAA,MACR,aAAa,IAAI;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,OAAO,IAAI;AACjB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC7DA,OAAO,QAAQ;AAcf,SAAS,aAAa,OAAwB;AAC5C,MAAI;AACF,WAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;AAAA,EAC9C,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAGO,SAAS,cACd,SACA,UAAyB,CAAC,GAClB;AACR,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAS;AACb,MAAI,SAAS;AACb,MAAI,eAAe;AAEnB,aAAW,EAAE,MAAM,OAAO,KAAK,SAAS;AACtC,QAAI,OAAO,IAAI;AACb;AACA,UAAI,CAAC,QAAQ,MAAO,OAAM,KAAK,GAAG,GAAG,MAAM,QAAG,CAAC,IAAI,IAAI,EAAE;AAAA,IAC3D,OAAO;AACL;AACA,YAAM,KAAK,GAAG,GAAG,IAAI,QAAG,CAAC,IAAI,IAAI,EAAE;AACnC,iBAAW,WAAW,OAAO,UAAU;AACrC;AACA,cAAM,QAAQ,GAAG,IAAI,WAAW,aAAa,QAAQ,KAAK,CAAC,GAAG;AAC9D,cAAM;AAAA,UACJ,KAAK,GAAG,IAAI,QAAG,CAAC,IAAI,GAAG,KAAK,QAAQ,GAAG,CAAC,KAAK,QAAQ,OAAO,IAAI,KAAK;AAAA,QACvE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,GAAG,QAAQ,MAAM;AAAA,IACjB,GAAG,MAAM,GAAG,MAAM,SAAS;AAAA,IAC3B,SAAS,IAAI,GAAG,IAAI,GAAG,MAAM,SAAS,IAAI,GAAG,MAAM;AAAA,IACnD,GAAG,YAAY;AAAA,EACjB,EAAE,KAAK,IAAI;AAEX,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,OAAO;AAClB,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
package/dist/cli.cjs ADDED
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_promises2 = require("fs/promises");
28
+ var import_node_util = require("util");
29
+ var import_tinyglobby = require("tinyglobby");
30
+
31
+ // src/parse.ts
32
+ var import_gray_matter = __toESM(require("gray-matter"), 1);
33
+ function parseFrontmatter(source) {
34
+ const hasFrontmatter = /^?\s*---\r?\n/.test(source);
35
+ try {
36
+ const parsed = (0, import_gray_matter.default)(source);
37
+ const data = parsed.data ?? {};
38
+ return {
39
+ data,
40
+ content: parsed.content,
41
+ hasFrontmatter
42
+ };
43
+ } catch (err) {
44
+ return {
45
+ data: {},
46
+ content: source,
47
+ hasFrontmatter,
48
+ error: err instanceof Error ? err.message : String(err)
49
+ };
50
+ }
51
+ }
52
+
53
+ // src/assertion.ts
54
+ var MISSING = /* @__PURE__ */ Symbol("missing");
55
+ function valueType(value) {
56
+ if (value === MISSING) return "undefined";
57
+ if (value === null) return "null";
58
+ if (Array.isArray(value)) return "array";
59
+ const t = typeof value;
60
+ if (t === "string" || t === "number" || t === "boolean" || t === "object") {
61
+ return t;
62
+ }
63
+ return "undefined";
64
+ }
65
+ function lengthOf(value) {
66
+ if (typeof value === "string" || Array.isArray(value)) return value.length;
67
+ return void 0;
68
+ }
69
+ function display(value) {
70
+ if (value === MISSING) return "undefined";
71
+ try {
72
+ return JSON.stringify(value) ?? String(value);
73
+ } catch {
74
+ return String(value);
75
+ }
76
+ }
77
+ var KeyAssertion = class {
78
+ constructor(recorder, key, value, present) {
79
+ this.recorder = recorder;
80
+ this.key = key;
81
+ this.value = value;
82
+ this.present = present;
83
+ }
84
+ recorder;
85
+ key;
86
+ value;
87
+ present;
88
+ #negated = false;
89
+ /** Negate the next matcher in the chain. */
90
+ get not() {
91
+ this.#negated = true;
92
+ return this;
93
+ }
94
+ /** The raw value (resolved against the sentinel) used by matchers. */
95
+ get resolved() {
96
+ return this.present ? this.value : MISSING;
97
+ }
98
+ /**
99
+ * Record a result, applying the pending `.not` negation, then reset it.
100
+ */
101
+ record(rule, rawPass, describe, expected) {
102
+ const negated = this.#negated;
103
+ this.#negated = false;
104
+ const ok = negated ? !rawPass : rawPass;
105
+ const result = {
106
+ key: this.key,
107
+ rule,
108
+ ok,
109
+ negated,
110
+ message: describe(negated),
111
+ value: this.present ? this.value : void 0,
112
+ expected
113
+ };
114
+ this.recorder.push(result);
115
+ return this;
116
+ }
117
+ /** Assert the key exists in the frontmatter. */
118
+ required() {
119
+ return this.record(
120
+ "required",
121
+ this.present,
122
+ (neg) => neg ? `should not exist` : `is required`
123
+ );
124
+ }
125
+ /** Alias of {@link required}. */
126
+ exists() {
127
+ return this.record(
128
+ "exists",
129
+ this.present,
130
+ (neg) => neg ? `should not exist` : `should exist`
131
+ );
132
+ }
133
+ /** Assert the value is of the given type. */
134
+ type(expected) {
135
+ const actual = valueType(this.resolved);
136
+ return this.record(
137
+ "type",
138
+ actual === expected,
139
+ (neg) => neg ? `should not be of type ${expected}` : `should be of type ${expected} (was ${actual})`,
140
+ expected
141
+ );
142
+ }
143
+ /** Assert the value strictly equals `expected` (deep for arrays/objects). */
144
+ eq(expected) {
145
+ return this.record(
146
+ "eq",
147
+ deepEqual(this.resolved, expected),
148
+ (neg) => neg ? `should not equal ${display(expected)}` : `should equal ${display(expected)}`,
149
+ expected
150
+ );
151
+ }
152
+ /**
153
+ * Assert the value is an array whose every element is in `allowed` (enum).
154
+ *
155
+ * Designed for catching tag notation drift: define the canonical set once
156
+ * and any stray / mistyped value is reported.
157
+ *
158
+ * ```ts
159
+ * fm.key("tags").subsetOf(["ok", "release", "blog"]);
160
+ * ```
161
+ */
162
+ subsetOf(allowed) {
163
+ const v = this.resolved;
164
+ const isArray = Array.isArray(v);
165
+ const invalid = isArray ? v.filter((el) => !allowed.some((a) => deepEqual(el, a))) : [];
166
+ return this.record(
167
+ "subsetOf",
168
+ isArray && invalid.length === 0,
169
+ (neg) => {
170
+ if (!isArray) return `should be an array of values from ${display(allowed)}`;
171
+ if (neg) return `should contain values outside ${display(allowed)}`;
172
+ return invalid.length ? `all items should be one of ${display(allowed)} (invalid: ${display(invalid)})` : `all items should be one of ${display(allowed)}`;
173
+ },
174
+ allowed
175
+ );
176
+ }
177
+ /** Alias of {@link subsetOf}. */
178
+ only(allowed) {
179
+ return this.subsetOf(allowed);
180
+ }
181
+ /**
182
+ * Apply matchers to every element of an array value.
183
+ *
184
+ * ```ts
185
+ * fm.key("tags").each.oneOf(["ok", "release"]);
186
+ * fm.key("tags").each.matches(/^[a-z0-9-]+$/);
187
+ * ```
188
+ */
189
+ get each() {
190
+ const negated = this.#negated;
191
+ this.#negated = false;
192
+ return new EachAssertion(this.recorder, this.key, this.resolved, negated);
193
+ }
194
+ /** Assert the value is one of `allowed` (enum). */
195
+ oneOf(allowed) {
196
+ return this.record(
197
+ "oneOf",
198
+ allowed.some((a) => deepEqual(this.resolved, a)),
199
+ (neg) => neg ? `should not be one of ${display(allowed)}` : `should be one of ${display(allowed)}`,
200
+ allowed
201
+ );
202
+ }
203
+ /** Assert a string value matches the given regular expression. */
204
+ matches(pattern) {
205
+ const v = this.resolved;
206
+ const pass = typeof v === "string" && pattern.test(v);
207
+ return this.record(
208
+ "matches",
209
+ pass,
210
+ (neg) => neg ? `should not match ${pattern}` : `should match ${pattern}`,
211
+ pattern.source
212
+ );
213
+ }
214
+ /** Assert an array contains `item`, or a string contains the substring. */
215
+ has(item) {
216
+ const v = this.resolved;
217
+ let pass = false;
218
+ if (Array.isArray(v)) pass = v.some((el) => deepEqual(el, item));
219
+ else if (typeof v === "string" && typeof item === "string")
220
+ pass = v.includes(item);
221
+ return this.record(
222
+ "has",
223
+ pass,
224
+ (neg) => neg ? `should not have ${display(item)}` : `should have ${display(item)}`,
225
+ item
226
+ );
227
+ }
228
+ /** Assert an array/string contains all of `items`. */
229
+ hasAll(items) {
230
+ const v = this.resolved;
231
+ const pass = items.every((item) => contains(v, item));
232
+ return this.record(
233
+ "hasAll",
234
+ pass,
235
+ (neg) => neg ? `should not have all of ${display(items)}` : `should have all of ${display(items)}`,
236
+ items
237
+ );
238
+ }
239
+ /** Assert an array/string contains at least one of `items`. */
240
+ hasAny(items) {
241
+ const v = this.resolved;
242
+ const pass = items.some((item) => contains(v, item));
243
+ return this.record(
244
+ "hasAny",
245
+ pass,
246
+ (neg) => neg ? `should not have any of ${display(items)}` : `should have any of ${display(items)}`,
247
+ items
248
+ );
249
+ }
250
+ /** Assert the string/array length equals `n`. */
251
+ length(n) {
252
+ const len = lengthOf(this.resolved);
253
+ return this.record(
254
+ "length",
255
+ len === n,
256
+ (neg) => neg ? `length should not be ${n} (was ${len ?? "n/a"})` : `length should be ${n} (was ${len ?? "n/a"})`,
257
+ n
258
+ );
259
+ }
260
+ /** Assert the string/array length is at least `n`. */
261
+ lengthMin(n) {
262
+ const len = lengthOf(this.resolved);
263
+ return this.record(
264
+ "lengthMin",
265
+ len !== void 0 && len >= n,
266
+ (neg) => neg ? `length should be < ${n} (was ${len ?? "n/a"})` : `length should be >= ${n} (was ${len ?? "n/a"})`,
267
+ n
268
+ );
269
+ }
270
+ /** Assert the string/array length is at most `n`. */
271
+ lengthMax(n) {
272
+ const len = lengthOf(this.resolved);
273
+ return this.record(
274
+ "lengthMax",
275
+ len !== void 0 && len <= n,
276
+ (neg) => neg ? `length should be > ${n} (was ${len ?? "n/a"})` : `length should be <= ${n} (was ${len ?? "n/a"})`,
277
+ n
278
+ );
279
+ }
280
+ };
281
+ var EachAssertion = class {
282
+ constructor(recorder, key, value, negated) {
283
+ this.recorder = recorder;
284
+ this.key = key;
285
+ this.value = value;
286
+ this.#negated = negated;
287
+ }
288
+ recorder;
289
+ key;
290
+ value;
291
+ #negated;
292
+ /** Negate the next matcher in the chain. */
293
+ get not() {
294
+ this.#negated = true;
295
+ return this;
296
+ }
297
+ record(rule, perElement, describe, expected) {
298
+ const negated = this.#negated;
299
+ this.#negated = false;
300
+ const isArray = Array.isArray(this.value);
301
+ const invalid = isArray ? this.value.filter((el) => !perElement(el)) : [];
302
+ const rawPass = isArray && invalid.length === 0;
303
+ const ok = isArray ? negated ? !rawPass : rawPass : false;
304
+ this.recorder.push({
305
+ key: this.key,
306
+ rule: `each.${rule}`,
307
+ ok,
308
+ negated,
309
+ message: describe(negated, invalid, isArray),
310
+ value: this.value === MISSING ? void 0 : this.value,
311
+ expected
312
+ });
313
+ return this;
314
+ }
315
+ /** Every element must be one of `allowed` (enum over array contents). */
316
+ oneOf(allowed) {
317
+ return this.record(
318
+ "oneOf",
319
+ (el) => allowed.some((a) => deepEqual(el, a)),
320
+ (neg, invalid, isArray) => !isArray ? `should be an array of values from ${display(allowed)}` : neg ? `every item should be outside ${display(allowed)}` : `every item should be one of ${display(allowed)}${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`,
321
+ allowed
322
+ );
323
+ }
324
+ /** Every element must be of the given type. */
325
+ type(expected) {
326
+ return this.record(
327
+ "type",
328
+ (el) => valueType(el) === expected,
329
+ (neg, invalid, isArray) => !isArray ? `should be an array of ${expected}` : neg ? `every item should not be of type ${expected}` : `every item should be of type ${expected}${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`,
330
+ expected
331
+ );
332
+ }
333
+ /** Every (string) element must match the pattern. */
334
+ matches(pattern) {
335
+ return this.record(
336
+ "matches",
337
+ (el) => typeof el === "string" && pattern.test(el),
338
+ (neg, invalid, isArray) => !isArray ? `should be an array of strings matching ${pattern}` : neg ? `every item should not match ${pattern}` : `every item should match ${pattern}${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`,
339
+ pattern.source
340
+ );
341
+ }
342
+ };
343
+ function contains(container, item) {
344
+ if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));
345
+ if (typeof container === "string" && typeof item === "string")
346
+ return container.includes(item);
347
+ return false;
348
+ }
349
+ function deepEqual(a, b) {
350
+ if (a === b) return true;
351
+ if (a === null || b === null) return false;
352
+ if (typeof a !== "object" || typeof b !== "object") return false;
353
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
354
+ if (Array.isArray(a) && Array.isArray(b)) {
355
+ if (a.length !== b.length) return false;
356
+ return a.every((el, i) => deepEqual(el, b[i]));
357
+ }
358
+ const ao = a;
359
+ const bo = b;
360
+ const ak = Object.keys(ao);
361
+ const bk = Object.keys(bo);
362
+ if (ak.length !== bk.length) return false;
363
+ return ak.every((k) => deepEqual(ao[k], bo[k]));
364
+ }
365
+
366
+ // src/recorder.ts
367
+ var Recorder = class {
368
+ constructor(data) {
369
+ this.data = data;
370
+ }
371
+ data;
372
+ results = [];
373
+ /** Begin a chain of assertions against the given frontmatter key. */
374
+ key(name) {
375
+ const present = Object.prototype.hasOwnProperty.call(this.data, name);
376
+ return new KeyAssertion(this, name, this.data[name], present);
377
+ }
378
+ /** Internal: append a recorded rule result. */
379
+ push(result) {
380
+ this.results.push(result);
381
+ }
382
+ };
383
+
384
+ // src/check.ts
385
+ function check(source, rules) {
386
+ const parsed = parseFrontmatter(source);
387
+ const recorder = new Recorder(parsed.data);
388
+ if (parsed.error) {
389
+ recorder.push(parseErrorResult(`invalid frontmatter: ${parsed.error}`));
390
+ } else if (!parsed.hasFrontmatter) {
391
+ recorder.push(parseErrorResult("no frontmatter block found"));
392
+ } else {
393
+ rules(recorder);
394
+ }
395
+ const results = recorder.results;
396
+ const failures = results.filter((r) => !r.ok);
397
+ return {
398
+ ok: failures.length === 0,
399
+ results,
400
+ failures,
401
+ data: parsed.data
402
+ };
403
+ }
404
+ function parseErrorResult(message) {
405
+ return {
406
+ key: "(frontmatter)",
407
+ rule: "parse",
408
+ ok: false,
409
+ negated: false,
410
+ message,
411
+ value: void 0
412
+ };
413
+ }
414
+
415
+ // src/config.ts
416
+ var import_node_url = require("url");
417
+ var import_promises = require("fs/promises");
418
+ var import_node_path = require("path");
419
+ var CONFIG_NAMES = [
420
+ "fluorite.config.js",
421
+ "fluorite.config.mjs",
422
+ "fluorite.config.cjs"
423
+ ];
424
+ async function resolveConfigPath(explicit, cwd = process.cwd()) {
425
+ if (explicit) return (0, import_node_path.resolve)(cwd, explicit);
426
+ for (const name of CONFIG_NAMES) {
427
+ const candidate = (0, import_node_path.resolve)(cwd, name);
428
+ if (await fileExists(candidate)) return candidate;
429
+ }
430
+ return void 0;
431
+ }
432
+ async function loadConfig(path) {
433
+ const mod = await import((0, import_node_url.pathToFileURL)(path).href);
434
+ const config = mod.default ?? mod;
435
+ if (!config || typeof config.rules !== "function") {
436
+ throw new Error(
437
+ `Config at ${path} must export a default object with a "rules" function.`
438
+ );
439
+ }
440
+ return config;
441
+ }
442
+ async function fileExists(path) {
443
+ try {
444
+ await (0, import_promises.access)(path);
445
+ return true;
446
+ } catch {
447
+ return false;
448
+ }
449
+ }
450
+
451
+ // src/report.ts
452
+ var import_picocolors = __toESM(require("picocolors"), 1);
453
+ function displayValue(value) {
454
+ try {
455
+ return JSON.stringify(value) ?? String(value);
456
+ } catch {
457
+ return String(value);
458
+ }
459
+ }
460
+ function formatReports(reports, options = {}) {
461
+ const lines = [];
462
+ let passed = 0;
463
+ let failed = 0;
464
+ let ruleFailures = 0;
465
+ for (const { file, result } of reports) {
466
+ if (result.ok) {
467
+ passed++;
468
+ if (!options.quiet) lines.push(`${import_picocolors.default.green("\u2714")} ${file}`);
469
+ } else {
470
+ failed++;
471
+ lines.push(`${import_picocolors.default.red("\u2718")} ${file}`);
472
+ for (const failure of result.failures) {
473
+ ruleFailures++;
474
+ const where = import_picocolors.default.dim(`(value: ${displayValue(failure.value)})`);
475
+ lines.push(
476
+ ` ${import_picocolors.default.red("\u2718")} ${import_picocolors.default.bold(failure.key)}: ${failure.message} ${where}`
477
+ );
478
+ }
479
+ }
480
+ }
481
+ const summary = [
482
+ `${reports.length} files`,
483
+ import_picocolors.default.green(`${passed} passed`),
484
+ failed > 0 ? import_picocolors.default.red(`${failed} failed`) : `${failed} failed`,
485
+ `${ruleFailures} rule failures`
486
+ ].join(", ");
487
+ lines.push("");
488
+ lines.push(summary);
489
+ return lines.join("\n");
490
+ }
491
+
492
+ // src/cli.ts
493
+ var HELP = `fluorite \u2014 inspect and validate Markdown frontmatter
494
+
495
+ Usage:
496
+ fluorite check [patterns...] [options]
497
+
498
+ Options:
499
+ -c, --config <path> Path to a config file (default: fluorite.config.{js,mjs,cjs})
500
+ -q, --quiet Only print files with failures
501
+ -h, --help Show this help
502
+
503
+ Examples:
504
+ fluorite check "docs/**/*.md"
505
+ fluorite check --config fluorite.config.mjs
506
+ `;
507
+ async function main(argv) {
508
+ const { values, positionals } = (0, import_node_util.parseArgs)({
509
+ args: argv,
510
+ allowPositionals: true,
511
+ options: {
512
+ config: { type: "string", short: "c" },
513
+ quiet: { type: "boolean", short: "q", default: false },
514
+ help: { type: "boolean", short: "h", default: false }
515
+ }
516
+ });
517
+ if (values.help) {
518
+ process.stdout.write(HELP);
519
+ return 0;
520
+ }
521
+ const command = positionals[0];
522
+ if (command !== "check") {
523
+ process.stderr.write(
524
+ command ? `Unknown command: ${command}
525
+
526
+ ${HELP}` : HELP
527
+ );
528
+ return command ? 1 : 0;
529
+ }
530
+ const cliPatterns = positionals.slice(1);
531
+ const configPath = await resolveConfigPath(values.config);
532
+ if (!configPath) {
533
+ process.stderr.write(
534
+ "No config file found. Create fluorite.config.mjs (export default defineConfig({ rules })) or pass --config.\n"
535
+ );
536
+ return 1;
537
+ }
538
+ let config;
539
+ try {
540
+ config = await loadConfig(configPath);
541
+ } catch (err) {
542
+ process.stderr.write(
543
+ `Failed to load config: ${err instanceof Error ? err.message : String(err)}
544
+ `
545
+ );
546
+ return 1;
547
+ }
548
+ const patterns = cliPatterns.length > 0 ? cliPatterns : config.include ?? ["**/*.md"];
549
+ const files = await (0, import_tinyglobby.glob)(patterns, {
550
+ ignore: config.exclude ?? ["**/node_modules/**"]
551
+ });
552
+ files.sort();
553
+ if (files.length === 0) {
554
+ process.stderr.write(`No files matched: ${patterns.join(", ")}
555
+ `);
556
+ return 1;
557
+ }
558
+ const reports = [];
559
+ for (const file of files) {
560
+ const source = await (0, import_promises2.readFile)(file, "utf8");
561
+ reports.push({ file, result: check(source, config.rules) });
562
+ }
563
+ process.stdout.write(formatReports(reports, { quiet: values.quiet }) + "\n");
564
+ return reports.some((r) => !r.result.ok) ? 1 : 0;
565
+ }
566
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
567
+ process.stderr.write(
568
+ `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}
569
+ `
570
+ );
571
+ process.exit(1);
572
+ });
573
+ //# sourceMappingURL=cli.cjs.map