snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

13
apps/v4/lib/ai.ts Normal file
View File

@@ -0,0 +1,13 @@
// Compatibility bridge for v4 examples that still import from `@/lib/ai`.
// The public chat API lives in `@shadcn/helpers/ai-sdk`; `getMessageText`
// remains local to the app.
import type { UIMessage } from "ai"
export * from "@shadcn/helpers/ai-sdk"
export function getMessageText(message: Pick<UIMessage, "parts">) {
return message.parts.reduce(
(text, part) => (part.type === "text" ? text + part.text : text),
""
)
}

72
apps/v4/lib/blocks.ts Normal file
View File

@@ -0,0 +1,72 @@
"use server"
import { registryItemSchema, registrySchema } from "shadcn/schema"
import { type z } from "zod"
import { registry as baseRegistry } from "@/registry/bases/base/registry"
import { registry as radixRegistry } from "@/registry/bases/radix/registry"
export async function getAllBlockIds(
types: z.infer<typeof registryItemSchema>["type"][] = [
"registry:block",
"registry:internal",
],
categories: string[] = []
): Promise<string[]> {
const blocks = await getAllBlocks(types, categories)
return blocks.map((block) => block.name)
}
export async function getAllBlocks(
types: z.infer<typeof registryItemSchema>["type"][] = [
"registry:block",
"registry:internal",
],
categories: string[] = []
) {
const { Index: StylesIndex } = await import("@/registry/__index__")
const allBlocks = new Map<string, z.infer<typeof registryItemSchema>>()
const baseRegistries = [baseRegistry, radixRegistry]
.map((registry) => registrySchema.safeParse(registry))
.filter((result) => result.success)
.map((result) => result.data)
for (const registry of baseRegistries) {
for (const item of registry.items) {
allBlocks.set(`${item.type}:${item.name}`, item)
}
}
for (const style in StylesIndex) {
const styleIndex = StylesIndex[style]
if (typeof styleIndex !== "object" || styleIndex === null) {
continue
}
for (const itemName in styleIndex) {
const item = styleIndex[itemName]
allBlocks.set(`${item.type}:${item.name}`, item)
}
}
// Validate each block.
const validatedBlocks = Array.from(allBlocks.values())
.map((block) => {
const result = registryItemSchema.safeParse(block)
return result.success ? result.data : null
})
.filter(
(block): block is z.infer<typeof registryItemSchema> => block !== null
)
return validatedBlocks.filter(
(block) =>
types.includes(block.type) &&
(categories.length === 0 ||
block.categories?.some((category) => categories.includes(category))) &&
!block.name.startsWith("chart-")
)
}

27
apps/v4/lib/categories.ts Normal file
View File

@@ -0,0 +1,27 @@
export const registryCategories = [
{
name: "Sidebar",
slug: "sidebar",
hidden: false,
},
{
name: "Dashboard",
slug: "dashboard",
hidden: true,
},
{
name: "Authentication",
slug: "authentication",
hidden: true,
},
{
name: "Login",
slug: "login",
hidden: false,
},
{
name: "Signup",
slug: "signup",
hidden: false,
},
]

50
apps/v4/lib/changelog.ts Normal file
View File

@@ -0,0 +1,50 @@
import fs from "fs"
import path from "path"
import fm from "front-matter"
import { source } from "@/lib/source"
export type ChangelogPageData = {
title: string
description?: string
}
export type ChangelogPage = ReturnType<typeof source.getPages>[number] & {
date: Date | null
}
// Reads the date from the frontmatter of a changelog file.
export function getDateFromFile(slugs: string[]) {
const filePath = path.join(
process.cwd(),
"content/docs",
...slugs.slice(0, -1),
`${slugs[slugs.length - 1]}.mdx`
)
try {
const content = fs.readFileSync(filePath, "utf-8")
const { attributes } = fm<{ date?: string | Date }>(content)
if (attributes.date) {
return new Date(attributes.date)
}
} catch {
// File not found or parse error.
}
return null
}
// Gets all changelog pages sorted by date descending.
export function getChangelogPages() {
return source
.getPages()
.filter((page) => page.slugs[0] === "changelog" && page.slugs.length > 1)
.map((page) => ({
...page,
date: getDateFromFile(page.slugs),
}))
.sort((a, b) => {
const dateA = a.date?.getTime() ?? 0
const dateB = b.date?.getTime() ?? 0
return dateB - dateA
})
}

96
apps/v4/lib/colors.ts Normal file
View File

@@ -0,0 +1,96 @@
import { z } from "zod"
import { colors } from "@/registry/_legacy-colors"
const colorSchema = z.object({
name: z.string(),
id: z.string(),
scale: z.number(),
className: z.string(),
hex: z.string(),
rgb: z.string(),
hsl: z.string(),
foreground: z.string(),
oklch: z.string(),
var: z.string(),
})
const colorPaletteSchema = z.object({
name: z.string(),
colors: z.array(colorSchema),
})
export type ColorPalette = z.infer<typeof colorPaletteSchema>
export function getColorFormat(color: Color) {
return {
className: `bg-${color.name}-100`,
hex: color.hex,
rgb: color.rgb,
hsl: color.hsl,
oklch: color.oklch,
var: `--color-${color.name}-${color.scale}`,
}
}
export type ColorFormat = keyof ReturnType<typeof getColorFormat>
export function getColors() {
const tailwindColors = colorPaletteSchema.array().parse(
Object.entries(colors)
.map(([name, color]) => {
if (!Array.isArray(color)) {
return null
}
return {
name,
colors: color.map((color) => {
const rgb = color.rgb.replace(
/^rgb\((\d+),(\d+),(\d+)\)$/,
"$1 $2 $3"
)
return {
...color,
name,
id: `${name}-${color.scale}`,
className: `${name}-${color.scale}`,
var: `--color-${name}-${color.scale}`,
rgb,
hsl: color.hsl.replace(
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
"$1 $2 $3"
),
oklch: `oklch(${color.oklch.replace(
/^oklch\(([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\)$/,
"$1 $2 $3"
)})`,
foreground: getForegroundFromBackground(rgb),
}
}),
}
})
.filter(Boolean)
)
return tailwindColors
}
export type Color = ReturnType<typeof getColors>[number]["colors"][number]
function getForegroundFromBackground(rgb: string) {
const [r, g, b] = rgb.split(" ").map(Number)
function toLinear(number: number): number {
const base = number / 255
return base <= 0.04045
? base / 12.92
: Math.pow((base + 0.055) / 1.055, 2.4)
}
const luminance =
0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b)
return luminance > 0.179 ? "#000" : "#fff"
}

65
apps/v4/lib/components.ts Normal file
View File

@@ -0,0 +1,65 @@
// All available UI components.
export const UI_COMPONENTS = [
"accordion",
"alert",
"alert-dialog",
"aspect-ratio",
"avatar",
"badge",
"breadcrumb",
"button",
"button-group",
"calendar",
"card",
"carousel",
"chart",
"checkbox",
"collapsible",
"combobox",
"command",
"context-menu",
"data-table",
"date-picker",
"dialog",
"direction",
"drawer",
"dropdown-menu",
"empty",
"field",
"form",
"hover-card",
"input",
"input-group",
"input-otp",
"item",
"kbd",
"label",
"menubar",
"native-select",
"navigation-menu",
"pagination",
"popover",
"progress",
"radio-group",
"resizable",
"scroll-area",
"select",
"separator",
"sheet",
"sidebar",
"skeleton",
"slider",
"sonner",
"spinner",
"switch",
"table",
"tabs",
"textarea",
"toast",
"toggle",
"toggle-group",
"tooltip",
"typography",
] as const
export type UIComponent = (typeof UI_COMPONENTS)[number]

50
apps/v4/lib/config.ts Normal file
View File

@@ -0,0 +1,50 @@
export const siteConfig = {
name: "shadcn/ui",
url: "https://ui.shadcn.com",
ogImage: "https://ui.shadcn.com/og.jpg",
description:
"Composable, accessible components with thoughtful defaults. Build your own component library with code you can customize, extend, and make your own.",
links: {
twitter: "https://twitter.com/shadcn",
github: "https://github.com/shadcn-ui/ui",
},
navItems: [
{
href: "/",
label: "Home",
},
{
href: "/docs/installation",
label: "Docs",
},
{
href: "/docs/components",
label: "Components",
},
{
href: "/blocks",
label: "Blocks",
},
{
href: "/charts/area",
label: "Charts",
},
{
href: "/docs/directory",
label: "Directory",
},
{
href: "/typeset",
label: "Typeset",
},
{
href: "/create",
label: "Create",
},
],
}
export const META_THEME_COLORS = {
light: "#ffffff",
dark: "#09090b",
}

View File

@@ -0,0 +1,66 @@
export const DOCS_SIDEBAR_SCROLL_STORAGE_KEY = "docs-sidebar-scroll"
export const DOCS_SIDEBAR_SCROLL_RESTORE_SCRIPT = `(function () {
if (location.pathname !== "/docs" && !location.pathname.startsWith("/docs/")) return
var stored = null
try {
stored = JSON.parse(sessionStorage.getItem("${DOCS_SIDEBAR_SCROLL_STORAGE_KEY}"))
} catch (e) {}
function getActiveItem(container) {
var items = container.querySelectorAll('[data-active="true"]')
var active = null
var activePathLength = -1
var activeDistance = Infinity
var containerCenter = container.getBoundingClientRect().top + container.clientHeight / 2
for (var i = 0; i < items.length; i++) {
var link = items[i].querySelector('a[href]')
var href = items[i].getAttribute('href') || (link && link.getAttribute('href'))
var pathLength = href ? href.length : 0
var itemRect = items[i].getBoundingClientRect()
var distance = Math.abs(itemRect.top + itemRect.height / 2 - containerCenter)
if (pathLength > activePathLength || (pathLength === activePathLength && distance < activeDistance)) {
active = items[i]
activePathLength = pathLength
activeDistance = distance
}
}
return active
}
function restoreScroll() {
var container = document.querySelector('[data-docs-sidebar-content]')
if (!container || !container.clientHeight) return
if (stored && stored.pathname === location.pathname) {
container.scrollTop = stored.scrollTop
return
}
var active = getActiveItem(container)
if (!active) return
var containerRect = container.getBoundingClientRect()
var activeRect = active.getBoundingClientRect()
if (activeRect.top >= containerRect.top && activeRect.bottom <= containerRect.bottom) return
container.scrollTop += activeRect.top - containerRect.top - (container.clientHeight - activeRect.height) / 2
}
var observer = new MutationObserver(restoreScroll)
observer.observe(document.documentElement, { childList: true, subtree: true })
restoreScroll()
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
restoreScroll()
observer.disconnect()
}, { once: true })
} else {
observer.disconnect()
}
})()`

12
apps/v4/lib/docs.ts Normal file
View File

@@ -0,0 +1,12 @@
export const PAGES_NEW = [
"/docs/changelog",
"/docs/changelog/2026-08-questionnaire",
"/docs/registry/dynamic-search",
"/docs/components/radix/questionnaire",
"/docs/components/base/questionnaire",
"/docs/components/aria/questionnaire",
"/docs/react/questionnaire",
"/docs/registry/health",
]
export const PAGES_UPDATED = []

41
apps/v4/lib/events.ts Normal file
View File

@@ -0,0 +1,41 @@
import va from "@vercel/analytics"
import { z } from "zod"
const eventSchema = z.object({
name: z.enum([
"copy_npm_command",
"copy_usage_import_code",
"copy_usage_code",
"copy_primitive_code",
"copy_theme_code",
"copy_block_code",
"copy_chunk_code",
"enable_lift_mode",
"copy_chart_code",
"copy_chart_theme",
"copy_chart_data",
"copy_color",
"set_layout",
"search_query",
"create_app",
"copy_create_share_url",
"copy_registry_add_command",
"copy_preset_command",
"copy_apply_command",
"copy_typeset_css",
"copy_typeset_prompt",
]),
// declare type AllowedPropertyValues = string | number | boolean | null
properties: z
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
.optional(),
})
export type Event = z.infer<typeof eventSchema>
export function trackEvent(input: Event): void {
const event = eventSchema.parse(input)
if (event) {
va.track(event.name, event.properties)
}
}

1
apps/v4/lib/flags.ts Normal file
View File

@@ -0,0 +1 @@
export const showMcpDocs = true

View File

@@ -0,0 +1,331 @@
export type FontDefinition = {
name: string
title: string
type: "sans" | "mono" | "serif"
family: string
registryVariable: "--font-sans" | "--font-mono" | "--font-serif"
previewVariable: string
provider: "google"
import: string
dependency: string
subsets: readonly string[]
weight?: readonly string[]
}
export const FONT_DEFINITIONS = [
{
name: "geist",
title: "Geist",
type: "sans",
family: "'Geist Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-geist-sans",
provider: "google",
import: "Geist",
dependency: "@fontsource-variable/geist",
subsets: ["latin"],
},
{
name: "inter",
title: "Inter",
type: "sans",
family: "'Inter Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-inter",
provider: "google",
import: "Inter",
dependency: "@fontsource-variable/inter",
subsets: ["latin"],
},
{
name: "noto-sans",
title: "Noto Sans",
type: "sans",
family: "'Noto Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-noto-sans",
provider: "google",
import: "Noto_Sans",
dependency: "@fontsource-variable/noto-sans",
subsets: ["latin"],
},
{
name: "nunito-sans",
title: "Nunito Sans",
type: "sans",
family: "'Nunito Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-nunito-sans",
provider: "google",
import: "Nunito_Sans",
dependency: "@fontsource-variable/nunito-sans",
subsets: ["latin"],
},
{
name: "figtree",
title: "Figtree",
type: "sans",
family: "'Figtree Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-figtree",
provider: "google",
import: "Figtree",
dependency: "@fontsource-variable/figtree",
subsets: ["latin"],
},
{
name: "roboto",
title: "Roboto",
type: "sans",
family: "'Roboto Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-roboto",
provider: "google",
import: "Roboto",
dependency: "@fontsource-variable/roboto",
subsets: ["latin"],
},
{
name: "raleway",
title: "Raleway",
type: "sans",
family: "'Raleway Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-raleway",
provider: "google",
import: "Raleway",
dependency: "@fontsource-variable/raleway",
subsets: ["latin"],
},
{
name: "dm-sans",
title: "DM Sans",
type: "sans",
family: "'DM Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-dm-sans",
provider: "google",
import: "DM_Sans",
dependency: "@fontsource-variable/dm-sans",
subsets: ["latin"],
},
{
name: "public-sans",
title: "Public Sans",
type: "sans",
family: "'Public Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-public-sans",
provider: "google",
import: "Public_Sans",
dependency: "@fontsource-variable/public-sans",
subsets: ["latin"],
},
{
name: "outfit",
title: "Outfit",
type: "sans",
family: "'Outfit Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-outfit",
provider: "google",
import: "Outfit",
dependency: "@fontsource-variable/outfit",
subsets: ["latin"],
},
{
name: "oxanium",
title: "Oxanium",
type: "sans",
family: "'Oxanium Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-oxanium",
provider: "google",
import: "Oxanium",
dependency: "@fontsource-variable/oxanium",
subsets: ["latin"],
},
{
name: "manrope",
title: "Manrope",
type: "sans",
family: "'Manrope Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-manrope",
provider: "google",
import: "Manrope",
dependency: "@fontsource-variable/manrope",
subsets: ["latin"],
},
{
name: "space-grotesk",
title: "Space Grotesk",
type: "sans",
family: "'Space Grotesk Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-space-grotesk",
provider: "google",
import: "Space_Grotesk",
dependency: "@fontsource-variable/space-grotesk",
subsets: ["latin"],
},
{
name: "montserrat",
title: "Montserrat",
type: "sans",
family: "'Montserrat Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-montserrat",
provider: "google",
import: "Montserrat",
dependency: "@fontsource-variable/montserrat",
subsets: ["latin"],
},
{
name: "ibm-plex-sans",
title: "IBM Plex Sans",
type: "sans",
family: "'IBM Plex Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-ibm-plex-sans",
provider: "google",
import: "IBM_Plex_Sans",
dependency: "@fontsource-variable/ibm-plex-sans",
subsets: ["latin"],
},
{
name: "source-sans-3",
title: "Source Sans 3",
type: "sans",
family: "'Source Sans 3 Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-source-sans-3",
provider: "google",
import: "Source_Sans_3",
dependency: "@fontsource-variable/source-sans-3",
subsets: ["latin"],
},
{
name: "instrument-sans",
title: "Instrument Sans",
type: "sans",
family: "'Instrument Sans Variable', sans-serif",
registryVariable: "--font-sans",
previewVariable: "--font-instrument-sans",
provider: "google",
import: "Instrument_Sans",
dependency: "@fontsource-variable/instrument-sans",
subsets: ["latin"],
},
{
name: "jetbrains-mono",
title: "JetBrains Mono",
type: "mono",
family: "'JetBrains Mono Variable', monospace",
registryVariable: "--font-mono",
previewVariable: "--font-jetbrains-mono",
provider: "google",
import: "JetBrains_Mono",
dependency: "@fontsource-variable/jetbrains-mono",
subsets: ["latin"],
},
{
name: "geist-mono",
title: "Geist Mono",
type: "mono",
family: "'Geist Mono Variable', monospace",
registryVariable: "--font-mono",
previewVariable: "--font-geist-mono",
provider: "google",
import: "Geist_Mono",
dependency: "@fontsource-variable/geist-mono",
subsets: ["latin"],
},
{
name: "noto-serif",
title: "Noto Serif",
type: "serif",
family: "'Noto Serif Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-noto-serif",
provider: "google",
import: "Noto_Serif",
dependency: "@fontsource-variable/noto-serif",
subsets: ["latin"],
},
{
name: "roboto-slab",
title: "Roboto Slab",
type: "serif",
family: "'Roboto Slab Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-roboto-slab",
provider: "google",
import: "Roboto_Slab",
dependency: "@fontsource-variable/roboto-slab",
subsets: ["latin"],
},
{
name: "merriweather",
title: "Merriweather",
type: "serif",
family: "'Merriweather Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-merriweather",
provider: "google",
import: "Merriweather",
dependency: "@fontsource-variable/merriweather",
subsets: ["latin"],
},
{
name: "lora",
title: "Lora",
type: "serif",
family: "'Lora Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-lora",
provider: "google",
import: "Lora",
dependency: "@fontsource-variable/lora",
subsets: ["latin"],
},
{
name: "playfair-display",
title: "Playfair Display",
type: "serif",
family: "'Playfair Display Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-playfair-display",
provider: "google",
import: "Playfair_Display",
dependency: "@fontsource-variable/playfair-display",
subsets: ["latin"],
},
{
name: "eb-garamond",
title: "EB Garamond",
type: "serif",
family: "'EB Garamond Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-eb-garamond",
provider: "google",
import: "EB_Garamond",
dependency: "@fontsource-variable/eb-garamond",
subsets: ["latin"],
},
{
name: "instrument-serif",
title: "Instrument Serif",
type: "serif",
family: "'Instrument Serif', serif",
registryVariable: "--font-serif",
previewVariable: "--font-instrument-serif",
provider: "google",
import: "Instrument_Serif",
dependency: "@fontsource/instrument-serif",
subsets: ["latin"],
weight: ["400"],
},
] as const satisfies readonly FontDefinition[]
export type FontName = (typeof FONT_DEFINITIONS)[number]["name"]

41
apps/v4/lib/fonts.ts Normal file
View File

@@ -0,0 +1,41 @@
import {
Geist_Mono as FontMono,
Noto_Sans_Arabic as FontNotoSansArabic,
Noto_Sans_Hebrew as FontNotoSansHebrew,
Geist as FontSans,
} from "next/font/google"
import { cn } from "cn"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
})
const fontHeading = FontSans({
subsets: ["latin"],
variable: "--font-heading",
})
const fontMono = FontMono({
subsets: ["latin"],
variable: "--font-mono",
weight: ["400"],
})
const fontNotoSansArabic = FontNotoSansArabic({
subsets: ["latin"],
variable: "--font-ar",
})
const fontNotoSansHebrew = FontNotoSansHebrew({
subsets: ["latin"],
variable: "--font-he",
})
export const fontVariables = cn(
fontSans.variable,
fontHeading.variable,
fontMono.variable,
fontNotoSansArabic.variable,
fontNotoSansHebrew.variable
)

142
apps/v4/lib/format-code.ts Normal file
View File

@@ -0,0 +1,142 @@
import { promises as fsPromises } from "fs"
import path from "path"
import {
createStyleMap,
transformIcons,
transformMenu,
transformRender,
transformStyle,
} from "shadcn/utils"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import { BASES } from "@/registry/bases"
function getStyleFromStyleName(styleName: string) {
const parts = styleName.split("-")
return parts.length > 1 ? parts.slice(1).join("-") : styleName
}
function buildDisplayConfig(styleName: string) {
return {
$schema: "https://ui.shadcn.com/schema.json",
style: styleName,
rsc: true,
tsx: true,
tailwind: {
config: "",
css: "",
baseColor: "neutral",
cssVariables: true,
prefix: "",
},
iconLibrary: "lucide",
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
resolvedPaths: {
cwd: "/",
tailwindConfig: "",
tailwindCss: "",
utils: "@/lib/utils",
components: "@/components",
lib: "@/lib",
hooks: "@/hooks",
ui: "@/components/ui",
},
}
}
type DisplayTransformer = (opts: {
filename: string
raw: string
sourceFile: SourceFile
config: ReturnType<typeof buildDisplayConfig>
}) => Promise<unknown>
const styleMapCache = new Map<string, Record<string, string>>()
async function getStyleMap(styleName: string) {
const style = getStyleFromStyleName(styleName)
if (styleMapCache.has(style)) {
return styleMapCache.get(style)!
}
try {
const cssPath = path.join(
process.cwd(),
`registry/styles/style-${style}.css`
)
const css = await fsPromises.readFile(cssPath, "utf-8")
const styleMap = createStyleMap(css)
styleMapCache.set(style, styleMap)
return styleMap
} catch {
return {}
}
}
export async function formatCode(code: string, styleName: string) {
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Always rewrite the legacy tree so src-based sources render clean imports
// regardless of the styleName in play.
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
for (const base of BASES) {
code = code.replaceAll(`@/registry/bases/${base.name}/`, "@/components/")
code = code.replaceAll(
`@/examples/${base.name}/ui-rtl/`,
"@/components/ui/"
)
code = code.replaceAll(`@/examples/${base.name}/ui/`, "@/components/ui/")
code = code.replaceAll(`@/examples/${base.name}/lib/`, "@/lib/")
code = code.replaceAll(`@/examples/${base.name}/hooks/`, "@/hooks/")
}
code = code.replace(
/@\/styles\/([\w-]+)\/(ui-rtl|ui)\/([\w-]+)/g,
(match, _styleName, type, component) => {
if (type === "ui" || type === "ui-rtl") {
return `@/components/ui/${component}`
}
return match
}
)
code = code.replaceAll("export default", "export")
try {
const styleMap = await getStyleMap(styleName)
const transformed = await transformStyle(code, { styleMap })
const config = buildDisplayConfig(styleName)
const project = new Project({ compilerOptions: {} })
const sourceFile = project.createSourceFile("component.tsx", transformed, {
scriptKind: ScriptKind.TSX,
})
const transformers: DisplayTransformer[] = [
transformIcons as DisplayTransformer,
transformMenu as DisplayTransformer,
transformRender as DisplayTransformer,
]
for (const transformer of transformers) {
await transformer({
filename: "component.tsx",
raw: transformed,
sourceFile,
config,
})
}
return sourceFile.getText()
} catch (error) {
console.error("Transform failed:", error)
return code
}
}

View File

@@ -0,0 +1,98 @@
import { createHash } from "crypto"
import { LRUCache } from "lru-cache"
import { codeToHtml } from "shiki"
import type { ShikiTransformer } from "shiki"
// LRU cache for cross-request caching of highlighted code.
// Shiki highlighting is CPU-intensive and deterministic, so caching is safe.
const highlightCache = new LRUCache<string, string>({
max: 500,
ttl: 1000 * 60 * 60, // 1 hour.
})
export const transformers = [
{
code(node) {
if (node.tagName === "code") {
const raw = this.source
node.properties["__raw__"] = raw
if (raw.startsWith("npm install")) {
node.properties["__npm__"] = raw
node.properties["__yarn__"] = raw.replace("npm install", "yarn add")
node.properties["__pnpm__"] = raw.replace("npm install", "pnpm add")
node.properties["__bun__"] = raw.replace("npm install", "bun add")
} else if (raw.startsWith("npx create-")) {
node.properties["__npm__"] = raw
node.properties["__yarn__"] = raw.replace(
"npx create-",
"yarn create "
)
node.properties["__pnpm__"] = raw.replace(
"npx create-",
"pnpm create "
)
node.properties["__bun__"] = raw.replace("npx", "bunx --bun")
} else if (raw.startsWith("npm create")) {
// npm create.
node.properties["__npm__"] = raw
node.properties["__yarn__"] = raw.replace("npm create", "yarn create")
node.properties["__pnpm__"] = raw.replace("npm create", "pnpm create")
node.properties["__bun__"] = raw.replace("npm create", "bun create")
} else if (raw.startsWith("npx")) {
// npx.
node.properties["__npm__"] = raw
node.properties["__yarn__"] = raw.replace("npx", "yarn dlx")
node.properties["__pnpm__"] = raw.replace("npx", "pnpm dlx")
node.properties["__bun__"] = raw.replace("npx", "bunx --bun")
} else if (raw.startsWith("npm run")) {
// npm run.
node.properties["__npm__"] = raw
node.properties["__yarn__"] = raw.replace("npm run", "yarn")
node.properties["__pnpm__"] = raw.replace("npm run", "pnpm")
node.properties["__bun__"] = raw.replace("npm run", "bun")
}
}
},
},
] as ShikiTransformer[]
export async function highlightCode(code: string, language: string = "tsx") {
// Create cache key from code content and language.
const cacheKey = createHash("sha256")
.update(`${language}:${code}`)
.digest("hex")
// Check cache first.
const cached = highlightCache.get(cacheKey)
if (cached) {
return cached
}
const html = await codeToHtml(code, {
lang: language,
themes: {
dark: "github-dark",
light: "github-light",
},
transformers: [
{
pre(node) {
node.properties["class"] =
"no-scrollbar min-w-0 overflow-x-auto overflow-y-auto overscroll-x-contain overscroll-y-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0 !bg-transparent"
},
code(node) {
node.properties["data-line-numbers"] = ""
},
line(node) {
node.properties["data-line"] = ""
},
},
],
})
// Cache the result.
highlightCache.set(cacheKey, html)
return html
}

143
apps/v4/lib/llm.ts Normal file
View File

@@ -0,0 +1,143 @@
import fs from "fs"
import { ExamplesIndex } from "@/examples/__index__"
import { PAGES_NEW } from "@/lib/docs"
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { Index as StylesIndex } from "@/registry/__index__"
import { type Style } from "@/registry/_legacy-styles"
import { BASES } from "@/registry/bases"
import { Index as BasesIndex } from "@/registry/bases/__index__"
function getBaseForStyle(styleName: string) {
for (const base of BASES) {
if (styleName.startsWith(`${base.name}-`)) {
return base.name
}
}
return null
}
function getDemoFilePath(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
const demo =
ExamplesIndex[styleName]?.[name] ??
(base ? ExamplesIndex[base]?.[name] : undefined)
if (!demo) {
return null
}
return demo.filePath
}
function getRegistryEntry(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
return (
StylesIndex[styleName]?.[name] ??
(base ? BasesIndex[base]?.[name] : undefined)
)
}
function getComponentsList(variant: "all" | "new") {
const componentsFolder = source.pageTree.children.find(
(page) => page.$id === "components"
)
if (componentsFolder?.type !== "folder") {
return ""
}
return getPagesFromFolder(componentsFolder as PageTreeFolder, "base")
.filter(
(component) => variant === "all" || PAGES_NEW.includes(component.url)
)
.map((component) => {
const slug = component.url.replace(/^\/docs\//, "").split("/")
const description = source.getPage(slug)?.data.description?.trim()
const url = absoluteUrl(component.url.replace("/base/", "/"))
return `- [${component.name}](${url})${
description ? `: ${description}` : ""
}`
})
.join("\n")
}
export function replaceComponentsList(content: string) {
return content
.replace(
/<ComponentsList\s+variant=["']new["']\s*\/>/g,
getComponentsList("new")
)
.replace(/<ComponentsList\s*\/>/g, getComponentsList("all"))
}
export function processMdxForLLMs(content: string, style: Style["name"]) {
content = replaceComponentsList(content)
const componentPreviewRegex =
/<ComponentPreview[\s\S]*?name="([^"]+)"[\s\S]*?\/>/g
return content.replace(componentPreviewRegex, (match, name) => {
try {
// Try to extract styleName from the match.
const styleNameMatch = match.match(/styleName="([^"]+)"/)
const effectiveStyle = styleNameMatch ? styleNameMatch[1] : style
let src = getDemoFilePath(name, effectiveStyle)
if (!src) {
const component = getRegistryEntry(name, effectiveStyle)
if (!component?.files) {
return match
}
src = component.files[0]?.path
}
if (!src) {
return match
}
let source = fs.readFileSync(src, "utf8")
// Replace all base-specific paths.
for (const base of BASES) {
source = source.replaceAll(
`@/registry/bases/${base.name}/`,
"@/components/"
)
source = source.replaceAll(
`@/examples/${base.name}/ui-rtl/`,
"@/components/ui/"
)
source = source.replaceAll(
`@/examples/${base.name}/ui/`,
"@/components/ui/"
)
source = source.replaceAll(`@/examples/${base.name}/lib/`, "@/lib/")
source = source.replaceAll(`@/examples/${base.name}/hooks/`, "@/hooks/")
}
source = source.replace(
/@\/styles\/([\w-]+)\/(ui-rtl|ui)\/([\w-]+)/g,
(match, _styleName, type, component) => {
if (type === "ui" || type === "ui-rtl") {
return `@/components/ui/${component}`
}
return match
}
)
source = source.replaceAll(
`@/registry/${effectiveStyle}/`,
"@/components/"
)
source = source.replaceAll("export default", "export")
return `\`\`\`tsx
${source}
\`\`\``
} catch (error) {
console.error(`Error processing ComponentPreview ${name}:`, error)
return match
}
})
}

View File

@@ -0,0 +1,137 @@
import type { Variants } from "motion/react"
const ANIMATIONS = [
{
id: "fade",
name: "Fade",
variants: {
initial: { opacity: 0 },
animate: {
opacity: 1,
transition: { duration: 0.22, ease: "easeOut" },
},
exit: { opacity: 0, transition: { duration: 0.15 } },
},
},
{
id: "slide-up",
name: "Slide Up",
variants: {
initial: { opacity: 0, y: 10 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.26, ease: [0.16, 1, 0.3, 1] },
},
exit: { opacity: 0, y: 4, transition: { duration: 0.15 } },
},
},
{
id: "slide-side",
name: "Slide Side",
variants: {
initial: { opacity: 0, x: 18 },
animate: {
opacity: 1,
x: 0,
transition: { duration: 0.28, ease: [0.16, 1, 0.3, 1] },
},
exit: {
opacity: 0,
x: 8,
transition: { duration: 0.15 },
},
},
},
{
id: "pop",
name: "Pop",
variants: {
initial: {
opacity: 0,
scale: 0.94,
y: 6,
originX: 1,
originY: 1,
},
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 500, damping: 34, mass: 0.7 },
},
exit: { opacity: 0, scale: 0.98, transition: { duration: 0.12 } },
},
},
{
id: "spring-bounce",
name: "Spring Bounce",
variants: {
initial: { opacity: 0, y: 12, scale: 0.96 },
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 520,
damping: 30,
mass: 0.7,
},
},
exit: { opacity: 0, scale: 0.98, transition: { duration: 0.12 } },
},
},
{
id: "blur-fade",
name: "Blur Fade",
variants: {
initial: { opacity: 0, filter: "blur(4px)", y: 6 },
animate: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.28, ease: "easeOut" },
},
exit: {
opacity: 0,
filter: "blur(2px)",
transition: { duration: 0.15 },
},
},
},
{
id: "scale-fade",
name: "Scale Fade",
variants: {
initial: { opacity: 0, scale: 0.98 },
animate: {
opacity: 1,
scale: 1,
transition: { duration: 0.24, ease: "easeOut" },
},
exit: { opacity: 0, scale: 0.99, transition: { duration: 0.12 } },
},
},
] as const satisfies {
id: string
name: string
variants: Variants
}[]
type MessageAnimationPreset = (typeof ANIMATIONS)[number]
type MessageAnimationId = MessageAnimationPreset["id"]
const MESSAGE_ANIMATIONS = ANIMATIONS.reduce(
(acc, preset) => {
acc[preset.id] = preset
return acc
},
{} as Record<MessageAnimationId, MessageAnimationPreset>
)
export {
MESSAGE_ANIMATIONS,
type MessageAnimationId,
type MessageAnimationPreset,
}

64
apps/v4/lib/page-tree.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { source } from "@/lib/source"
export type PageTreeNode = (typeof source.pageTree)["children"][number]
export type PageTreeFolder = Extract<PageTreeNode, { type: "folder" }>
export type PageTreePage = Extract<PageTreeNode, { type: "page" }>
// Recursively find all pages in a folder tree.
export function getAllPagesFromFolder(folder: PageTreeFolder): PageTreePage[] {
const pages: PageTreePage[] = []
for (const child of folder.children) {
if (child.type === "page") {
pages.push(child)
} else if (child.type === "folder") {
pages.push(...getAllPagesFromFolder(child))
}
}
return pages
}
// Get the pages from a folder, handling nested base folders (radix/base).
export function getPagesFromFolder(
folder: PageTreeFolder,
currentBase: string
): PageTreePage[] {
// For the components folder, find the base subfolder.
if (folder.$id === "components" || folder.name === "Components") {
for (const child of folder.children) {
if (child.type === "folder") {
// Match by $id or by name.
const isRadix = child.$id === "radix" || child.name === "Radix UI"
const isBase = child.$id === "base" || child.name === "Base UI"
const isReactAria = child.$id === "aria" || child.name === "React Aria"
if (
(currentBase === "radix" && isRadix) ||
(currentBase === "base" && isBase) ||
(currentBase === "aria" && isReactAria)
) {
return child.children.filter(
(c): c is PageTreePage => c.type === "page"
)
}
}
}
// Fallback: return all pages from nested folders.
return getAllPagesFromFolder(folder).filter(
(page) => !page.url.endsWith("/components")
)
}
// For other folders, return direct page children.
return folder.children.filter(
(child): child is PageTreePage => child.type === "page"
)
}
// Get current base (radix, base, or aria) from pathname.
export function getCurrentBase(pathname: string): string {
const baseMatch = pathname.match(/\/docs\/components\/(radix|base|aria)\//)
return baseMatch ? baseMatch[1] : "base" // Default to base.
}

7
apps/v4/lib/read-file.ts Normal file
View File

@@ -0,0 +1,7 @@
import { promises as fs } from "fs"
import path from "path"
export async function readFileFromRoot(relativePath: string) {
const absolutePath = path.join(process.cwd(), relativePath)
return fs.readFile(absolutePath, "utf-8")
}

View File

@@ -0,0 +1,76 @@
import { z } from "zod"
const registryNamespaceSchema = z.string().regex(/^@[a-zA-Z0-9][a-zA-Z0-9_-]*$/)
const registryDirectoryEntrySchema = z
.object({
name: registryNamespaceSchema,
homepage: z.string().url(),
url: z
.string()
.url()
.refine((url) => url.includes("{name}"), {
message: "URL must include {name} placeholder",
}),
description: z.string(),
author: z.string().optional(),
logo: z.string(),
})
.strict()
const registryDirectorySchema = z
.array(registryDirectoryEntrySchema)
.superRefine((entries, context) => {
const names = new Set<string>()
entries.forEach((entry, index) => {
const name = entry.name.toLowerCase()
if (names.has(name)) {
context.addIssue({
code: "custom",
message: `Duplicate registry namespace: ${entry.name}`,
path: [index, "name"],
})
}
names.add(name)
})
})
const publicRegistryDirectoryEntrySchema = registryDirectoryEntrySchema.pick({
name: true,
homepage: true,
url: true,
description: true,
})
const publicRegistryDirectorySchema = z.array(
publicRegistryDirectoryEntrySchema
)
type RegistryDirectoryEntry = z.infer<typeof registryDirectoryEntrySchema>
function createPublicRegistryDirectory(
entries: readonly RegistryDirectoryEntry[]
) {
return entries.map(({ name, homepage, url, description }) => ({
name,
homepage,
url,
description,
}))
}
function normalizeRegistryName(value: string) {
return value.toLowerCase().replaceAll(" ", "").replace(/^@/, "")
}
export {
createPublicRegistryDirectory,
normalizeRegistryName,
publicRegistryDirectoryEntrySchema,
publicRegistryDirectorySchema,
registryDirectoryEntrySchema,
registryDirectorySchema,
registryNamespaceSchema,
}
export type { RegistryDirectoryEntry }

View File

@@ -0,0 +1,204 @@
import { describe, expect, it, vi } from "vitest"
import {
cleanRegistryHealthHistory,
loadRegistryHealthSnapshot,
loadRegistryMonitorState,
publishRegistryHealth,
type BlobOperations,
} from "./blob"
import type {
RegistryHealthSnapshot,
RegistryMonitorRun,
RegistryMonitorState,
} from "./schema"
function streamJson(value: unknown) {
const bytes = new TextEncoder().encode(JSON.stringify(value))
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(bytes)
controller.close()
},
})
}
function createOperations(overrides: Partial<BlobOperations> = {}) {
return {
get: vi.fn(async () => null),
put: vi.fn(async (pathname) => ({
url: `https://blob.example.com/${pathname}`,
})),
list: vi.fn(async () => ({ blobs: [], hasMore: false })),
del: vi.fn(async () => {}),
...overrides,
} satisfies BlobOperations
}
const STATE: RegistryMonitorState = {
schemaVersion: 1,
scoreVersion: 1,
updatedAt: "2026-08-24T12:00:00.000Z",
registries: {},
}
const SNAPSHOT: RegistryHealthSnapshot = {
schemaVersion: 1,
scoreVersion: 1,
generatedAt: "2026-08-24T12:00:00.000Z",
globalMeans: {
availability7d: 0.85,
availability30d: 0.85,
indexSchema: 0.9,
itemValidity: 0.9,
dryRun: 0.9,
},
registries: {},
}
const RUN: RegistryMonitorRun = {
schemaVersion: 1,
startedAt: "2026-08-24T12:00:00.000Z",
completedAt: "2026-08-24T12:01:00.000Z",
mode: "hourly",
totals: {
registries: 0,
reachable: 0,
unavailable: 0,
challenges: 0,
itemChecks: 0,
dryRuns: 0,
},
results: {},
diagnostics: [],
}
describe("loadRegistryMonitorState", () => {
it("returns null on the first run", async () => {
const state = await loadRegistryMonitorState({
token: "test-token",
operations: createOperations(),
})
expect(state).toBeNull()
})
it("validates stored state before using it", async () => {
const operations = createOperations({
get: vi.fn(async () => ({ stream: streamJson(STATE) })),
})
await expect(
loadRegistryMonitorState({ token: "test-token", operations })
).resolves.toEqual(STATE)
expect(operations.get).toHaveBeenCalledWith(
"registry-health/v1/state.json",
{
access: "private",
token: "test-token",
useCache: false,
}
)
})
})
describe("loadRegistryHealthSnapshot", () => {
it("reads and validates the private latest snapshot", async () => {
const abortController = new AbortController()
const operations = createOperations({
get: vi.fn(async () => ({ stream: streamJson(SNAPSHOT) })),
})
await expect(
loadRegistryHealthSnapshot({
token: "test-token",
storeId: "test-store-id",
abortSignal: abortController.signal,
operations,
})
).resolves.toEqual(SNAPSHOT)
expect(operations.get).toHaveBeenCalledWith(
"registry-health/v1/latest.json",
{
access: "private",
token: "test-token",
storeId: "test-store-id",
useCache: false,
abortSignal: abortController.signal,
}
)
})
it("rejects an invalid latest snapshot", async () => {
const operations = createOperations({
get: vi.fn(async () => ({
stream: streamJson({ ...SNAPSHOT, schemaVersion: 2 }),
})),
})
await expect(
loadRegistryHealthSnapshot({ token: "test-token", operations })
).rejects.toThrow()
})
})
describe("publishRegistryHealth", () => {
it("publishes immutable history before overwriting state and latest", async () => {
const operations = createOperations()
const result = await publishRegistryHealth({
state: STATE,
snapshot: SNAPSHOT,
run: RUN,
token: "test-token",
operations,
})
const calls = vi.mocked(operations.put).mock.calls
expect(calls.map(([pathname]) => pathname)).toEqual([
"registry-health/v1/runs/2026-08-24T12-00-00-000Z.json",
"registry-health/v1/daily/2026-08-24.json",
"registry-health/v1/state.json",
"registry-health/v1/latest.json",
])
expect(calls[0][2].allowOverwrite).toBe(false)
expect(calls[2][2].allowOverwrite).toBe(true)
expect(calls[3][2].allowOverwrite).toBe(true)
expect(calls.every(([, , options]) => options.access === "private")).toBe(
true
)
expect(result.latestPath).toBe("registry-health/v1/latest.json")
})
})
describe("cleanRegistryHealthHistory", () => {
it("paginates listings and deletes expired history", async () => {
const list = vi
.fn()
.mockResolvedValueOnce({
blobs: [
{
pathname: "registry-health/v1/runs/old.json",
uploadedAt: new Date("2026-08-01T00:00:00.000Z"),
},
],
cursor: "next",
hasMore: true,
})
.mockResolvedValueOnce({ blobs: [], hasMore: false })
.mockResolvedValueOnce({ blobs: [], hasMore: false })
const operations = createOperations({ list })
const result = await cleanRegistryHealthHistory({
token: "test-token",
now: new Date("2026-08-24T12:00:00.000Z"),
operations,
})
expect(list).toHaveBeenCalledTimes(3)
expect(operations.del).toHaveBeenCalledWith(
["registry-health/v1/runs/old.json"],
{ token: "test-token" }
)
expect(result.deleted).toBe(1)
})
})

View File

@@ -0,0 +1,294 @@
import {
del as deleteBlob,
get as getBlob,
list as listBlobs,
put as putBlob,
} from "@vercel/blob"
import {
registryHealthSnapshotSchema,
registryMonitorStateSchema,
type RegistryHealthSnapshot,
type RegistryMonitorRun,
type RegistryMonitorState,
} from "./schema"
const BLOB_PREFIX = "registry-health/v1"
const STATE_PATH = `${BLOB_PREFIX}/state.json`
const LATEST_PATH = `${BLOB_PREFIX}/latest.json`
type BlobListItem = {
pathname: string
uploadedAt: Date
}
type BlobOperations = {
get: (
pathname: string,
options: {
access: "private"
token?: string
storeId?: string
useCache: false
abortSignal?: AbortSignal
}
) => Promise<{ stream: ReadableStream<Uint8Array> | null } | null>
put: (
pathname: string,
body: string,
options: {
access: "private"
token?: string
addRandomSuffix: false
allowOverwrite?: boolean
cacheControlMaxAge: number
contentType: "application/json"
}
) => Promise<{ url: string }>
list: (options: {
prefix: string
cursor?: string
limit: number
token?: string
}) => Promise<{
blobs: BlobListItem[]
cursor?: string
hasMore: boolean
}>
del: (pathnames: string[], options: { token?: string }) => Promise<void>
}
const defaultOperations: BlobOperations = {
get: getBlob,
put: putBlob,
list: listBlobs,
del: deleteBlob,
}
async function readJsonStream(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader()
const chunks: Uint8Array[] = []
let size = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
size += value.byteLength
}
const body = new Uint8Array(size)
let offset = 0
for (const chunk of chunks) {
body.set(chunk, offset)
offset += chunk.byteLength
}
return JSON.parse(new TextDecoder().decode(body)) as unknown
}
export async function loadRegistryMonitorState({
token,
operations = defaultOperations,
}: {
token?: string
operations?: BlobOperations
}) {
const result = await operations.get(STATE_PATH, {
access: "private",
token,
useCache: false,
})
if (!result?.stream) return null
return registryMonitorStateSchema.parse(await readJsonStream(result.stream))
}
export async function loadRegistryHealthSnapshot({
token,
storeId,
abortSignal,
operations = defaultOperations,
}: {
token?: string
storeId?: string
abortSignal?: AbortSignal
operations?: BlobOperations
}) {
const result = await operations.get(LATEST_PATH, {
access: "private",
token,
storeId,
useCache: false,
abortSignal,
})
if (!result?.stream) return null
return registryHealthSnapshotSchema.parse(await readJsonStream(result.stream))
}
function getRunPath(run: RegistryMonitorRun) {
const timestamp = run.startedAt.replaceAll(":", "-").replaceAll(".", "-")
return `${BLOB_PREFIX}/runs/${timestamp}.json`
}
function getDailyPath(snapshot: RegistryHealthSnapshot) {
return `${BLOB_PREFIX}/daily/${snapshot.generatedAt.slice(0, 10)}.json`
}
function getDailyDocument(
state: RegistryMonitorState,
snapshot: RegistryHealthSnapshot
) {
const date = snapshot.generatedAt.slice(0, 10)
return {
schemaVersion: snapshot.schemaVersion,
scoreVersion: snapshot.scoreVersion,
date,
generatedAt: snapshot.generatedAt,
globalMeans: snapshot.globalMeans,
registries: Object.fromEntries(
Object.entries(state.registries).map(([name, entry]) => [
name,
{
aggregate: entry.daily.find((bucket) => bucket.date === date) ?? null,
health: snapshot.registries[name],
},
])
),
}
}
async function putJson({
pathname,
value,
token,
allowOverwrite = false,
operations,
}: {
pathname: string
value: unknown
token?: string
allowOverwrite?: boolean
operations: BlobOperations
}) {
return operations.put(pathname, JSON.stringify(value), {
access: "private",
token,
addRandomSuffix: false,
allowOverwrite,
cacheControlMaxAge: 60,
contentType: "application/json",
})
}
export async function publishRegistryHealth({
state,
snapshot,
run,
token,
operations = defaultOperations,
}: {
state: RegistryMonitorState
snapshot: RegistryHealthSnapshot
run: RegistryMonitorRun
token?: string
operations?: BlobOperations
}) {
await putJson({
pathname: getRunPath(run),
value: run,
token,
operations,
})
await putJson({
pathname: getDailyPath(snapshot),
value: getDailyDocument(state, snapshot),
token,
allowOverwrite: true,
operations,
})
await putJson({
pathname: STATE_PATH,
value: state,
token,
allowOverwrite: true,
operations,
})
await putJson({
pathname: LATEST_PATH,
value: snapshot,
token,
allowOverwrite: true,
operations,
})
return { latestPath: LATEST_PATH }
}
async function listAll({
prefix,
token,
operations,
}: {
prefix: string
token?: string
operations: BlobOperations
}) {
const blobs: BlobListItem[] = []
let cursor: string | undefined
do {
const page = await operations.list({
prefix,
cursor,
limit: 1000,
token,
})
blobs.push(...page.blobs)
cursor = page.hasMore ? page.cursor : undefined
} while (cursor)
return blobs
}
export async function cleanRegistryHealthHistory({
token,
now = new Date(),
operations = defaultOperations,
}: {
token?: string
now?: Date
operations?: BlobOperations
}) {
const policies = [
{ prefix: `${BLOB_PREFIX}/runs/`, retentionDays: 14 },
{ prefix: `${BLOB_PREFIX}/daily/`, retentionDays: 90 },
]
const stale: string[] = []
for (const policy of policies) {
const blobs = await listAll({
prefix: policy.prefix,
token,
operations,
})
const cutoff = now.getTime() - policy.retentionDays * 24 * 60 * 60 * 1000
stale.push(
...blobs
.filter((blob) => blob.uploadedAt.getTime() < cutoff)
.map((blob) => blob.pathname)
)
}
for (let index = 0; index < stale.length; index += 100) {
await operations.del(stale.slice(index, index + 100), { token })
}
return { deleted: stale.length }
}
export { BLOB_PREFIX, LATEST_PATH, STATE_PATH }
export type { BlobOperations }

View File

@@ -0,0 +1,117 @@
import { promises as fs } from "node:fs"
import os from "node:os"
import path from "node:path"
import { describe, expect, it } from "vitest"
import { getDryRunEnvironment, runLocalCliDryRun } from "./dry-run"
import { REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH } from "./schema"
describe("getDryRunEnvironment", () => {
it("allowlists process variables and excludes credentials", () => {
const environment = getDryRunEnvironment("/tmp/registry-health-test")
expect(environment).toMatchObject({
CI: "1",
NO_COLOR: "1",
XDG_CACHE_HOME: "/tmp/registry-health-test",
XDG_CONFIG_HOME: "/tmp/registry-health-test",
})
expect(environment).not.toHaveProperty("BLOB_READ_WRITE_TOKEN")
expect(environment).not.toHaveProperty("GITHUB_TOKEN")
expect(environment).not.toHaveProperty("GH_TOKEN")
})
})
describe("runLocalCliDryRun", () => {
it("creates a TypeScript project the CLI can load", async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), "registry-health-cli-test-")
)
const cliPath = path.join(directory, "cli.mjs")
await fs.writeFile(
cliPath,
`
import { readFileSync } from "node:fs"
const tsconfig = JSON.parse(readFileSync("tsconfig.json", "utf8"))
if (tsconfig.compilerOptions.paths["@/*"][0] !== "./*") {
process.exit(1)
}
`
)
try {
await expect(
runLocalCliDryRun({
namespace: "@acme",
item: "button",
registryUrl: "https://acme.example.com/r/{name}.json",
cliPath,
})
).resolves.toMatchObject({ success: true })
} finally {
await fs.rm(directory, { recursive: true, force: true })
}
})
it("captures bounded sanitized CLI errors", async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), "registry-health-cli-test-")
)
const cliPath = path.join(directory, "cli.mjs")
await fs.writeFile(
cliPath,
'process.stderr.write("\\u001b[31m" + "x".repeat(1500) + " useful failure\\u001b[0m"); process.exit(1)'
)
try {
const result = await runLocalCliDryRun({
namespace: "@acme",
item: "button",
registryUrl: "https://acme.example.com/r/{name}.json",
cliPath,
})
expect(result).toMatchObject({
success: false,
failureCode: "exit_1",
})
expect(result.failureMessage).toContain("useful failure")
expect(result.failureMessage).not.toContain("\u001b")
expect(result.failureMessage?.length).toBeLessThanOrEqual(
REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH
)
} finally {
await fs.rm(directory, { recursive: true, force: true })
}
})
it("force kills a CLI process that ignores SIGTERM", async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), "registry-health-cli-test-")
)
const cliPath = path.join(directory, "cli.mjs")
await fs.writeFile(
cliPath,
'process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'
)
try {
const result = await runLocalCliDryRun({
namespace: "@acme",
item: "button",
registryUrl: "https://acme.example.com/r/{name}.json",
cliPath,
timeoutMs: 25,
killGraceMs: 25,
})
expect(result).toMatchObject({
success: false,
failureCode: "timeout",
})
} finally {
await fs.rm(directory, { recursive: true, force: true })
}
})
})

View File

@@ -0,0 +1,186 @@
import { spawn, type ChildProcess } from "node:child_process"
import { promises as fs } from "node:fs"
import os from "node:os"
import path from "node:path"
import { stripVTControlCharacters } from "node:util"
import { DEFAULT_STYLE, type RegistryDryRunResult } from "./monitor"
import { REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH } from "./schema"
const MAX_CAPTURED_OUTPUT_LENGTH = 4000
function getFailureMessage(output: string) {
const message = stripVTControlCharacters(output).replace(/\s+/g, " ").trim()
return message
? message.slice(-REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH)
: undefined
}
async function runLocalCliDryRun({
namespace,
item,
registryUrl,
cliPath,
timeoutMs = 60_000,
killGraceMs = 5_000,
}: {
namespace: string
item: string
registryUrl: string
cliPath: string
timeoutMs?: number
killGraceMs?: number
}): Promise<RegistryDryRunResult> {
const startedAt = Date.now()
const temporaryDirectory = await fs.mkdtemp(
path.join(os.tmpdir(), "shadcn-registry-health-")
)
try {
await fs.mkdir(path.join(temporaryDirectory, "app"), { recursive: true })
await Promise.all([
fs.writeFile(
path.join(temporaryDirectory, "components.json"),
JSON.stringify(
{
$schema: "https://ui.shadcn.com/schema.json",
style: DEFAULT_STYLE,
rsc: true,
tsx: true,
tailwind: {
config: "",
css: "app/globals.css",
baseColor: "neutral",
cssVariables: true,
prefix: "",
},
iconLibrary: "lucide",
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
registries: {
[namespace]: registryUrl,
},
},
null,
2
)
),
fs.writeFile(path.join(temporaryDirectory, "app/globals.css"), ""),
fs.writeFile(
path.join(temporaryDirectory, "package.json"),
JSON.stringify({ private: true })
),
fs.writeFile(
path.join(temporaryDirectory, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
baseUrl: ".",
paths: {
"@/*": ["./*"],
},
jsx: "preserve",
},
},
null,
2
)
),
])
return await new Promise<RegistryDryRunResult>((resolve) => {
const child: ChildProcess = spawn(
process.execPath,
[
cliPath,
"add",
`${namespace}/${item}`,
"--dry-run",
"--yes",
"--cwd",
temporaryDirectory,
],
{
cwd: temporaryDirectory,
env: getDryRunEnvironment(temporaryDirectory),
stdio: ["ignore", "pipe", "pipe"],
}
)
let completed = false
let timedOut = false
let output = ""
let forceKillTimeout: NodeJS.Timeout | undefined
const captureOutput = (chunk: Buffer | string) => {
const value = typeof chunk === "string" ? chunk : chunk.toString("utf8")
output = `${output}${value}`.slice(-MAX_CAPTURED_OUTPUT_LENGTH)
}
const finish = (result: RegistryDryRunResult) => {
if (completed) return
completed = true
clearTimeout(timeout)
if (forceKillTimeout) clearTimeout(forceKillTimeout)
resolve(result)
}
const timeout = setTimeout(() => {
timedOut = true
child.kill("SIGTERM")
forceKillTimeout = setTimeout(() => {
if (!completed) child.kill("SIGKILL")
}, killGraceMs)
}, timeoutMs)
child.stdout?.on("data", captureOutput)
child.stderr?.on("data", captureOutput)
child.on("error", (error) => {
captureOutput(error.message)
const failureMessage = getFailureMessage(output)
finish({
success: false,
failureCode: timedOut ? "timeout" : "spawn_error",
...(failureMessage ? { failureMessage } : {}),
durationMs: Date.now() - startedAt,
})
})
child.on("close", (code, signal) => {
const failureCode = timedOut
? "timeout"
: code === 0
? undefined
: signal
? "terminated"
: `exit_${code}`
const failureMessage =
code === 0 ? undefined : getFailureMessage(output)
finish({
success: !timedOut && code === 0,
failureCode,
...(failureMessage ? { failureMessage } : {}),
durationMs: Date.now() - startedAt,
})
})
})
} finally {
await fs.rm(temporaryDirectory, { recursive: true, force: true })
}
}
function getDryRunEnvironment(temporaryDirectory: string) {
return {
PATH: process.env.PATH,
TMPDIR: os.tmpdir(),
XDG_CACHE_HOME: temporaryDirectory,
XDG_CONFIG_HOME: temporaryDirectory,
npm_config_userconfig: path.join(temporaryDirectory, ".npmrc"),
NODE_ENV: "production",
CI: "1",
NO_COLOR: "1",
} satisfies NodeJS.ProcessEnv
}
export { getDryRunEnvironment, runLocalCliDryRun }

View File

@@ -0,0 +1,746 @@
import { describe, expect, it, vi } from "vitest"
import type { RegistryDirectoryEntry } from "../registry-directory"
import { runRegistryMonitor } from "./monitor"
import {
REGISTRY_DRY_RUN_VERSION,
type RegistryMonitorEntryState,
type RegistryMonitorState,
} from "./schema"
const NOW = new Date("2026-08-24T12:00:00.000Z")
const DIRECTORY: RegistryDirectoryEntry[] = [
{
name: "@acme",
homepage: "https://acme.example.com",
url: "https://acme.example.com/r/{name}.json",
description: "Acme components",
logo: "https://acme.example.com/logo.svg",
},
]
function createEntryState(overrides: Partial<RegistryMonitorEntryState> = {}) {
return {
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-24T10:00:00.000Z",
status: "healthy",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
itemCursor: 0,
itemNames: [],
recentIndex: [],
recentDryRuns: [],
daily: [],
latestHygiene: {
contentTypeJson: null,
noDuplicateNames: null,
nameMatches: null,
},
...overrides,
} satisfies RegistryMonitorEntryState
}
function createState(entry = createEntryState()) {
return {
schemaVersion: 1,
scoreVersion: 1,
dryRunVersion: REGISTRY_DRY_RUN_VERSION,
updatedAt: "2026-08-24T10:00:00.000Z",
registries: { "@acme": entry },
} satisfies RegistryMonitorState
}
function createSuccessfulFetch(itemCount = 1) {
return {
ok: true as const,
json: {
name: "acme",
homepage: "https://acme.example.com",
items: Array.from({ length: itemCount }, (_, index) => ({
name: `item-${index}`,
type: "registry:ui",
files: [],
})),
},
status: 200,
durationMs: 120,
responseSize: 500,
contentType: "application/json; charset=utf-8",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
}
}
describe("runRegistryMonitor", () => {
it("records completion separately from the observation time", async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2026-08-24T12:05:00.000Z"))
try {
const result = await runRegistryMonitor({
directory: DIRECTORY,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
expect(result.run.startedAt).toBe(NOW.toISOString())
expect(result.run.completedAt).toBe("2026-08-24T12:05:00.000Z")
} finally {
vi.useRealTimers()
}
})
it("records a successful hourly index observation", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch(2)),
runDryRun: vi.fn(),
})
expect(result.run.totals).toMatchObject({
registries: 1,
reachable: 1,
unavailable: 0,
challenges: 0,
})
expect(result.state.registries["@acme"].itemNames).toEqual([
"item-0",
"item-1",
])
expect(result.snapshot.registries["@acme"]).toMatchObject({
status: "observing",
monitoringLimited: false,
checkedAt: NOW.toISOString(),
})
})
it("starts an outage from the last successful index check", async () => {
const lastSuccessfulCheck = "2026-08-23T11:00:00.000Z"
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck,
consecutiveSuccessfulIndexes: 24,
})
),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "timeout",
reachable: false,
botChallenge: false,
durationMs: 100,
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"]).toMatchObject({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 1,
availabilityOutageSince: lastSuccessfulCheck,
availabilityRecoveryRequired: true,
})
expect(result.snapshot.registries["@acme"].status).toBe("unavailable")
})
it("clears availability recovery after two valid index checks", async () => {
const outageSince = "2026-08-16T00:00:00.000Z"
const recentIndex = Array.from({ length: 24 }, (_, index) => ({
checkedAt: new Date(
NOW.getTime() - (24 - index) * 60 * 60 * 1000
).toISOString(),
outcome: "unreachable" as const,
durationMs: 100,
redirectCount: 0,
}))
const previousState = createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-15T23:00:00.000Z",
status: "unavailable",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 24,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: true,
recentIndex,
})
)
const first = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
const recovering = first.state.registries["@acme"]
expect(recovering).toMatchObject({
consecutiveSuccessfulIndexes: 1,
consecutiveIndexFailures: 0,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: true,
})
expect(first.snapshot.registries["@acme"]).toMatchObject({
status: "unavailable",
hidden: true,
})
const second = await runRegistryMonitor({
directory: DIRECTORY,
previousState: first.state,
mode: "hourly",
now: new Date("2026-08-24T13:00:00.000Z"),
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
const recovered = second.state.registries["@acme"]
expect(recovered).toMatchObject({
consecutiveSuccessfulIndexes: 2,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
})
expect(recovered).not.toHaveProperty("availabilityOutageSince")
expect(second.snapshot.registries["@acme"].status).toBe("healthy")
})
it("records a challenge without lowering availability", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
const bucket = result.state.registries["@acme"].daily[0]
const state = result.state.registries["@acme"]
expect(bucket.challengeObservations).toBe(1)
expect(bucket.availabilityObservations).toBe(0)
expect(state).toMatchObject({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
})
expect(state).not.toHaveProperty("availabilityOutageSince")
expect(result.snapshot.registries["@acme"].monitoringLimited).toBe(true)
})
it("allows an existing outage to become unavailable during a challenge", async () => {
const outageSince = "2026-08-23T11:00:00.000Z"
const previousState = createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: outageSince,
consecutiveIndexFailures: 1,
availabilityOutageSince: outageSince,
recentIndex: [
{
checkedAt: outageSince,
outcome: "unreachable",
durationMs: 100,
redirectCount: 0,
},
],
})
)
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"]).toMatchObject({
consecutiveIndexFailures: 1,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: false,
})
expect(result.snapshot.registries["@acme"]).toMatchObject({
status: "unavailable",
monitoringLimited: true,
})
})
it("refreshes hygiene after a reachable invalid index", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
latestHygiene: {
contentTypeJson: true,
noDuplicateNames: true,
nameMatches: true,
},
})
),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
...createSuccessfulFetch(),
json: { invalid: true },
contentType: "text/plain",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"].latestHygiene).toEqual({
contentTypeJson: false,
noDuplicateNames: null,
nameMatches: null,
})
})
it("rotates enough daily items to cover a catalog in thirty days", async () => {
const itemNames = Array.from({ length: 61 }, (_, index) => `item-${index}`)
const fetchJson = vi.fn(async (url: string) => {
const item = url.match(/item-\d+/)?.[0] ?? "item-0"
return {
...createSuccessfulFetch(),
json: { name: item, type: "registry:ui", files: [] },
finalUrl: url,
}
})
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(createEntryState({ itemNames })),
mode: "daily",
now: NOW,
fetchJson,
runDryRun: vi.fn(),
})
expect(fetchJson).toHaveBeenCalledTimes(3)
expect(result.state.registries["@acme"].itemCursor).toBe(3)
expect(result.run.totals.itemChecks).toBe(3)
expect(result.run.results["@acme"].items).toHaveLength(3)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
})
it("excludes item challenges from validation rates", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
itemNames: ["button"],
daily: [
{
date: "2026-08-24",
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 8,
itemObservations: 9,
dryRunSuccesses: 0,
dryRunObservations: 0,
},
],
})
),
mode: "daily",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/button.json",
})),
runDryRun: vi.fn(),
})
const bucket = result.state.registries["@acme"].daily[0]
expect(bucket.itemObservations).toBe(9)
expect(bucket.itemSuccesses).toBe(8)
expect(result.run.totals).toMatchObject({
challenges: 1,
itemChecks: 1,
})
})
it("runs one rotating weekly dry-run item", async () => {
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({ itemNames: ["button", "dialog"] })
),
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(runDryRun).toHaveBeenCalledWith({
namespace: "@acme",
item: "button",
registryUrl: DIRECTORY[0].url,
})
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.registries["@acme"].recentDryRuns).toHaveLength(1)
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("discards invalid pre-tsconfig dry runs and reruns the weekly check", async () => {
const now = new Date("2026-09-02T12:00:00.000Z")
const recentIndex = Array.from({ length: 24 }, (_, index) => ({
checkedAt: new Date(
now.getTime() - (24 - index) * 60 * 60 * 1000
).toISOString(),
outcome: "reachable" as const,
status: 200,
durationMs: 100,
redirectCount: 0,
schemaValid: true,
}))
const previousState = {
...createState(
createEntryState({
itemNames: ["button"],
recentIndex,
recentDryRuns: [
{
checkedAt: "2026-08-25T12:04:27.443Z",
item: "button",
success: false,
failureCode: "exit_1",
durationMs: 1500,
},
{
checkedAt: "2026-08-31T12:38:36.084Z",
item: "dialog",
success: false,
failureCode: "exit_1",
durationMs: 1700,
},
],
daily: [
{
date: "2026-08-25",
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 0,
itemObservations: 0,
dryRunSuccesses: 0,
dryRunObservations: 1,
},
{
date: "2026-08-31",
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 0,
itemObservations: 0,
dryRunSuccesses: 0,
dryRunObservations: 1,
},
],
latestHygiene: {
contentTypeJson: true,
noDuplicateNames: true,
nameMatches: true,
},
})
),
dryRunVersion: 0,
lastDailyRunAt: "2026-09-02T11:00:00.000Z",
lastWeeklyRunAt: "2026-08-31T12:38:36.084Z",
}
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun,
})
expect(runDryRun).toHaveBeenCalledOnce()
expect(result.state.registries["@acme"].recentDryRuns).toEqual([
{
checkedAt: now.toISOString(),
item: "item-0",
success: true,
durationMs: 250,
},
])
expect(result.state.registries["@acme"].daily.slice(0, 2)).toMatchObject([
{ date: "2026-08-25", dryRunSuccesses: 0, dryRunObservations: 0 },
{ date: "2026-08-31", dryRunSuccesses: 0, dryRunObservations: 0 },
])
expect(result.state.lastWeeklyRunAt).toBe(now.toISOString())
expect(result.run.diagnostics).toEqual([
"Discarded 2 invalid CLI dry-run observations from the pre-tsconfig scaffold.",
])
expect(result.snapshot.registries["@acme"]).toMatchObject({
status: "healthy",
breakdown: { installability: 18.5 },
})
})
it("continues weekly rotation after dry-run history is trimmed", async () => {
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const recentDryRuns = Array.from({ length: 12 }, (_, index) => ({
checkedAt: `2026-08-${String(index + 1).padStart(2, "0")}T12:00:00.000Z`,
item: index === 11 ? "dialog" : "button",
success: true,
durationMs: 250,
}))
await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
itemNames: ["button", "dialog", "tooltip"],
recentDryRuns,
})
),
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(runDryRun).toHaveBeenCalledWith({
namespace: "@acme",
item: "tooltip",
registryUrl: DIRECTORY[0].url,
})
})
it("runs delayed auto checks using their last run timestamps", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-23T15:00:00.000Z",
lastWeeklyRunAt: "2026-08-18T12:00:00.000Z",
}
const fetchJson = vi.fn(async (url: string) =>
url.endsWith("/registry.json")
? createSuccessfulFetch()
: {
...createSuccessfulFetch(),
json: { name: "item-0", type: "registry:ui", files: [] },
finalUrl: url,
}
)
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson,
runDryRun,
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("skips auto checks until their elapsed intervals", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-23T17:00:00.000Z",
lastWeeklyRunAt: "2026-08-19T12:00:00.000Z",
}
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
expect(result.run.totals.itemChecks).toBe(0)
expect(result.run.totals.dryRuns).toBe(0)
expect(result.state.lastDailyRunAt).toBe(previousState.lastDailyRunAt)
expect(result.state.lastWeeklyRunAt).toBe(previousState.lastWeeklyRunAt)
})
it("runs auto checks for state without last run timestamps", async () => {
const previousState = createState(
createEntryState({ itemNames: ["button"] })
)
const fetchJson = vi.fn(async (url: string) =>
url.endsWith("/registry.json")
? createSuccessfulFetch()
: {
...createSuccessfulFetch(),
json: { name: "item-0", type: "registry:ui", files: [] },
finalUrl: url,
}
)
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson,
runDryRun: vi.fn(async () => ({
success: true,
durationMs: 250,
})),
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("runs auto checks when last run timestamps are in the future", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-25T12:00:00.000Z",
lastWeeklyRunAt: "2026-08-25T12:00:00.000Z",
}
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(async () => ({
success: true,
durationMs: 250,
})),
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("limits concurrent CLI dry runs to four", async () => {
const directory = Array.from({ length: 6 }, (_, index) => ({
name: `@registry${index}`,
homepage: `https://registry${index}.example.com`,
url: `https://registry${index}.example.com/r/{name}.json`,
description: `Registry ${index}`,
logo: `https://registry${index}.example.com/logo.svg`,
}))
const previousState: RegistryMonitorState = {
schemaVersion: 1,
scoreVersion: 1,
updatedAt: "2026-08-24T10:00:00.000Z",
registries: Object.fromEntries(
directory.map((entry) => [
entry.name,
createEntryState({ itemNames: ["button"] }),
])
),
}
let active = 0
let maximumActive = 0
const runDryRun = vi.fn(async () => {
active += 1
maximumActive = Math.max(maximumActive, active)
await new Promise((resolve) => setTimeout(resolve, 20))
active -= 1
return { success: true, durationMs: 20 }
})
const result = await runRegistryMonitor({
directory,
previousState,
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(result.run.totals.dryRuns).toBe(6)
expect(maximumActive).toBe(4)
})
it("resolves {style} urls with the default style", async () => {
const fetchJson = vi.fn(async () => createSuccessfulFetch())
await runRegistryMonitor({
directory: [
{
...DIRECTORY[0],
url: "https://acme.example.com/r/{style}/{name}.json",
},
],
mode: "daily",
now: NOW,
previousState: createState(createEntryState({ itemNames: ["button"] })),
fetchJson,
runDryRun: vi.fn(),
})
expect(fetchJson).toHaveBeenCalledWith(
"https://acme.example.com/r/radix-vega/button.json"
)
})
})

View File

@@ -0,0 +1,615 @@
import { registryItemSchema, registrySchema } from "shadcn/schema"
import {
normalizeRegistryName,
type RegistryDirectoryEntry,
} from "../registry-directory"
import { fetchRegistryJson, type RegistryJsonResult } from "./network"
import {
REGISTRY_DRY_RUN_VERSION,
REGISTRY_HEALTH_SCHEMA_VERSION,
REGISTRY_HEALTH_SCORE_VERSION,
registryHealthSnapshotSchema,
registryMonitorRunSchema,
registryMonitorStateSchema,
type RegistryDryRunObservation,
type RegistryHealthDailyBucket,
type RegistryHealthSnapshot,
type RegistryIndexObservation,
type RegistryItemObservation,
type RegistryMonitorEntryState,
type RegistryMonitorOutput,
type RegistryMonitorRun,
type RegistryMonitorState,
} from "./schema"
import { calculateGlobalMeans, calculateRegistryHealth } from "./score"
import { createRegistryMonitorEntryState } from "./state"
const DAY_MS = 24 * 60 * 60 * 1000
const RECENT_INDEX_RETENTION_MS = 8 * DAY_MS
const DAILY_CHECK_INTERVAL_MS = 20 * 60 * 60 * 1000
const WEEKLY_CHECK_INTERVAL_MS = 6 * DAY_MS
const MAX_DRY_RUN_CONCURRENCY = 4
// Registries cannot declare a default style yet, so resolve {style} urls
// with the canonical first base and style combination.
export const DEFAULT_STYLE = "radix-vega"
export type RegistryMonitorMode = RegistryMonitorRun["mode"]
export type RegistryDryRunResult = {
success: boolean
failureCode?: string
failureMessage?: string
durationMs: number
}
function createDailyBucket(date: string) {
return {
date,
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 0,
itemObservations: 0,
dryRunSuccesses: 0,
dryRunObservations: 0,
} satisfies RegistryHealthDailyBucket
}
function discardInvalidDryRunHistory(
previousState: RegistryMonitorState | null | undefined
) {
if (!previousState) {
return { state: previousState, discardedDryRuns: 0 }
}
const state = structuredClone(previousState)
if (state.dryRunVersion === REGISTRY_DRY_RUN_VERSION) {
return { state, discardedDryRuns: 0 }
}
let discardedRecentDryRuns = 0
let discardedDailyDryRuns = 0
for (const entry of Object.values(state.registries)) {
discardedRecentDryRuns += entry.recentDryRuns.length
entry.recentDryRuns = []
for (const bucket of entry.daily) {
discardedDailyDryRuns += bucket.dryRunObservations
bucket.dryRunSuccesses = 0
bucket.dryRunObservations = 0
}
}
const discardedDryRuns = Math.max(
discardedRecentDryRuns,
discardedDailyDryRuns
)
state.dryRunVersion = REGISTRY_DRY_RUN_VERSION
delete state.lastWeeklyRunAt
return { state, discardedDryRuns }
}
function getDailyBucket(state: RegistryMonitorEntryState, now: Date) {
const date = now.toISOString().slice(0, 10)
let bucket = state.daily.find((entry) => entry.date === date)
if (!bucket) {
bucket = createDailyBucket(date)
state.daily.push(bucket)
}
return bucket
}
function trimState(state: RegistryMonitorEntryState, now: Date) {
state.recentIndex = state.recentIndex
.filter(
(observation) =>
now.getTime() - new Date(observation.checkedAt).getTime() <
RECENT_INDEX_RETENTION_MS
)
.slice(-256)
state.recentDryRuns = state.recentDryRuns.slice(-12)
state.daily = state.daily
.filter(
(bucket) =>
now.getTime() - new Date(`${bucket.date}T00:00:00.000Z`).getTime() <
31 * DAY_MS
)
.toSorted((a, b) => a.date.localeCompare(b.date))
}
function isJsonMediaType(contentType: string) {
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase()
return (
mediaType === "application/json" ||
mediaType === "text/json" ||
mediaType.endsWith("+json")
)
}
function getIndexObservation(
result: RegistryJsonResult,
entry: RegistryDirectoryEntry,
now: Date
) {
if (!result.ok) {
return {
observation: {
checkedAt: now.toISOString(),
outcome: result.botChallenge
? ("bot_challenge" as const)
: result.reachable
? ("reachable" as const)
: ("unreachable" as const),
status: result.status,
failureCode: result.failureCode,
durationMs: result.durationMs,
responseSize: result.responseSize,
redirectCount: result.redirectCount,
schemaValid: result.reachable ? false : undefined,
contentTypeJson: result.contentType
? isJsonMediaType(result.contentType)
: undefined,
} satisfies RegistryIndexObservation,
itemNames: null,
}
}
const parsed = registrySchema.safeParse(result.json)
const itemNames = parsed.success
? parsed.data.items.map((item) => item.name)
: null
const duplicateNames = itemNames
? new Set(itemNames).size !== itemNames.length
: undefined
const nameMatches = parsed.success
? normalizeRegistryName(parsed.data.name) ===
normalizeRegistryName(entry.name)
: undefined
return {
observation: {
checkedAt: now.toISOString(),
outcome: "reachable" as const,
status: result.status,
failureCode: parsed.success ? undefined : "invalid_schema",
durationMs: result.durationMs,
responseSize: result.responseSize,
redirectCount: result.redirectCount,
schemaValid: parsed.success,
contentTypeJson: isJsonMediaType(result.contentType),
duplicateNames,
nameMatches,
itemCount: itemNames?.length,
} satisfies RegistryIndexObservation,
itemNames,
}
}
function recordIndexObservation(
state: RegistryMonitorEntryState,
observation: RegistryIndexObservation,
itemNames: string[] | null,
now: Date
) {
state.recentIndex.push(observation)
const bucket = getDailyBucket(state, now)
if (observation.outcome !== "bot_challenge") {
if (observation.outcome === "unreachable") {
state.consecutiveSuccessfulIndexes = 0
state.consecutiveIndexFailures += 1
state.availabilityOutageSince ??=
state.lastSuccessfulCheck ?? observation.checkedAt
const outageDuration =
new Date(observation.checkedAt).getTime() -
new Date(state.availabilityOutageSince).getTime()
const outageRequiresRecovery = outageDuration >= DAY_MS
if (state.consecutiveIndexFailures >= 3 || outageRequiresRecovery) {
state.availabilityRecoveryRequired = true
}
} else {
const outageDuration = state.availabilityOutageSince
? new Date(observation.checkedAt).getTime() -
new Date(state.availabilityOutageSince).getTime()
: 0
const outageRequiresRecovery = outageDuration >= DAY_MS
state.consecutiveIndexFailures = 0
if (observation.schemaValid) {
state.consecutiveSuccessfulIndexes += 1
if (
(state.availabilityRecoveryRequired || outageRequiresRecovery) &&
state.consecutiveSuccessfulIndexes < 2
) {
state.availabilityRecoveryRequired = true
} else {
state.availabilityRecoveryRequired = false
delete state.availabilityOutageSince
}
} else {
state.consecutiveSuccessfulIndexes = 0
if (state.availabilityRecoveryRequired || outageRequiresRecovery) {
state.availabilityRecoveryRequired = true
} else {
delete state.availabilityOutageSince
}
}
}
}
if (observation.outcome === "bot_challenge") {
bucket.challengeObservations += 1
} else {
bucket.availabilityObservations += 1
bucket.availabilitySuccesses += observation.outcome === "reachable" ? 1 : 0
}
if (observation.outcome === "reachable") {
bucket.schemaObservations += 1
bucket.schemaSuccesses += observation.schemaValid ? 1 : 0
}
if (observation.outcome === "reachable") {
state.latestHygiene = {
contentTypeJson: observation.contentTypeJson ?? false,
noDuplicateNames:
observation.duplicateNames === undefined
? null
: !observation.duplicateNames,
nameMatches: observation.nameMatches ?? null,
}
}
if (observation.schemaValid && itemNames) {
state.itemNames = itemNames
state.lastSuccessfulCheck = observation.checkedAt
}
}
function getItemNamesForDailyCheck(state: RegistryMonitorEntryState) {
if (state.itemNames.length === 0) return []
const count = Math.max(1, Math.ceil(state.itemNames.length / 30))
const selected = Array.from({ length: count }, (_, offset) => {
const index = (state.itemCursor + offset) % state.itemNames.length
return state.itemNames[index]
})
state.itemCursor = (state.itemCursor + count) % state.itemNames.length
return selected
}
function recordItemObservation(
state: RegistryMonitorEntryState,
observation: RegistryItemObservation,
now: Date
) {
if (observation.failureCode === "bot_challenge") return
const bucket = getDailyBucket(state, now)
bucket.itemObservations += 1
bucket.itemSuccesses += observation.success ? 1 : 0
}
function getNextDryRunItem(state: RegistryMonitorEntryState) {
if (state.itemNames.length === 0) return null
const previousItem = state.recentDryRuns.at(-1)?.item
const previousIndex = previousItem
? state.itemNames.indexOf(previousItem)
: -1
return state.itemNames[(previousIndex + 1) % state.itemNames.length]
}
function recordDryRunObservation(
state: RegistryMonitorEntryState,
observation: RegistryDryRunObservation,
now: Date
) {
state.recentDryRuns.push(observation)
const bucket = getDailyBucket(state, now)
bucket.dryRunObservations += 1
bucket.dryRunSuccesses += observation.success ? 1 : 0
}
function replaceRegistryItem(url: string, item: string) {
return url.replace("{name}", item).replace("{style}", DEFAULT_STYLE)
}
function hasIntervalElapsed(
lastRunAt: string | undefined,
now: Date,
intervalMs: number
) {
if (!lastRunAt) return true
const elapsed = now.getTime() - new Date(lastRunAt).getTime()
return elapsed < 0 || elapsed >= intervalMs
}
function getChecks(
mode: RegistryMonitorMode,
now: Date,
previousState?: RegistryMonitorState | null
) {
if (mode === "all") {
return { index: true, items: true, dryRun: true }
}
if (mode !== "auto") {
return {
index: mode === "hourly",
items: mode === "daily",
dryRun: mode === "weekly",
}
}
return {
index: true,
items: hasIntervalElapsed(
previousState?.lastDailyRunAt,
now,
DAILY_CHECK_INTERVAL_MS
),
dryRun: hasIntervalElapsed(
previousState?.lastWeeklyRunAt,
now,
WEEKLY_CHECK_INTERVAL_MS
),
}
}
function createConcurrencyLimit(concurrency: number) {
let active = 0
const waiting: Array<() => void> = []
return async function limit<T>(task: () => Promise<T>) {
while (active >= concurrency) {
await new Promise<void>((resolve) => waiting.push(resolve))
}
active += 1
try {
return await task()
} finally {
active -= 1
waiting.shift()?.()
}
}
}
async function mapWithConcurrency<T>(
values: T[],
concurrency: number,
task: (value: T) => Promise<void>
) {
let cursor = 0
async function worker() {
while (cursor < values.length) {
const index = cursor
cursor += 1
await task(values[index])
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, values.length) }, () => worker())
)
}
function groupByHost(directory: RegistryDirectoryEntry[]) {
const groups = new Map<string, RegistryDirectoryEntry[]>()
for (const entry of directory) {
let host = entry.name
try {
host = new URL(entry.url).host
} catch {
host = entry.name
}
const group = groups.get(host) ?? []
group.push(entry)
groups.set(host, group)
}
return [...groups.values()]
}
export async function runRegistryMonitor({
directory,
previousState,
mode = "auto",
now = new Date(),
fetchJson = fetchRegistryJson,
runDryRun,
concurrency = 12,
}: {
directory: RegistryDirectoryEntry[]
previousState?: RegistryMonitorState | null
mode?: RegistryMonitorMode
now?: Date
fetchJson?: typeof fetchRegistryJson
runDryRun: (options: {
namespace: string
item: string
registryUrl: string
}) => Promise<RegistryDryRunResult>
concurrency?: number
}): Promise<RegistryMonitorOutput> {
const startedAt = now.toISOString()
const { state: repairedPreviousState, discardedDryRuns } =
discardInvalidDryRunHistory(previousState)
const checks = getChecks(mode, now, repairedPreviousState)
const state: RegistryMonitorState = {
schemaVersion: REGISTRY_HEALTH_SCHEMA_VERSION,
scoreVersion: REGISTRY_HEALTH_SCORE_VERSION,
dryRunVersion: REGISTRY_DRY_RUN_VERSION,
updatedAt: now.toISOString(),
...(repairedPreviousState?.lastDailyRunAt
? { lastDailyRunAt: repairedPreviousState.lastDailyRunAt }
: {}),
...(repairedPreviousState?.lastWeeklyRunAt
? { lastWeeklyRunAt: repairedPreviousState.lastWeeklyRunAt }
: {}),
registries: {},
}
const limitDryRun = createConcurrencyLimit(MAX_DRY_RUN_CONCURRENCY)
const runResults: RegistryMonitorRun["results"] = {}
const totals = {
registries: directory.length,
reachable: 0,
unavailable: 0,
challenges: 0,
itemChecks: 0,
dryRuns: 0,
}
for (const entry of directory) {
state.registries[entry.name] =
repairedPreviousState?.registries[entry.name] ??
createRegistryMonitorEntryState(now)
runResults[entry.name] = { items: [] }
}
await mapWithConcurrency(
groupByHost(directory),
concurrency,
async (group) => {
for (const entry of group) {
const entryState = state.registries[entry.name]
const result = runResults[entry.name]
if (checks.index) {
const indexResult = await fetchJson(
replaceRegistryItem(entry.url, "registry")
)
const { observation, itemNames } = getIndexObservation(
indexResult,
entry,
now
)
recordIndexObservation(entryState, observation, itemNames, now)
result.index = observation
if (observation.outcome === "reachable") totals.reachable += 1
if (observation.outcome === "unreachable") totals.unavailable += 1
if (observation.outcome === "bot_challenge") totals.challenges += 1
}
if (checks.items) {
for (const item of getItemNamesForDailyCheck(entryState)) {
const itemResult = await fetchJson(
replaceRegistryItem(entry.url, item)
)
const parsed = itemResult.ok
? registryItemSchema.safeParse(itemResult.json)
: null
const success = !!(parsed?.success && parsed.data.name === item)
const observation: RegistryItemObservation = {
checkedAt: now.toISOString(),
item,
success,
failureCode: success
? undefined
: itemResult.ok
? parsed?.success
? "name_mismatch"
: "invalid_schema"
: itemResult.failureCode,
durationMs: itemResult.durationMs,
}
recordItemObservation(entryState, observation, now)
result.items.push(observation)
totals.itemChecks += 1
if (!itemResult.ok && itemResult.botChallenge) {
totals.challenges += 1
}
}
}
const dryRunItem = checks.dryRun ? getNextDryRunItem(entryState) : null
if (dryRunItem) {
const dryRunResult = await limitDryRun(() =>
runDryRun({
namespace: entry.name,
item: dryRunItem,
registryUrl: entry.url,
})
)
const observation: RegistryDryRunObservation = {
checkedAt: now.toISOString(),
item: dryRunItem,
...dryRunResult,
}
recordDryRunObservation(entryState, observation, now)
result.dryRun = observation
totals.dryRuns += 1
}
trimState(entryState, now)
}
}
)
if (checks.items) {
state.lastDailyRunAt = now.toISOString()
}
if (checks.dryRun) {
state.lastWeeklyRunAt = now.toISOString()
}
const registryUrls = Object.fromEntries(
directory.map((entry) => [entry.name, entry.url])
)
const globalMeans = calculateGlobalMeans(state, registryUrls, now)
const snapshot: RegistryHealthSnapshot = {
schemaVersion: REGISTRY_HEALTH_SCHEMA_VERSION,
scoreVersion: REGISTRY_HEALTH_SCORE_VERSION,
generatedAt: now.toISOString(),
globalMeans,
registries: {},
}
for (const entry of directory) {
const entryState = state.registries[entry.name]
const health = calculateRegistryHealth({
state: entryState,
registryUrl: entry.url,
globalMeans,
now,
})
entryState.status = health.status
snapshot.registries[entry.name] = health
}
const run: RegistryMonitorRun = {
schemaVersion: REGISTRY_HEALTH_SCHEMA_VERSION,
startedAt,
completedAt: new Date().toISOString(),
mode,
totals,
results: runResults,
diagnostics:
discardedDryRuns > 0
? [
`Discarded ${discardedDryRuns} invalid CLI dry-run observations from the pre-tsconfig scaffold.`,
]
: [],
}
return {
state: registryMonitorStateSchema.parse(state),
snapshot: registryHealthSnapshotSchema.parse(snapshot),
run: registryMonitorRunSchema.parse(run),
}
}

View File

@@ -0,0 +1,242 @@
import { describe, expect, it, vi } from "vitest"
import {
createGuardedConnector,
detectBotChallenge,
fetchRegistryJson,
isForbiddenAddress,
validateRegistryUrl,
} from "./network"
describe("network destination safety", () => {
it.each([
"127.0.0.1",
"10.0.0.1",
"168.63.129.16",
"169.254.169.254",
"192.0.2.1",
"192.88.99.1",
"192.168.1.1",
"::1",
"64:ff9b::7f00:1",
"100::",
"2001::1",
"2002:7f00:1::",
"fe80::1",
"fec0::1",
"fd00:ec2::254",
"::ffff:127.0.0.1",
])("rejects non-public address %s", (address) => {
expect(isForbiddenAddress(address)).toBe(true)
})
it.each(["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"])(
"allows public address %s",
(address) => {
expect(isForbiddenAddress(address)).toBe(false)
}
)
it("rejects unsupported protocols and embedded credentials", () => {
expect(() => validateRegistryUrl("file:///etc/passwd")).toThrow(
"unsupported_protocol"
)
expect(() =>
validateRegistryUrl("https://user:secret@example.com/registry.json")
).toThrow("embedded_credentials")
})
it("pins the validated address used by the socket connector", async () => {
const connect = vi.fn((_options, callback) => callback(null, {}))
const connector = createGuardedConnector({
resolve: async () => [{ address: "203.0.114.10", family: 4 }],
connect,
})
await new Promise<void>((resolve, reject) => {
connector(
{
hostname: "registry.example.com",
protocol: "https:",
port: "443",
},
(error, _socket) => (error ? reject(error) : resolve())
)
})
expect(connect).toHaveBeenCalledWith(
expect.objectContaining({
hostname: "203.0.114.10",
servername: "registry.example.com",
}),
expect.any(Function)
)
})
it("rejects a hostname when DNS includes a private address", async () => {
const connector = createGuardedConnector({
resolve: async () => [
{ address: "203.0.114.10", family: 4 },
{ address: "127.0.0.1", family: 4 },
],
connect: vi.fn(),
})
await expect(
new Promise<void>((resolve, reject) => {
connector(
{
hostname: "registry.example.com",
protocol: "https:",
port: "443",
},
(error, _socket) => (error ? reject(error) : resolve())
)
})
).rejects.toThrow("forbidden_address")
})
})
describe("fetchRegistryJson", () => {
const dispatcher = {
close: vi.fn(async () => {}),
}
it("validates every manual redirect target", async () => {
const fetchImpl = vi.fn(
async () =>
new Response(null, {
status: 302,
headers: { location: "http://127.0.0.1/registry.json" },
})
)
await expect(
fetchRegistryJson("https://example.com/registry.json", {
fetchImpl,
createDispatcher: () => dispatcher as never,
attempts: 1,
})
).resolves.toMatchObject({
ok: false,
failureCode: "forbidden_address",
})
})
it("shares one timeout budget across redirects", async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2026-08-24T12:00:00.000Z"))
try {
const fetchImpl = vi
.fn()
.mockImplementationOnce(async () => {
vi.setSystemTime(new Date("2026-08-24T12:00:00.080Z"))
return new Response(null, {
status: 302,
headers: { location: "https://example.com/final.json" },
})
})
.mockImplementationOnce(
async (_url: string, init: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init.signal?.addEventListener("abort", () => {
reject(new DOMException("Aborted", "AbortError"))
})
})
)
const resultPromise = fetchRegistryJson(
"https://example.com/registry.json",
{
fetchImpl,
createDispatcher: () => dispatcher as never,
attempts: 1,
timeoutMs: 100,
}
)
await vi.advanceTimersByTimeAsync(20)
await expect(resultPromise).resolves.toMatchObject({
ok: false,
failureCode: "timeout",
})
expect(fetchImpl).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
})
it("retries a retryable response and honors Retry-After", async () => {
const wait = vi.fn(async () => {})
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
new Response("busy", {
status: 429,
headers: { "retry-after": "1" },
})
)
.mockResolvedValueOnce(
new Response('{"name":"acme"}', {
status: 200,
headers: { "content-type": "application/json" },
})
)
const result = await fetchRegistryJson(
"https://example.com/registry.json",
{
fetchImpl,
createDispatcher: () => dispatcher as never,
wait,
}
)
expect(result.ok).toBe(true)
expect(fetchImpl).toHaveBeenCalledTimes(2)
expect(wait).toHaveBeenCalledWith(1000)
})
it("enforces the response-size limit", async () => {
const result = await fetchRegistryJson(
"https://example.com/registry.json",
{
fetchImpl: async () => new Response("too large"),
createDispatcher: () => dispatcher as never,
maximumBytes: 3,
attempts: 1,
}
)
expect(result).toMatchObject({
ok: false,
failureCode: "body_too_large",
})
})
})
describe("detectBotChallenge", () => {
it("recognizes a provider challenge page", () => {
expect(
detectBotChallenge({
status: 403,
contentType: "text/html",
server: "cloudflare",
body: "<title>Just a moment...</title><div class='cf-chl-test'>",
})
).toBe(true)
})
it("does not classify an ordinary JSON 403 as a challenge", () => {
expect(
detectBotChallenge({
status: 403,
contentType: "application/json",
server: "",
body: '{"error":"forbidden"}',
})
).toBe(false)
})
})

View File

@@ -0,0 +1,586 @@
import { lookup } from "node:dns/promises"
import { isIP } from "node:net"
import {
Agent,
buildConnector,
fetch as undiciFetch,
type Dispatcher,
} from "undici"
const DEFAULT_TIMEOUT_MS = 10_000
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
const DEFAULT_MAX_REDIRECTS = 5
const MAX_RETRY_AFTER_MS = 30_000
type ResolvedAddress = {
address: string
family: number
}
type ResolveHostname = (hostname: string) => Promise<ResolvedAddress[]>
type Connector = ReturnType<typeof buildConnector>
type FetchLike = (
url: string,
init: RequestInit & { dispatcher?: Dispatcher }
) => Promise<Response>
export type RegistryJsonResult =
| {
ok: true
json: unknown
status: number
durationMs: number
responseSize: number
contentType: string
redirectCount: number
finalUrl: string
}
| {
ok: false
failureCode: string
reachable: boolean
botChallenge: boolean
status?: number
durationMs: number
responseSize?: number
contentType?: string
redirectCount: number
finalUrl: string
}
type RegistryJsonAttemptResult =
| Extract<RegistryJsonResult, { ok: true }>
| (Extract<RegistryJsonResult, { ok: false }> & {
retryAfterMs?: number
})
function parseIpv4(address: string) {
if (isIP(address) !== 4) return null
return address.split(".").map(Number)
}
function parseIpv6(address: string) {
if (address.includes("%") || isIP(address) !== 6) return null
let value = address.toLowerCase()
const ipv4Match = value.match(/(\d+\.\d+\.\d+\.\d+)$/)
if (ipv4Match) {
const ipv4 = parseIpv4(ipv4Match[1])
if (!ipv4) return null
value = value.replace(
ipv4Match[1],
`${((ipv4[0] << 8) | ipv4[1]).toString(16)}:${(
(ipv4[2] << 8) |
ipv4[3]
).toString(16)}`
)
}
const [left = "", right = ""] = value.split("::")
const leftParts = left ? left.split(":") : []
const rightParts = right ? right.split(":") : []
const missing = 8 - leftParts.length - rightParts.length
const parts = value.includes("::")
? [
...leftParts,
...Array.from({ length: missing }, () => "0"),
...rightParts,
]
: leftParts
if (parts.length !== 8) return null
const parsed = parts.map((part) => Number.parseInt(part || "0", 16))
return parsed.every((part) => Number.isInteger(part) && part <= 0xffff)
? parsed
: null
}
function isForbiddenIpv4(address: string) {
const parts = parseIpv4(address)
if (!parts) return true
const [a, b, c] = parts
return (
a === 0 ||
a === 10 ||
a === 127 ||
(a === 100 && b >= 64 && b <= 127) ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 0 && c === 0) ||
(a === 192 && b === 0 && c === 2) ||
(a === 192 && b === 88 && c === 99) ||
(a === 192 && b === 168) ||
(a === 198 && (b === 18 || b === 19)) ||
(a === 198 && b === 51 && c === 100) ||
(a === 203 && b === 0 && c === 113) ||
(a === 168 && b === 63 && c === 129 && parts[3] === 16) ||
a >= 224
)
}
function isForbiddenIpv6(address: string) {
const parts = parseIpv6(address)
if (!parts) return true
const isUnspecified = parts.every((part) => part === 0)
const isLoopback =
parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1
const isUniqueLocal = (parts[0] & 0xfe00) === 0xfc00
const isLinkLocal = (parts[0] & 0xffc0) === 0xfe80
const isSiteLocal = (parts[0] & 0xffc0) === 0xfec0
const isMulticast = (parts[0] & 0xff00) === 0xff00
const isDocumentation = parts[0] === 0x2001 && parts[1] === 0x0db8
const isProtocolAssignment =
parts[0] === 0x2001 && parts[1] >= 0 && parts[1] <= 0x01ff
const isSixToFour = parts[0] === 0x2002
const isDiscardOnly =
parts[0] === 0x0100 && parts.slice(1, 4).every((part) => part === 0)
const isNat64 =
(parts[0] === 0x0064 &&
parts[1] === 0xff9b &&
parts.slice(2, 6).every((part) => part === 0)) ||
(parts[0] === 0x0064 && parts[1] === 0xff9b && parts[2] === 0x0001)
const isIpv4Mapped =
parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff
const isIpv4Compatible = parts.slice(0, 6).every((part) => part === 0)
if (isIpv4Mapped) {
return isForbiddenIpv4(
`${parts[6] >> 8}.${parts[6] & 255}.${parts[7] >> 8}.${parts[7] & 255}`
)
}
return (
isUnspecified ||
isLoopback ||
isUniqueLocal ||
isLinkLocal ||
isSiteLocal ||
isMulticast ||
isDocumentation ||
isProtocolAssignment ||
isSixToFour ||
isDiscardOnly ||
isNat64 ||
isIpv4Compatible
)
}
export function isForbiddenAddress(address: string) {
const normalized = address.replace(/^\[|\]$/g, "")
const family = isIP(normalized)
if (family === 4) return isForbiddenIpv4(normalized)
if (family === 6) return isForbiddenIpv6(normalized)
return true
}
export function validateRegistryUrl(input: string) {
const url = new URL(input)
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("unsupported_protocol")
}
if (url.username || url.password) {
throw new Error("embedded_credentials")
}
const hostname = url.hostname.replace(/^\[|\]$/g, "")
if (isIP(hostname) && isForbiddenAddress(hostname)) {
throw new Error("forbidden_address")
}
return url
}
const defaultResolve: ResolveHostname = (hostname) =>
lookup(hostname, { all: true, verbatim: true })
export function createGuardedConnector({
resolve = defaultResolve,
connect = buildConnector({ timeout: DEFAULT_TIMEOUT_MS }),
}: {
resolve?: ResolveHostname
connect?: Connector
} = {}): Connector {
return (options, callback) => {
const hostname = options.hostname.replace(/^\[|\]$/g, "")
void (async () => {
const addresses = isIP(hostname)
? [{ address: hostname, family: isIP(hostname) }]
: await resolve(hostname)
if (
addresses.length === 0 ||
addresses.some(({ address }) => isForbiddenAddress(address))
) {
throw new Error("forbidden_address")
}
const [{ address }] = addresses
connect(
{
...options,
hostname: address,
servername: options.servername ?? hostname,
},
callback
)
})().catch((error) =>
callback(error instanceof Error ? error : new Error("dns_failure"), null)
)
}
}
function createGuardedDispatcher() {
return new Agent({
connect: createGuardedConnector(),
headersTimeout: DEFAULT_TIMEOUT_MS,
bodyTimeout: DEFAULT_TIMEOUT_MS,
})
}
async function readLimitedBody(response: Response, maximumBytes: number) {
if (!response.body) return { text: "", size: 0 }
const reader = response.body.getReader()
const chunks: Uint8Array[] = []
let size = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
size += value.byteLength
if (size > maximumBytes) {
await reader.cancel()
throw new Error("body_too_large")
}
chunks.push(value)
}
const body = new Uint8Array(size)
let offset = 0
for (const chunk of chunks) {
body.set(chunk, offset)
offset += chunk.byteLength
}
return { text: new TextDecoder().decode(body), size }
}
export function detectBotChallenge({
status,
contentType,
server,
body,
}: {
status: number
contentType: string
server: string
body: string
}) {
const content = body.toLowerCase()
const provider = server.toLowerCase()
const isHtml = contentType.toLowerCase().includes("text/html")
const marker = [
"cf-chl-",
"challenge-platform",
"just a moment",
"attention required",
"vercel security checkpoint",
"captcha",
].some((value) => content.includes(value))
return (
marker &&
(isHtml || status === 403 || status === 429) &&
(provider.includes("cloudflare") ||
provider.includes("vercel") ||
content.includes("challenge") ||
content.includes("captcha"))
)
}
function isRedirect(status: number) {
return [301, 302, 303, 307, 308].includes(status)
}
function isRetryableStatus(status: number) {
return status === 408 || status === 429 || status >= 500
}
function parseRetryAfter(value: string | null, now: number) {
if (!value) return null
const seconds = Number(value)
const delay = Number.isFinite(seconds)
? seconds * 1000
: new Date(value).getTime() - now
if (!Number.isFinite(delay) || delay < 0) return null
return Math.min(delay, MAX_RETRY_AFTER_MS)
}
function sleep(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
async function fetchOnce(
input: string,
{
timeoutMs,
maximumBytes,
maximumRedirects,
fetchImpl,
createDispatcher,
}: {
timeoutMs: number
maximumBytes: number
maximumRedirects: number
fetchImpl: FetchLike
createDispatcher: () => Dispatcher
}
) {
const startedAt = Date.now()
let currentUrl = validateRegistryUrl(input)
let redirectCount = 0
while (true) {
validateRegistryUrl(currentUrl.toString())
const remainingMs = timeoutMs - (Date.now() - startedAt)
if (remainingMs <= 0) {
return {
ok: false,
failureCode: "timeout",
reachable: false,
botChallenge: false,
durationMs: Date.now() - startedAt,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
const dispatcher = createDispatcher()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), remainingMs)
try {
const response = await fetchImpl(currentUrl.toString(), {
dispatcher,
redirect: "manual",
signal: controller.signal,
headers: {
Accept: "application/json",
"User-Agent": "shadcn-registry-health/1.0",
},
})
if (isRedirect(response.status)) {
const location = response.headers.get("location")
await response.body?.cancel()
if (!location) {
return {
ok: false,
failureCode: "redirect_without_location",
reachable: false,
botChallenge: false,
status: response.status,
durationMs: Date.now() - startedAt,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
redirectCount += 1
if (redirectCount > maximumRedirects) {
return {
ok: false,
failureCode: "too_many_redirects",
reachable: false,
botChallenge: false,
status: response.status,
durationMs: Date.now() - startedAt,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
currentUrl = validateRegistryUrl(
new URL(location, currentUrl).toString()
)
continue
}
let body: Awaited<ReturnType<typeof readLimitedBody>>
try {
body = await readLimitedBody(response, maximumBytes)
} catch {
return {
ok: false,
failureCode: "body_too_large",
reachable: response.ok,
botChallenge: false,
status: response.status,
durationMs: Date.now() - startedAt,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
const contentType = response.headers.get("content-type") ?? ""
const botChallenge = detectBotChallenge({
status: response.status,
contentType,
server: response.headers.get("server") ?? "",
body: body.text,
})
if (botChallenge) {
return {
ok: false,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: response.status,
durationMs: Date.now() - startedAt,
responseSize: body.size,
contentType,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
if (!response.ok) {
return {
ok: false,
failureCode: `http_${response.status}`,
reachable: false,
botChallenge: false,
status: response.status,
durationMs: Date.now() - startedAt,
responseSize: body.size,
contentType,
redirectCount,
finalUrl: currentUrl.toString(),
retryAfterMs:
parseRetryAfter(response.headers.get("retry-after"), Date.now()) ??
undefined,
} satisfies RegistryJsonAttemptResult
}
try {
return {
ok: true,
json: JSON.parse(body.text),
status: response.status,
durationMs: Date.now() - startedAt,
responseSize: body.size,
contentType,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
} catch {
return {
ok: false,
failureCode: "invalid_json",
reachable: true,
botChallenge: false,
status: response.status,
durationMs: Date.now() - startedAt,
responseSize: body.size,
contentType,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
}
} catch (error) {
const messages = [
error instanceof Error ? error.message : "",
error instanceof Error && error.cause instanceof Error
? error.cause.message
: "",
]
const guardedFailure = [
"embedded_credentials",
"forbidden_address",
"unsupported_protocol",
].find((failureCode) => messages.includes(failureCode))
const failureCode =
guardedFailure ??
(error instanceof Error && error.name === "AbortError"
? "timeout"
: "transport_error")
return {
ok: false,
failureCode,
reachable: false,
botChallenge: false,
durationMs: Date.now() - startedAt,
redirectCount,
finalUrl: currentUrl.toString(),
} satisfies RegistryJsonAttemptResult
} finally {
clearTimeout(timeout)
await dispatcher.close()
}
}
}
export async function fetchRegistryJson(
input: string,
options: {
attempts?: number
timeoutMs?: number
maximumBytes?: number
maximumRedirects?: number
fetchImpl?: FetchLike
createDispatcher?: () => Dispatcher
wait?: (milliseconds: number) => Promise<unknown>
random?: () => number
} = {}
): Promise<RegistryJsonResult> {
const attempts = options.attempts ?? 3
const wait = options.wait ?? sleep
const random = options.random ?? Math.random
let result: Awaited<ReturnType<typeof fetchOnce>> | undefined
for (let attempt = 0; attempt < attempts; attempt += 1) {
result = await fetchOnce(input, {
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
maximumBytes: options.maximumBytes ?? DEFAULT_MAX_BODY_BYTES,
maximumRedirects: options.maximumRedirects ?? DEFAULT_MAX_REDIRECTS,
fetchImpl: options.fetchImpl ?? (undiciFetch as unknown as FetchLike),
createDispatcher: options.createDispatcher ?? createGuardedDispatcher,
})
const retryable =
!result.ok &&
!result.botChallenge &&
(result.failureCode === "transport_error" ||
result.failureCode === "timeout" ||
(result.status !== undefined && isRetryableStatus(result.status)))
if (!retryable || attempt === attempts - 1) {
return result
}
await wait(
result.retryAfterMs ??
Math.min(1000 * 2 ** attempt + random() * 250, 5000)
)
}
return result!
}

View File

@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest"
import { registryMonitorEntryStateSchema } from "./schema"
describe("registryMonitorEntryStateSchema", () => {
it("migrates legacy availability recovery state from retained history", () => {
const failures = Array.from({ length: 3 }, (_, index) => ({
checkedAt: `2026-08-16T0${index}:00:00.000Z`,
outcome: "unreachable" as const,
durationMs: 100,
redirectCount: 0,
}))
const recovery = {
checkedAt: "2026-08-24T11:00:00.000Z",
outcome: "reachable" as const,
durationMs: 100,
redirectCount: 0,
schemaValid: true,
}
const state = registryMonitorEntryStateSchema.parse({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: recovery.checkedAt,
status: "unavailable",
itemCursor: 0,
itemNames: [],
recentIndex: [...failures, recovery],
recentDryRuns: [],
daily: [],
latestHygiene: {
contentTypeJson: true,
noDuplicateNames: true,
nameMatches: true,
},
})
expect(state).toMatchObject({
consecutiveSuccessfulIndexes: 1,
consecutiveIndexFailures: 0,
availabilityOutageSince: failures[0].checkedAt,
availabilityRecoveryRequired: true,
})
})
it("preserves the last success as the start of a legacy outage", () => {
const lastSuccessfulCheck = "2026-08-23T08:00:00.000Z"
const state = registryMonitorEntryStateSchema.parse({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck,
status: "degraded",
itemCursor: 0,
itemNames: [],
recentIndex: Array.from({ length: 3 }, (_, index) => ({
checkedAt: `2026-08-24T0${index}:00:00.000Z`,
outcome: "unreachable" as const,
durationMs: 100,
redirectCount: 0,
})),
recentDryRuns: [],
daily: [],
latestHygiene: {
contentTypeJson: true,
noDuplicateNames: true,
nameMatches: true,
},
})
expect(state).toMatchObject({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 3,
availabilityOutageSince: lastSuccessfulCheck,
availabilityRecoveryRequired: true,
})
})
})

View File

@@ -0,0 +1,315 @@
import { z } from "zod"
import { registryNamespaceSchema } from "../registry-directory"
export const REGISTRY_HEALTH_SCHEMA_VERSION = 1 as const
export const REGISTRY_HEALTH_SCORE_VERSION = 1 as const
export const REGISTRY_DRY_RUN_VERSION = 1 as const
export const REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH = 1000
export const registryHealthStatusSchema = z.enum([
"healthy",
"observing",
"degraded",
"unavailable",
])
export const registryHealthStatusReasonCodeSchema = z.enum([
"healthy_thresholds",
"collecting_baseline",
"index_unavailable",
"recovery_pending",
"consecutive_index_failures",
"index_schema_invalid",
"item_validation_failures",
"dry_run_failures",
])
export const registryHealthBreakdownSchema = z
.object({
reliability: z.number().min(0).max(45),
correctness: z.number().min(0).max(25),
installability: z.number().min(0).max(20),
hygiene: z.number().min(0).max(10),
})
.strict()
export const registryHealthSchema = z
.object({
schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION),
scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION),
status: registryHealthStatusSchema,
statusReason: z
.object({
code: registryHealthStatusReasonCodeSchema,
message: z.string().min(1),
})
.strict()
.optional(),
score: z.number().min(0).max(100),
breakdown: registryHealthBreakdownSchema,
availability7d: z.number().min(0).max(1),
availability30d: z.number().min(0).max(1),
monitoringLimited: z.boolean(),
firstObservedAt: z.string().datetime().optional(),
checkedAt: z.string().datetime(),
lastSuccessfulCheck: z.string().datetime().optional(),
hidden: z.boolean(),
})
.strict()
export const registryHealthGlobalMeansSchema = z
.object({
availability7d: z.number().min(0).max(1),
availability30d: z.number().min(0).max(1),
indexSchema: z.number().min(0).max(1),
itemValidity: z.number().min(0).max(1),
dryRun: z.number().min(0).max(1),
})
.strict()
export const registryHealthSnapshotSchema = z
.object({
schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION),
scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION),
generatedAt: z.string().datetime(),
globalMeans: registryHealthGlobalMeansSchema,
registries: z.record(registryNamespaceSchema, registryHealthSchema),
})
.strict()
export const registryIndexObservationSchema = z
.object({
checkedAt: z.string().datetime(),
outcome: z.enum(["reachable", "unreachable", "bot_challenge"]),
status: z.number().int().min(100).max(599).optional(),
failureCode: z.string().optional(),
durationMs: z.number().int().nonnegative(),
responseSize: z.number().int().nonnegative().optional(),
redirectCount: z.number().int().nonnegative(),
schemaValid: z.boolean().optional(),
contentTypeJson: z.boolean().optional(),
duplicateNames: z.boolean().optional(),
nameMatches: z.boolean().optional(),
itemCount: z.number().int().nonnegative().optional(),
})
.strict()
export const registryItemObservationSchema = z
.object({
checkedAt: z.string().datetime(),
item: z.string(),
success: z.boolean(),
failureCode: z.string().optional(),
durationMs: z.number().int().nonnegative(),
})
.strict()
export const registryDryRunObservationSchema = z
.object({
checkedAt: z.string().datetime(),
item: z.string(),
success: z.boolean(),
failureCode: z.string().optional(),
failureMessage: z
.string()
.max(REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH)
.optional(),
durationMs: z.number().int().nonnegative(),
})
.strict()
export const registryHealthDailyBucketSchema = z
.object({
date: z.string().date(),
availabilitySuccesses: z.number().int().nonnegative(),
availabilityObservations: z.number().int().nonnegative(),
challengeObservations: z.number().int().nonnegative(),
schemaSuccesses: z.number().int().nonnegative(),
schemaObservations: z.number().int().nonnegative(),
itemSuccesses: z.number().int().nonnegative(),
itemObservations: z.number().int().nonnegative(),
dryRunSuccesses: z.number().int().nonnegative(),
dryRunObservations: z.number().int().nonnegative(),
})
.strict()
const registryMonitorEntryStateInputSchema = z
.object({
firstObservedAt: z.string().datetime(),
lastSuccessfulCheck: z.string().datetime().optional(),
status: registryHealthStatusSchema,
consecutiveSuccessfulIndexes: z.number().int().nonnegative().optional(),
consecutiveIndexFailures: z.number().int().nonnegative().optional(),
availabilityOutageSince: z.string().datetime().optional(),
availabilityRecoveryRequired: z.boolean().optional(),
itemCursor: z.number().int().nonnegative(),
itemNames: z.array(z.string()),
recentIndex: z.array(registryIndexObservationSchema),
recentDryRuns: z.array(registryDryRunObservationSchema),
daily: z.array(registryHealthDailyBucketSchema),
latestHygiene: z
.object({
contentTypeJson: z.boolean().nullable(),
noDuplicateNames: z.boolean().nullable(),
nameMatches: z.boolean().nullable(),
})
.strict(),
})
.strict()
function getLegacyAvailabilityState(
entry: z.infer<typeof registryMonitorEntryStateInputSchema>
) {
const observations = entry.recentIndex.filter(
(observation) => observation.outcome !== "bot_challenge"
)
let cursor = observations.length - 1
let consecutiveSuccessfulIndexes = 0
while (cursor >= 0) {
const observation = observations[cursor]
if (observation.outcome !== "reachable" || !observation.schemaValid) {
break
}
consecutiveSuccessfulIndexes += 1
cursor -= 1
}
const failureEnd = cursor
let precedingFailures = 0
while (cursor >= 0 && observations[cursor].outcome === "unreachable") {
precedingFailures += 1
cursor -= 1
}
const consecutiveIndexFailures =
consecutiveSuccessfulIndexes === 0 ? precedingFailures : 0
const recoveryRequired =
entry.status === "unavailable" ||
(entry.status === "degraded" && precedingFailures >= 3)
const failureStart =
precedingFailures > 0
? observations[failureEnd - precedingFailures + 1]
: undefined
const outageStart =
consecutiveSuccessfulIndexes > 0
? failureStart?.checkedAt
: (entry.lastSuccessfulCheck ?? failureStart?.checkedAt)
const availabilityOutageSince =
recoveryRequired || consecutiveIndexFailures > 0
? (outageStart ?? entry.firstObservedAt)
: undefined
return {
consecutiveSuccessfulIndexes,
consecutiveIndexFailures,
availabilityOutageSince,
availabilityRecoveryRequired: recoveryRequired,
}
}
export const registryMonitorEntryStateSchema =
registryMonitorEntryStateInputSchema.transform((entry) => {
if (
entry.consecutiveSuccessfulIndexes !== undefined &&
entry.consecutiveIndexFailures !== undefined &&
entry.availabilityRecoveryRequired !== undefined
) {
return entry as typeof entry & {
consecutiveSuccessfulIndexes: number
consecutiveIndexFailures: number
availabilityRecoveryRequired: boolean
}
}
return {
...entry,
...getLegacyAvailabilityState(entry),
}
})
export const registryMonitorStateSchema = z
.object({
schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION),
scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION),
dryRunVersion: z.number().int().nonnegative().optional(),
updatedAt: z.string().datetime(),
lastDailyRunAt: z.string().datetime().optional(),
lastWeeklyRunAt: z.string().datetime().optional(),
registries: z.record(
registryNamespaceSchema,
registryMonitorEntryStateSchema
),
})
.strict()
export const registryMonitorRunSchema = z
.object({
schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION),
startedAt: z.string().datetime(),
completedAt: z.string().datetime(),
mode: z.enum(["auto", "hourly", "daily", "weekly", "all"]),
totals: z
.object({
registries: z.number().int().nonnegative(),
reachable: z.number().int().nonnegative(),
unavailable: z.number().int().nonnegative(),
challenges: z.number().int().nonnegative(),
itemChecks: z.number().int().nonnegative(),
dryRuns: z.number().int().nonnegative(),
})
.strict(),
results: z.record(
registryNamespaceSchema,
z
.object({
index: registryIndexObservationSchema.optional(),
items: z.array(registryItemObservationSchema),
dryRun: registryDryRunObservationSchema.optional(),
})
.strict()
),
diagnostics: z.array(z.string()),
})
.strict()
export const registryMonitorOutputSchema = z
.object({
state: registryMonitorStateSchema,
snapshot: registryHealthSnapshotSchema,
run: registryMonitorRunSchema,
})
.strict()
export type RegistryHealth = z.infer<typeof registryHealthSchema>
export type RegistryHealthStatusReasonCode = z.infer<
typeof registryHealthStatusReasonCodeSchema
>
export type RegistryHealthBreakdown = z.infer<
typeof registryHealthBreakdownSchema
>
export type RegistryHealthGlobalMeans = z.infer<
typeof registryHealthGlobalMeansSchema
>
export type RegistryHealthSnapshot = z.infer<
typeof registryHealthSnapshotSchema
>
export type RegistryIndexObservation = z.infer<
typeof registryIndexObservationSchema
>
export type RegistryItemObservation = z.infer<
typeof registryItemObservationSchema
>
export type RegistryDryRunObservation = z.infer<
typeof registryDryRunObservationSchema
>
export type RegistryHealthDailyBucket = z.infer<
typeof registryHealthDailyBucketSchema
>
export type RegistryMonitorEntryState = z.infer<
typeof registryMonitorEntryStateSchema
>
export type RegistryMonitorState = z.infer<typeof registryMonitorStateSchema>
export type RegistryMonitorRun = z.infer<typeof registryMonitorRunSchema>
export type RegistryMonitorOutput = z.infer<typeof registryMonitorOutputSchema>

View File

@@ -0,0 +1,393 @@
import { describe, expect, it } from "vitest"
import type {
RegistryHealthDailyBucket,
RegistryIndexObservation,
RegistryMonitorEntryState,
} from "./schema"
import {
calculateRegistryHealth,
DEFAULT_GLOBAL_MEANS,
getRegistryHealthSignals,
} from "./score"
const NOW = new Date("2026-08-24T12:00:00.000Z")
function createDailyBucket(overrides: Partial<RegistryHealthDailyBucket> = {}) {
return {
date: "2026-08-24",
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 0,
itemObservations: 0,
dryRunSuccesses: 0,
dryRunObservations: 0,
...overrides,
} satisfies RegistryHealthDailyBucket
}
function createIndexObservations(
count = 25,
outcome: RegistryIndexObservation["outcome"] = "reachable"
) {
return Array.from({ length: count }, (_, index) => ({
checkedAt: new Date(
NOW.getTime() - (count - index) * 60 * 60 * 1000
).toISOString(),
outcome,
durationMs: 100,
redirectCount: 0,
schemaValid: outcome === "reachable",
})) satisfies RegistryIndexObservation[]
}
function createState(overrides: Partial<RegistryMonitorEntryState> = {}) {
return {
firstObservedAt: "2026-08-22T10:00:00.000Z",
lastSuccessfulCheck: "2026-08-24T11:00:00.000Z",
status: "healthy",
consecutiveSuccessfulIndexes: 25,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
itemCursor: 0,
itemNames: [],
recentIndex: createIndexObservations(),
recentDryRuns: [],
daily: [
createDailyBucket({
availabilitySuccesses: 25,
availabilityObservations: 25,
schemaSuccesses: 25,
schemaObservations: 25,
}),
],
latestHygiene: {
contentTypeJson: null,
noDuplicateNames: null,
nameMatches: null,
},
...overrides,
} satisfies RegistryMonitorEntryState
}
describe("calculateRegistryHealth", () => {
it("derives the published total from rounded components", () => {
const health = calculateRegistryHealth({
state: createState(),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.score).toBe(
health.breakdown.reliability +
health.breakdown.correctness +
health.breakdown.installability +
health.breakdown.hygiene
)
expect(health.score).toBeGreaterThanOrEqual(0)
expect(health.score).toBeLessThanOrEqual(100)
expect(health.statusReason).toEqual({
code: "healthy_thresholds",
message: "Recent checks are within healthy thresholds",
})
})
it("explains when baseline observations are still being collected", () => {
const health = calculateRegistryHealth({
state: createState({ recentIndex: createIndexObservations(3) }),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.status).toBe("observing")
expect(health.statusReason).toEqual({
code: "collecting_baseline",
message: "Collecting baseline data (3 of 24 checks)",
})
})
it("gives unknown hygiene signals half credit", () => {
const health = calculateRegistryHealth({
state: createState(),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.breakdown.hygiene).toBe(6.25)
})
it("uses a cadence-scaled prior for weekly dry runs", () => {
const state = createState({
daily: [
createDailyBucket({
availabilitySuccesses: 25,
availabilityObservations: 25,
schemaSuccesses: 25,
schemaObservations: 25,
dryRunSuccesses: 1,
dryRunObservations: 1,
}),
],
})
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.breakdown.installability).toBe(18.5)
})
it("excludes challenge observations from availability", () => {
const state = createState({
recentIndex: [
...createIndexObservations(),
{
checkedAt: NOW.toISOString(),
outcome: "bot_challenge",
durationMs: 100,
redirectCount: 0,
},
],
})
const signals = getRegistryHealthSignals(
state,
"https://example.com/{name}.json",
NOW
)
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(signals.availability7d).toEqual({
successes: 25,
observations: 25,
})
expect(health.monitoringLimited).toBe(true)
expect(health.status).toBe("healthy")
})
it("hides a registry only after seven days unavailable", () => {
const state = createState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-16T00:00:00.000Z",
status: "unavailable",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 8,
availabilityOutageSince: "2026-08-16T00:00:00.000Z",
availabilityRecoveryRequired: true,
recentIndex: createIndexObservations(8, "unreachable"),
})
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.status).toBe("unavailable")
expect(health.statusReason).toEqual({
code: "index_unavailable",
message: "No successful index check in the last 24 hours",
})
expect(health.hidden).toBe(true)
})
it("keeps an unavailable registry visible before seven days", () => {
const state = createState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-20T00:00:00.000Z",
status: "unavailable",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 8,
availabilityOutageSince: "2026-08-20T00:00:00.000Z",
availabilityRecoveryRequired: true,
recentIndex: createIndexObservations(8, "unreachable"),
})
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.status).toBe("unavailable")
expect(health.hidden).toBe(false)
})
it("keeps a long outage hidden through the first recovery success", () => {
const recovery = createIndexObservations(1)[0]
const state = createState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: recovery.checkedAt,
status: "unavailable",
consecutiveSuccessfulIndexes: 1,
consecutiveIndexFailures: 0,
availabilityOutageSince: "2026-08-16T00:00:00.000Z",
availabilityRecoveryRequired: true,
recentIndex: [...createIndexObservations(24, "unreachable"), recovery],
})
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.status).toBe("unavailable")
expect(health.statusReason?.code).toBe("recovery_pending")
expect(health.hidden).toBe(true)
})
it("requires two successful checks to recover from availability degradation", () => {
const history = createIndexObservations()
const failures = createIndexObservations(3, "unreachable")
const successes = createIndexObservations(2)
const state = createState({
status: "degraded",
consecutiveSuccessfulIndexes: 1,
consecutiveIndexFailures: 0,
availabilityOutageSince: failures[0].checkedAt,
availabilityRecoveryRequired: true,
recentIndex: [...history, ...failures, successes[0]],
})
const recovering = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
const recovered = calculateRegistryHealth({
state: {
...state,
consecutiveSuccessfulIndexes: 2,
availabilityOutageSince: undefined,
availabilityRecoveryRequired: false,
recentIndex: [...state.recentIndex, successes[1]],
},
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(recovering.status).toBe("degraded")
expect(recovering.statusReason).toEqual({
code: "recovery_pending",
message: "Waiting for a second successful recovery check",
})
expect(recovered.status).toBe("healthy")
})
it("explains each degraded status condition", () => {
const failures = calculateRegistryHealth({
state: createState({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 3,
availabilityOutageSince: "2026-08-24T09:00:00.000Z",
availabilityRecoveryRequired: true,
recentIndex: [
...createIndexObservations(),
...createIndexObservations(3, "unreachable"),
],
}),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
const observations = createIndexObservations()
const invalidSchema = calculateRegistryHealth({
state: createState({
recentIndex: [
...observations.slice(0, -1),
{ ...observations.at(-1)!, schemaValid: false },
],
}),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
const invalidItems = calculateRegistryHealth({
state: createState({
daily: [
createDailyBucket({
availabilitySuccesses: 25,
availabilityObservations: 25,
schemaSuccesses: 25,
schemaObservations: 25,
itemSuccesses: 8,
itemObservations: 10,
}),
],
}),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
const dryRuns = calculateRegistryHealth({
state: createState({
recentDryRuns: [
{
checkedAt: "2026-08-17T12:00:00.000Z",
item: "button",
success: false,
durationMs: 100,
},
{
checkedAt: "2026-08-24T12:00:00.000Z",
item: "button",
success: false,
durationMs: 100,
},
],
}),
registryUrl: "https://example.com/r/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(failures.statusReason).toEqual({
code: "consecutive_index_failures",
message: "The registry index failed 3 consecutive checks",
})
expect(invalidSchema.statusReason).toEqual({
code: "index_schema_invalid",
message: "The registry index failed schema validation",
})
expect(invalidItems.statusReason).toEqual({
code: "item_validation_failures",
message: "Sampled registry items are failing validation",
})
expect(dryRuns.statusReason).toEqual({
code: "dry_run_failures",
message: "The last two CLI dry-run checks failed",
})
})
it("recovers correctness degradation when its condition clears", () => {
const state = createState({
status: "degraded",
recentIndex: createIndexObservations(),
})
const health = calculateRegistryHealth({
state,
registryUrl: "https://example.com/{name}.json",
globalMeans: DEFAULT_GLOBAL_MEANS,
now: NOW,
})
expect(health.status).toBe("healthy")
})
})

View File

@@ -0,0 +1,442 @@
import {
REGISTRY_HEALTH_SCHEMA_VERSION,
REGISTRY_HEALTH_SCORE_VERSION,
type RegistryHealth,
type RegistryHealthGlobalMeans,
type RegistryMonitorEntryState,
type RegistryMonitorState,
} from "./schema"
const DAY_MS = 24 * 60 * 60 * 1000
const DEFAULT_GLOBAL_MEANS: RegistryHealthGlobalMeans = {
availability7d: 0.85,
availability30d: 0.85,
indexSchema: 0.9,
itemValidity: 0.9,
dryRun: 0.9,
}
const PRIOR_WEIGHTS = {
availability7d: 24,
availability30d: 24,
indexSchema: 12,
itemValidity: 10,
dryRun: 3,
} as const
type RateSignal = {
successes: number
observations: number
}
export type RegistryHealthSignals = {
availability7d: RateSignal
availability30d: RateSignal
indexSchema: RateSignal
itemValidity: RateSignal
dryRun: RateSignal
hygiene: {
https: boolean
contentTypeJson: boolean | null
noDuplicateNames: boolean | null
nameMatches: boolean | null
}
}
function round(value: number, digits: number) {
const factor = 10 ** digits
return Math.round((value + Number.EPSILON) * factor) / factor
}
function isWithinWindow(timestamp: string, now: Date, days: number) {
const age = now.getTime() - new Date(timestamp).getTime()
return age >= 0 && age < days * DAY_MS
}
function isBucketWithinWindow(date: string, now: Date, days: number) {
return isWithinWindow(`${date}T00:00:00.000Z`, now, days)
}
function sumDailySignal(
state: RegistryMonitorEntryState,
now: Date,
days: number,
successesKey:
| "availabilitySuccesses"
| "schemaSuccesses"
| "itemSuccesses"
| "dryRunSuccesses",
observationsKey:
| "availabilityObservations"
| "schemaObservations"
| "itemObservations"
| "dryRunObservations"
) {
return state.daily.reduce<RateSignal>(
(signal, bucket) => {
if (!isBucketWithinWindow(bucket.date, now, days)) {
return signal
}
signal.successes += bucket[successesKey]
signal.observations += bucket[observationsKey]
return signal
},
{ successes: 0, observations: 0 }
)
}
export function getRegistryHealthSignals(
state: RegistryMonitorEntryState,
registryUrl: string,
now: Date
): RegistryHealthSignals {
const availability7d = state.recentIndex.reduce<RateSignal>(
(signal, observation) => {
if (
observation.outcome === "bot_challenge" ||
!isWithinWindow(observation.checkedAt, now, 7)
) {
return signal
}
signal.observations += 1
signal.successes += observation.outcome === "reachable" ? 1 : 0
return signal
},
{ successes: 0, observations: 0 }
)
return {
availability7d,
availability30d: sumDailySignal(
state,
now,
30,
"availabilitySuccesses",
"availabilityObservations"
),
indexSchema: sumDailySignal(
state,
now,
30,
"schemaSuccesses",
"schemaObservations"
),
itemValidity: sumDailySignal(
state,
now,
30,
"itemSuccesses",
"itemObservations"
),
dryRun: sumDailySignal(
state,
now,
30,
"dryRunSuccesses",
"dryRunObservations"
),
hygiene: {
https: registryUrl.startsWith("https://"),
contentTypeJson: state.latestHygiene.contentTypeJson,
noDuplicateNames: state.latestHygiene.noDuplicateNames,
nameMatches: state.latestHygiene.nameMatches,
},
}
}
function getObservedMean(
signals: RateSignal[],
minimumObservations: number,
fallback: number
) {
const eligible = signals.filter(
(signal) => signal.observations >= minimumObservations
)
const observations = eligible.reduce(
(total, signal) => total + signal.observations,
0
)
if (observations === 0) {
return fallback
}
const successes = eligible.reduce(
(total, signal) => total + signal.successes,
0
)
return successes / observations
}
export function calculateGlobalMeans(
state: RegistryMonitorState,
registryUrls: Record<string, string>,
now: Date
): RegistryHealthGlobalMeans {
const signals = Object.entries(state.registries).map(([name, entry]) =>
getRegistryHealthSignals(entry, registryUrls[name] ?? "http://invalid", now)
)
return {
availability7d: getObservedMean(
signals.map((signal) => signal.availability7d),
24,
DEFAULT_GLOBAL_MEANS.availability7d
),
availability30d: getObservedMean(
signals.map((signal) => signal.availability30d),
24,
DEFAULT_GLOBAL_MEANS.availability30d
),
indexSchema: getObservedMean(
signals.map((signal) => signal.indexSchema),
12,
DEFAULT_GLOBAL_MEANS.indexSchema
),
itemValidity: getObservedMean(
signals.map((signal) => signal.itemValidity),
10,
DEFAULT_GLOBAL_MEANS.itemValidity
),
dryRun: getObservedMean(
signals.map((signal) => signal.dryRun),
3,
DEFAULT_GLOBAL_MEANS.dryRun
),
}
}
function smooth(signal: RateSignal, globalMean: number, priorWeight: number) {
return (
(signal.successes + globalMean * priorWeight) /
(signal.observations + priorWeight)
)
}
function getHygienePoints(value: boolean | null) {
if (value === null) {
return 1.25
}
return value ? 2.5 : 0
}
function getNonChallengeObservations(state: RegistryMonitorEntryState) {
return state.recentIndex.filter(
(observation) => observation.outcome !== "bot_challenge"
)
}
function getUnavailableSince(state: RegistryMonitorEntryState) {
return state.availabilityOutageSince ?? state.firstObservedAt
}
function deriveStatus(
state: RegistryMonitorEntryState,
signals: RegistryHealthSignals,
now: Date
) {
const observations = getNonChallengeObservations(state)
const latest = observations.at(-1)
const unavailableFor =
now.getTime() - new Date(getUnavailableSince(state)).getTime()
const unavailable =
unavailableFor >= DAY_MS &&
(state.availabilityRecoveryRequired || latest?.outcome === "unreachable")
if (unavailable) {
if (latest?.outcome === "reachable") {
return {
status: "unavailable" as const,
statusReason: {
code: "recovery_pending" as const,
message: "Waiting for a second successful recovery check",
},
}
}
return {
status: "unavailable" as const,
statusReason: {
code: "index_unavailable" as const,
message: "No successful index check in the last 24 hours",
},
}
}
const observationSpan = latest
? new Date(latest.checkedAt).getTime() -
new Date(state.firstObservedAt).getTime()
: 0
if (observations.length < 24 || observationSpan < DAY_MS) {
return {
status: "observing" as const,
statusReason: {
code: "collecting_baseline" as const,
message:
observations.length < 24
? `Collecting baseline data (${observations.length} of 24 checks)`
: "Collecting a full day of baseline data",
},
}
}
const trailingFailures = state.consecutiveIndexFailures
const latestSchemaInvalid =
latest?.outcome === "reachable" && latest.schemaValid === false
const itemDegraded =
signals.itemValidity.observations >= 10 &&
signals.itemValidity.successes / signals.itemValidity.observations < 0.9
const recentDryRuns = state.recentDryRuns.slice(-2)
const dryRunDegraded =
recentDryRuns.length === 2 &&
recentDryRuns.every((observation) => !observation.success)
if (trailingFailures >= 3) {
return {
status: "degraded" as const,
statusReason: {
code: "consecutive_index_failures" as const,
message: `The registry index failed ${trailingFailures} consecutive checks`,
},
}
}
if (latestSchemaInvalid) {
return {
status: "degraded" as const,
statusReason: {
code: "index_schema_invalid" as const,
message: "The registry index failed schema validation",
},
}
}
if (itemDegraded) {
return {
status: "degraded" as const,
statusReason: {
code: "item_validation_failures" as const,
message: "Sampled registry items are failing validation",
},
}
}
if (dryRunDegraded) {
return {
status: "degraded" as const,
statusReason: {
code: "dry_run_failures" as const,
message: "The last two CLI dry-run checks failed",
},
}
}
if (state.availabilityRecoveryRequired) {
return {
status: "degraded" as const,
statusReason: {
code: "recovery_pending" as const,
message: "Waiting for a second successful recovery check",
},
}
}
return {
status: "healthy" as const,
statusReason: {
code: "healthy_thresholds" as const,
message: "Recent checks are within healthy thresholds",
},
}
}
export function calculateRegistryHealth({
state,
registryUrl,
globalMeans,
now,
}: {
state: RegistryMonitorEntryState
registryUrl: string
globalMeans: RegistryHealthGlobalMeans
now: Date
}): RegistryHealth {
const signals = getRegistryHealthSignals(state, registryUrl, now)
const availability7d = smooth(
signals.availability7d,
globalMeans.availability7d,
PRIOR_WEIGHTS.availability7d
)
const availability30d = smooth(
signals.availability30d,
globalMeans.availability30d,
PRIOR_WEIGHTS.availability30d
)
const indexSchema = smooth(
signals.indexSchema,
globalMeans.indexSchema,
PRIOR_WEIGHTS.indexSchema
)
const itemValidity = smooth(
signals.itemValidity,
globalMeans.itemValidity,
PRIOR_WEIGHTS.itemValidity
)
const dryRun = smooth(
signals.dryRun,
globalMeans.dryRun,
PRIOR_WEIGHTS.dryRun
)
const breakdown = {
reliability: round(
45 * (0.65 * availability7d + 0.35 * availability30d),
3
),
correctness: round(10 * indexSchema + 15 * itemValidity, 3),
installability: round(20 * dryRun, 3),
hygiene: round(
(signals.hygiene.https ? 2.5 : 0) +
getHygienePoints(signals.hygiene.contentTypeJson) +
getHygienePoints(signals.hygiene.noDuplicateNames) +
getHygienePoints(signals.hygiene.nameMatches),
3
),
}
const score = round(
breakdown.reliability +
breakdown.correctness +
breakdown.installability +
breakdown.hygiene,
3
)
const { status, statusReason } = deriveStatus(state, signals, now)
const unavailableFor =
now.getTime() - new Date(getUnavailableSince(state)).getTime()
const hidden = status === "unavailable" && unavailableFor >= 7 * DAY_MS
const latestObservation = state.recentIndex.at(-1)
const checkedAt = latestObservation?.checkedAt ?? state.firstObservedAt
return {
schemaVersion: REGISTRY_HEALTH_SCHEMA_VERSION,
scoreVersion: REGISTRY_HEALTH_SCORE_VERSION,
status,
statusReason,
score,
breakdown,
availability7d: round(availability7d, 6),
availability30d: round(availability30d, 6),
monitoringLimited: latestObservation?.outcome === "bot_challenge",
firstObservedAt: state.firstObservedAt,
checkedAt,
lastSuccessfulCheck: state.lastSuccessfulCheck,
hidden,
}
}
export { DEFAULT_GLOBAL_MEANS, PRIOR_WEIGHTS }

View File

@@ -0,0 +1,23 @@
import type { RegistryMonitorEntryState } from "./schema"
function createRegistryMonitorEntryState(now: Date) {
return {
firstObservedAt: now.toISOString(),
status: "observing",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
itemCursor: 0,
itemNames: [],
recentIndex: [],
recentDryRuns: [],
daily: [],
latestHygiene: {
contentTypeJson: null,
noDuplicateNames: null,
nameMatches: null,
},
} satisfies RegistryMonitorEntryState
}
export { createRegistryMonitorEntryState }

346
apps/v4/lib/registry.ts Normal file
View File

@@ -0,0 +1,346 @@
import { promises as fs } from "fs"
import path from "path"
import { getComponent as getExamplesComponent } from "@/examples/__components__"
import { ExamplesIndex } from "@/examples/__index__"
import { LRUCache } from "lru-cache"
import { registryItemSchema, type registryItemFileSchema } from "shadcn/schema"
import { type z } from "zod"
import { readFileFromRoot } from "@/lib/read-file"
import { getComponent as getStylesComponent } from "@/registry/__components__"
import { Index as StylesIndex } from "@/registry/__index__"
import { BASES } from "@/registry/bases"
import { getComponent as getBasesComponent } from "@/registry/bases/__components__"
import { Index as BasesIndex } from "@/registry/bases/__index__"
// LRU cache for cross-request caching of registry items.
// File reads are I/O-bound, so caching improves dev server performance.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registryCache = new LRUCache<string, any>({
max: 500,
ttl: 1000 * 60 * 5, // 5 minutes (shorter for dev to pick up changes).
})
function getBaseForStyle(styleName: string) {
for (const base of BASES) {
if (styleName.startsWith(`${base.name}-`)) {
return base.name
}
}
return null
}
function getDemoIndexKey(styleName: string) {
if (ExamplesIndex[styleName]) {
return styleName
}
const base = getBaseForStyle(styleName)
if (base && ExamplesIndex[base]) {
return base
}
return styleName
}
function getBaseIndex(styleName: string) {
const base = getBaseForStyle(styleName)
return base ? BasesIndex[base] : null
}
function getStyleIndex(styleName: string) {
return StylesIndex[styleName] ?? null
}
function getMergedIndexForStyle(styleName: string) {
const styleIndex = getStyleIndex(styleName)
const baseIndex = getBaseIndex(styleName)
if (styleIndex && baseIndex) {
return { ...baseIndex, ...styleIndex }
}
return styleIndex ?? baseIndex
}
function getRegistryEntry(name: string, styleName: string) {
return getStyleIndex(styleName)?.[name] ?? getBaseIndex(styleName)?.[name]
}
// Lazy components live in per-style __components__ shards (separate from the
// metadata indexes) so consumers only pull the requested style's dynamic
// imports into their module graph. Lookups mirror getRegistryEntry.
function getRegistryComponentEntry(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
return (
getStylesComponent(styleName, name) ??
(base ? getBasesComponent(base, name) : undefined)
)
}
export function getDemoComponent(name: string, styleName: string) {
const key = getDemoIndexKey(styleName)
return getExamplesComponent(key, name)
}
export async function getDemoItem(name: string, styleName: string) {
const key = getDemoIndexKey(styleName)
const demo = ExamplesIndex[key]?.[name]
if (!demo) {
return null
}
const content = await readFileFromRoot(demo.filePath)
return {
name: demo.name,
type: "registry:internal" as const,
files: [
{
path: demo.filePath,
content,
type: "registry:internal" as const,
},
],
}
}
export function getRegistryComponent(name: string, styleName: string) {
const demoComponent = getDemoComponent(name, styleName)
if (demoComponent) {
return demoComponent
}
return getRegistryComponentEntry(name, styleName)
}
export async function getRegistryItems(
styleName: string,
filter?: (item: z.infer<typeof registryItemSchema>) => boolean
) {
const styleIndex = getMergedIndexForStyle(styleName)
if (!styleIndex) {
return []
}
const entries = Object.values(styleIndex)
const filteredEntries = filter ? entries.filter(filter) : entries
return await Promise.all(
filteredEntries.map(async (entry) => {
const item = await getRegistryItem(entry.name, styleName)
return item
})
).then((results) => results.filter(Boolean))
}
export async function getRegistryItem(name: string, styleName: string) {
const cacheKey = `${styleName}:${name}`
// Check cache first.
if (registryCache.has(cacheKey)) {
return registryCache.get(cacheKey)
}
const item = getRegistryEntry(name, styleName)
if (!item) {
registryCache.set(cacheKey, null)
return null
}
const normalizedItem = {
...item,
files: item.files.map((file: unknown) =>
typeof file === "string" ? { path: file } : file
),
}
// Convert all file paths to object.
// TODO: remove when we migrate to new registry.
// Fail early before doing expensive file operations.
const result = registryItemSchema.safeParse(normalizedItem)
if (!result.success) {
registryCache.set(cacheKey, null)
return null
}
// Read all files in parallel.
let files: typeof result.data.files = await Promise.all(
item.files.map(async (file: z.infer<typeof registryItemFileSchema>) => {
const content = await getFileContent(file)
const relativePath = path.relative(process.cwd(), file.path)
return {
...file,
path: relativePath,
content,
}
})
)
// Fix file paths.
files = fixFilePaths(files)
const parsed = registryItemSchema.safeParse({
...result.data,
files,
})
if (!parsed.success) {
console.error(parsed.error.message)
registryCache.set(cacheKey, null)
return null
}
// Cache the result.
registryCache.set(cacheKey, parsed.data)
return parsed.data
}
async function getFileContent(file: z.infer<typeof registryItemFileSchema>) {
let code = await fs.readFile(file.path, "utf-8")
// Some registry items uses default export.
// We want to use named export instead.
if (file.type !== "registry:page") {
code = code.replaceAll("export default", "export")
}
// Fix imports.
code = fixImport(code)
return code
}
function getFileTarget(file: z.infer<typeof registryItemFileSchema>) {
let target = file.target
if (!target || target === "") {
const fileName = file.path.split("/").pop()
if (
file.type === "registry:block" ||
file.type === "registry:component" ||
file.type === "registry:example"
) {
target = `components/${fileName}`
}
if (file.type === "registry:ui") {
target = `components/ui/${fileName}`
}
if (file.type === "registry:hook") {
target = `hooks/${fileName}`
}
if (file.type === "registry:lib") {
target = `lib/${fileName}`
}
}
return target ?? ""
}
function fixFilePaths(files: z.infer<typeof registryItemSchema>["files"]) {
if (!files) {
return []
}
// Resolve all paths relative to the first file's directory.
const firstFilePath = files[0].path
const firstFilePathDir = path.dirname(firstFilePath)
return files.map((file) => {
return {
...file,
path: path.relative(firstFilePathDir, file.path),
target: getFileTarget(file),
}
})
}
export function fixImport(content: string) {
content = content.replace(
/@\/styles\/([\w-]+)\/(ui-rtl|ui)\/([\w-]+)/g,
(match, _styleName, type, component) => {
if (type === "ui" || type === "ui-rtl") {
return `@/components/ui/${component}`
}
return match
}
)
const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|ui|hooks|lib))\/([\w-]+)/g
const replacement = (
match: string,
path: string,
type: string,
component: string
) => {
if (type.endsWith("components")) {
return `@/components/${component}`
} else if (type.endsWith("ui")) {
return `@/components/ui/${component}`
} else if (type.endsWith("hooks")) {
return `@/hooks/${component}`
} else if (type.endsWith("lib")) {
return `@/lib/${component}`
}
return match
}
return content.replace(regex, replacement)
}
export type FileTree = {
name: string
path?: string
children?: FileTree[]
}
export function createFileTreeForRegistryItemFiles(
files: Array<{ path: string; target?: string }>
) {
const root: FileTree[] = []
for (const file of files) {
const path = file.target ?? file.path
const parts = path.split("/")
let currentLevel = root
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const isFile = i === parts.length - 1
const existingNode = currentLevel.find((node) => node.name === part)
if (existingNode) {
if (isFile) {
// Update existing file node with full path
existingNode.path = path
} else {
// Move to next level in the tree
currentLevel = existingNode.children!
}
} else {
const newNode: FileTree = isFile
? { name: part, path }
: { name: part, children: [] }
currentLevel.push(newNode)
if (!isFile) {
currentLevel = newNode.children!
}
}
}
}
return root
}

221
apps/v4/lib/rehype.ts Normal file
View File

@@ -0,0 +1,221 @@
import fs from "fs"
import path from "path"
import { ExamplesIndex } from "@/examples/__index__"
import { u } from "unist-builder"
import { visit } from "unist-util-visit"
import { formatCode } from "@/lib/format-code"
import { Index as StylesIndex } from "@/registry/__index__"
import { getActiveStyle } from "@/registry/_legacy-styles"
import { BASES } from "@/registry/bases"
import { Index as BasesIndex } from "@/registry/bases/__index__"
export { formatCode } from "@/lib/format-code"
function getBaseForStyle(styleName: string) {
for (const base of BASES) {
if (styleName.startsWith(`${base.name}-`)) {
return base.name
}
}
return null
}
function getDemoFilePath(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
const demo =
ExamplesIndex[styleName]?.[name] ??
(base ? ExamplesIndex[base]?.[name] : undefined)
if (!demo) return null
return demo.filePath
}
function getRegistryEntry(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
return (
StylesIndex[styleName]?.[name] ??
(base ? BasesIndex[base]?.[name] : undefined)
)
}
interface UnistNode {
type: string
name?: string
tagName?: string
value?: string
properties?: Record<string, unknown>
attributes?: {
name: string
value: unknown
type?: string
}[]
children?: UnistNode[]
}
export interface UnistTree {
type: string
children: UnistNode[]
}
interface NodeToProcess {
node: UnistNode
type: "ComponentSource" | "ComponentPreview"
name: string
styleName: string
fileName?: string
srcPath?: string
hideCode?: boolean
}
export function rehypeComponent() {
return async (tree: UnistTree) => {
const activeStyle = await getActiveStyle()
const nodesToProcess: NodeToProcess[] = []
visit(tree, (node: UnistNode) => {
const { value: srcPath } =
(getNodeAttributeByName(node, "src") as {
name: string
value?: string
type?: string
}) || {}
if (node.name === "ComponentSource") {
const name = getNodeAttributeByName(node, "name")?.value as string
const fileName = getNodeAttributeByName(node, "fileName")?.value as
| string
| undefined
const styleName =
(getNodeAttributeByName(node, "styleName")?.value as string) ||
activeStyle.name
if (name || srcPath) {
nodesToProcess.push({
node,
type: "ComponentSource",
name,
styleName,
fileName,
srcPath,
})
}
}
if (node.name === "ComponentPreview") {
const name = getNodeAttributeByName(node, "name")?.value as string
const styleName =
(getNodeAttributeByName(node, "styleName")?.value as string) ||
activeStyle.name
const hideCode = isTruthyMdxAttribute(
getNodeAttributeByName(node, "hideCode")
)
if (name) {
nodesToProcess.push({
node,
type: "ComponentPreview",
name,
styleName,
hideCode,
})
}
}
})
await Promise.all(
nodesToProcess.map(async (item) => {
try {
if (item.type === "ComponentPreview" && item.hideCode) {
return
}
let src: string | null = null
if (item.srcPath) {
src = path.join(process.cwd(), item.srcPath)
} else {
src = getDemoFilePath(item.name, item.styleName)
if (!src) {
const component = getRegistryEntry(item.name, item.styleName)
if (!component?.files) {
return
}
if (item.type === "ComponentSource" && item.fileName) {
src =
component.files.find((file: unknown) => {
if (typeof file === "string") {
return (
file.endsWith(`${item.fileName}.tsx`) ||
file.endsWith(`${item.fileName}.ts`)
)
}
return false
}) || component.files[0]?.path
} else {
src = component.files[0]?.path
}
}
}
if (!src) {
return
}
const raw = fs.readFileSync(path.join(process.cwd(), src), "utf8")
const source = await formatCode(raw, item.styleName)
item.node.children?.push(
u("element", {
tagName: "pre",
properties: {
__src__: src,
},
children: [
u("element", {
tagName: "code",
properties: {
className: ["language-tsx"],
},
children: [
{
type: "text",
value: source,
},
],
}),
],
})
)
} catch (error) {
console.error(error)
}
})
)
}
}
function getNodeAttributeByName(node: UnistNode, name: string) {
return node.attributes?.find((attribute) => attribute.name === name)
}
function isTruthyMdxAttribute(
attribute?: {
value?: unknown
} | null
) {
if (!attribute) return false
if (!("value" in attribute)) return true
const { value } = attribute
if (value === undefined || value === null) return true
if (typeof value === "boolean") return value
if (typeof value === "string") return value !== "false"
return Boolean(value)
}

7
apps/v4/lib/source.ts Normal file
View File

@@ -0,0 +1,7 @@
import { docs } from "@/.source/server"
import { loader } from "fumadocs-core/source"
export const source = loader({
baseUrl: "/docs",
source: docs.toFumadocsSource(),
})

5
apps/v4/lib/themes.ts Normal file
View File

@@ -0,0 +1,5 @@
import { baseColors } from "@/registry/_legacy-base-colors"
export const THEMES = baseColors
.filter((theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name))
.sort((a, b) => a.name.localeCompare(b.name))

5
apps/v4/lib/utils.ts Normal file
View File

@@ -0,0 +1,5 @@
export { cn } from "cn"
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}