import type { ContractCanonicalMappingInput } from "./contract/index.js"; import type { GovernanceConfig } from "./governance.js"; export interface LocalCanonicalResolveRule { tag: string; role?: string; inputType?: string; } export interface LocalCanonicalDeclaration { importPath: string; resolves: readonly LocalCanonicalResolveRule[]; } export interface InspectConfig { localCanonical?: Readonly>; } interface PreferLibraryRuleConfig { enabled?: boolean; severity?: "error" | "warn" | "info"; options?: { canonicalMappings?: unknown[]; [key: string]: unknown; }; [key: string]: unknown; } export function localCanonicalContractMappings( inspect: InspectConfig | null | undefined ): ContractCanonicalMappingInput[] { return localCanonicalRuleMappings(inspect).map((mapping) => ({ component: mapping.htmlEquivalent, canonical: mapping.name, status: "confirmed", importPath: mapping.importPath, resolves: [ { tag: mapping.htmlEquivalent, ...(mapping.ariaRole ? { role: mapping.ariaRole } : {}), ...(mapping.htmlType ? { inputType: mapping.htmlType } : {}), }, ], })); } export function governanceConfigWithLocalCanonical( config: GovernanceConfig | undefined, inspect: InspectConfig | null | undefined ): GovernanceConfig | undefined { const localMappings = localCanonicalRuleMappings(inspect); if (localMappings.length === 0) return config; const existingRule = readPreferLibraryRule(config); const existingOptions = existingRule.options ?? {}; return { ...(config ?? {}), rules: { ...(config?.rules ?? {}), "components/prefer-library": { ...existingRule, enabled: existingRule.enabled ?? true, severity: existingRule.severity ?? "error", options: { ...existingOptions, canonicalMappings: [ ...(Array.isArray(existingOptions.canonicalMappings) ? existingOptions.canonicalMappings : []), ...localMappings, ], }, }, }, }; } function localCanonicalRuleMappings(inspect: InspectConfig | null | undefined) { return Object.entries(inspect?.localCanonical ?? {}).flatMap(([name, declaration]) => declaration.resolves.map((rule) => ({ name, canonical: name, htmlEquivalent: rule.tag, ...(rule.role ? { ariaRole: rule.role } : {}), ...(rule.inputType ? { htmlType: rule.inputType } : {}), importPath: declaration.importPath, confidence: 1, // Provenance: the user explicitly declared this tag → component mapping // (not an inferred htmlEquivalent), so prefer-library may enforce a // specific interactive tag against this exact (possibly non-Fragments) // component name. See isEnforceableHtmlEquivalent(explicit). declaredHtmlEquivalent: true, })) ); } function readPreferLibraryRule(config: GovernanceConfig | undefined): PreferLibraryRuleConfig { const raw = config?.rules?.["components/prefer-library"]; return raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as PreferLibraryRuleConfig) : {}; }