mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
Compare commits
2 Commits
shadcn@3.4
...
shadcn/reg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
319f7f9419 | ||
|
|
e6bc16461a |
3
apps/v4/.gitignore
vendored
3
apps/v4/.gitignore
vendored
@@ -46,3 +46,6 @@ next-env.d.ts
|
||||
.contentlayer
|
||||
.content-collections
|
||||
.source
|
||||
|
||||
# Generated data
|
||||
.data/
|
||||
|
||||
150
apps/v4/app/api/search/community/route.ts
Normal file
150
apps/v4/app/api/search/community/route.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import type { Orama } from "@orama/orama"
|
||||
import { create, load, search } from "@orama/orama"
|
||||
import { registryItemSchema } from "shadcn/registry"
|
||||
import { z } from "zod"
|
||||
|
||||
type RegistryItem = z.infer<typeof registryItemSchema>
|
||||
|
||||
const searchSchema = {
|
||||
name: "string",
|
||||
description: "string",
|
||||
type: "string",
|
||||
author: "string",
|
||||
url: "string",
|
||||
registryName: "string",
|
||||
} as const
|
||||
|
||||
let searchDb: Orama<typeof searchSchema> | null = null
|
||||
|
||||
async function getSearchDb() {
|
||||
if (searchDb) return searchDb
|
||||
|
||||
try {
|
||||
const indexPath = path.join(
|
||||
process.cwd(),
|
||||
".data",
|
||||
"external-registries-index.json"
|
||||
)
|
||||
const indexData = await fs.readFile(indexPath, "utf-8")
|
||||
const savedDb = JSON.parse(indexData)
|
||||
|
||||
searchDb = await create({
|
||||
schema: searchSchema,
|
||||
})
|
||||
|
||||
await load(searchDb, savedDb)
|
||||
return searchDb
|
||||
} catch (error) {
|
||||
console.error("Failed to load search index:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get("q") || ""
|
||||
const limit = parseInt(searchParams.get("limit") || "20")
|
||||
const offset = parseInt(searchParams.get("offset") || "0")
|
||||
|
||||
if (!query) {
|
||||
// Return all items if no query
|
||||
const registryPath = path.join(
|
||||
process.cwd(),
|
||||
".data",
|
||||
"external-registries.json"
|
||||
)
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(registryPath, "utf-8")
|
||||
const registry = JSON.parse(data)
|
||||
|
||||
const items = registry.items.slice(offset, offset + limit)
|
||||
return Response.json(
|
||||
{
|
||||
items,
|
||||
total: registry.items.length,
|
||||
hasMore: offset + limit < registry.items.length,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control":
|
||||
"public, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return Response.json({
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Search using Orama
|
||||
const db = await getSearchDb()
|
||||
if (!db) {
|
||||
// No search index - return empty results
|
||||
return Response.json({
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
})
|
||||
}
|
||||
|
||||
const results = await search(db, {
|
||||
term: query,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
|
||||
console.log(`Search query: "${query}", found ${results.count} results`)
|
||||
|
||||
// Load the full registry to get complete item data
|
||||
const registryPath = path.join(
|
||||
process.cwd(),
|
||||
".data",
|
||||
"external-registries.json"
|
||||
)
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(registryPath, "utf-8")
|
||||
const registry = JSON.parse(data)
|
||||
|
||||
// Map search results to full items
|
||||
const items = results.hits
|
||||
.map((hit) => {
|
||||
return registry.items.find(
|
||||
(item: RegistryItem) => item.name === hit.document.name
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
items,
|
||||
total: results.count,
|
||||
hasMore: offset + limit < results.count,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control":
|
||||
"public, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return Response.json({
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Search API error:", error)
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next"
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app"
|
||||
|
||||
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
|
||||
import { fontVariables } from "@/lib/fonts"
|
||||
@@ -88,16 +89,18 @@ export default function RootLayout({
|
||||
fontVariables
|
||||
)}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<LayoutProvider>
|
||||
<ActiveThemeProvider>
|
||||
{children}
|
||||
<TailwindIndicator />
|
||||
<Toaster position="top-center" />
|
||||
<Analytics />
|
||||
</ActiveThemeProvider>
|
||||
</LayoutProvider>
|
||||
</ThemeProvider>
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider>
|
||||
<LayoutProvider>
|
||||
<ActiveThemeProvider>
|
||||
{children}
|
||||
<TailwindIndicator />
|
||||
<Toaster position="top-center" />
|
||||
<Analytics />
|
||||
</ActiveThemeProvider>
|
||||
</LayoutProvider>
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
236
apps/v4/components/components-community.tsx
Normal file
236
apps/v4/components/components-community.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ArrowUpRightIcon, Loader2Icon, SearchIcon } from "lucide-react"
|
||||
import { useQueryState } from "nuqs"
|
||||
import { registryItemSchema } from "shadcn/registry"
|
||||
import { z } from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
|
||||
|
||||
export function ComponentsCommunitySearch({
|
||||
className,
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("grid gap-4", className)}>
|
||||
<ComponentsCommunitySearchForm />
|
||||
<ComponentsCommunitySearchResults />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
items: z.infer<typeof registryItemSchema>[]
|
||||
total: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
function ComponentsCommunitySearchForm() {
|
||||
const [search, setSearch] = useQueryState("q", {
|
||||
defaultValue: "",
|
||||
throttleMs: 150,
|
||||
})
|
||||
const [isSearching, setIsSearching] = React.useState(false)
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
|
||||
const activeElement = document.activeElement
|
||||
const isInputFocused =
|
||||
activeElement instanceof HTMLInputElement ||
|
||||
activeElement instanceof HTMLTextAreaElement
|
||||
if (!isInputFocused) {
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
return () => document.removeEventListener("keydown", handleKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchIcon className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Search components..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
setIsSearching(true)
|
||||
setTimeout(() => setIsSearching(false), 300)
|
||||
}}
|
||||
className="pr-9 pl-9 shadow-none"
|
||||
/>
|
||||
{isSearching && (
|
||||
<Loader2Icon className="text-muted-foreground absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComponentsCommunityResults({
|
||||
items,
|
||||
}: {
|
||||
items: z.infer<typeof registryItemSchema>[]
|
||||
}) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="p-6 text-center text-sm">
|
||||
<p className="text-muted-foreground text-balance">
|
||||
No components found for this search. Try a different search term.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
{items.map((item, index) => (
|
||||
<a
|
||||
key={`${item.type}-${item.name}-${index}`}
|
||||
href={`${item.meta?.url ?? ""}?utm_source=shadcn-ui&utm_medium=referral&utm_campaign=components-community`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group hover:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 flex h-16 flex-col gap-1 rounded-md p-3 transition-colors outline-none focus-visible:ring-[3px]"
|
||||
title={`${item.name} - ${item.description}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 leading-none font-medium underline-offset-4">
|
||||
{item.name}{" "}
|
||||
{item.meta?.registryName && (
|
||||
<div className="text-muted-foreground ml-auto flex items-center gap-1 text-xs opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||
{item.meta.registryName}
|
||||
<ArrowUpRightIcon className="size-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div className="text-muted-foreground line-clamp-1 max-w-[80%] text-sm">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComponentsCommunitySkeleton() {
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<div key={i} className="flex h-16 flex-col gap-1 rounded-md p-3">
|
||||
<Skeleton className="h-4 w-32 rounded-md" />
|
||||
<Skeleton className="h-4 w-full max-w-[90%] rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComponentsCommunitySearchResults() {
|
||||
const [search] = useQueryState("q", { defaultValue: "" })
|
||||
const [items, setItems] = React.useState<
|
||||
z.infer<typeof registryItemSchema>[]
|
||||
>([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [hasMore, setHasMore] = React.useState(false)
|
||||
const [offset, setOffset] = React.useState(0)
|
||||
const abortControllerRef = React.useRef<AbortController | null>(null)
|
||||
const searchCacheRef = React.useRef<Map<string, SearchResponse>>(new Map())
|
||||
|
||||
const performSearch = React.useCallback(
|
||||
async (query: string, currentOffset = 0) => {
|
||||
const cacheKey = `${query}:${currentOffset}`
|
||||
const cached = searchCacheRef.current.get(cacheKey)
|
||||
if (cached && currentOffset === 0) {
|
||||
setItems(cached.items)
|
||||
setHasMore(cached.hasMore)
|
||||
setOffset(0)
|
||||
return
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
limit: "20",
|
||||
offset: currentOffset.toString(),
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/search/community?${params}`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Search failed")
|
||||
}
|
||||
|
||||
const data: SearchResponse = await response.json()
|
||||
|
||||
searchCacheRef.current.set(cacheKey, data)
|
||||
|
||||
if (currentOffset === 0) {
|
||||
setItems(data.items)
|
||||
} else {
|
||||
setItems((prev) => [...prev, ...data.items])
|
||||
}
|
||||
|
||||
setHasMore(data.hasMore)
|
||||
setOffset(currentOffset)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name !== "AbortError") {
|
||||
console.error("Search error:", error)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
performSearch(search, 0)
|
||||
}, [search, performSearch])
|
||||
|
||||
const loadMore = React.useCallback(() => {
|
||||
if (!loading && hasMore) {
|
||||
performSearch(search, offset + 20)
|
||||
}
|
||||
}, [search, offset, loading, hasMore, performSearch])
|
||||
|
||||
if (loading && items.length === 0) {
|
||||
return <ComponentsCommunitySkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ComponentsCommunityResults items={items} />
|
||||
{hasMore && (
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
disabled={loading}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{loading ? "Loading..." : "Load more"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
12
apps/v4/content/docs/components/community.mdx
Normal file
12
apps/v4/content/docs/components/community.mdx
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Community
|
||||
description: Discover components from the community.
|
||||
---
|
||||
|
||||
import { ComponentsCommunitySearch } from "@/components/components-community"
|
||||
|
||||
The following components are created and maintained by the community. They are compatible with shadcn/ui primitives and works with the CLI.
|
||||
|
||||
You will be taken to the external component page for preview, documentation and installation instructions.
|
||||
|
||||
<ComponentsCommunitySearch className="mt-6" />
|
||||
@@ -3,4 +3,9 @@ title: Components
|
||||
description: Here you can find all the components available in the library. We are working on adding more components.
|
||||
---
|
||||
|
||||
<Callout className="mb-6">
|
||||
Looking for more components? Check out the [Components
|
||||
Community](/docs/components/community) page.
|
||||
</Callout>
|
||||
|
||||
<ComponentsList />
|
||||
|
||||
4
apps/v4/content/docs/components/meta.json
Normal file
4
apps/v4/content/docs/components/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Components",
|
||||
"pages": ["!community", "..."]
|
||||
}
|
||||
@@ -33,6 +33,10 @@ export const siteConfig = {
|
||||
href: "/colors",
|
||||
label: "Colors",
|
||||
},
|
||||
{
|
||||
href: "/docs/components/community",
|
||||
label: "Community",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ const nextConfig = {
|
||||
destination: "/view/:name",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/community",
|
||||
destination: "/docs/components/community",
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack --port 4000",
|
||||
"build": "pnpm --filter=shadcn build && next build",
|
||||
"build": "pnpm --filter=shadcn build && pnpm build:external && next build",
|
||||
"start": "next start --port 4000",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
@@ -14,6 +14,7 @@
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"registry:build": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,json,mdx}\" --cache",
|
||||
"registry:capture": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/capture-registry.mts",
|
||||
"build:external": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-external-registry.mts",
|
||||
"postinstall": "fumadocs-mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -23,6 +24,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@orama/orama": "^3.1.11",
|
||||
"@radix-ui/react-accessible-icon": "^1.1.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
@@ -77,6 +79,7 @@
|
||||
"motion": "^12.12.1",
|
||||
"next": "15.3.1",
|
||||
"next-themes": "0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
"postcss": "^8.5.1",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.7.0",
|
||||
|
||||
5
apps/v4/registry/registry-external.json
Normal file
5
apps/v4/registry/registry-external.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"https://api.npoint.io/db04a255782ab866fcd3",
|
||||
"https://api.npoint.io/8183a39e7dad9bb86e78",
|
||||
"https://api.npoint.io/e69a11a4d660bb12c0de"
|
||||
]
|
||||
171
apps/v4/scripts/build-external-registry.mts
Normal file
171
apps/v4/scripts/build-external-registry.mts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { create, insertMultiple, save } from "@orama/orama"
|
||||
import { z } from "zod"
|
||||
|
||||
// Schema for registries.json - just an array of URLs
|
||||
const RegistriesConfigSchema = z.array(z.string().url())
|
||||
|
||||
// Schema for registry items (matching the public schema)
|
||||
const RegistryItemSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.enum([
|
||||
"registry:lib",
|
||||
"registry:block",
|
||||
"registry:component",
|
||||
"registry:ui",
|
||||
"registry:hook",
|
||||
"registry:theme",
|
||||
"registry:page",
|
||||
"registry:file",
|
||||
"registry:style",
|
||||
"registry:item",
|
||||
]),
|
||||
description: z.string().optional(),
|
||||
author: z.string().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
devDependencies: z.array(z.string()).optional(),
|
||||
registryDependencies: z.array(z.string()).optional(),
|
||||
files: z.array(z.any()).optional(),
|
||||
categories: z.array(z.string()).optional(),
|
||||
meta: z.record(z.any()).optional(),
|
||||
})
|
||||
|
||||
const RegistrySchema = z.object({
|
||||
name: z.string(),
|
||||
homepage: z.string(),
|
||||
items: z.array(RegistryItemSchema),
|
||||
})
|
||||
|
||||
type Registry = z.infer<typeof RegistrySchema>
|
||||
type RegistryItem = z.infer<typeof RegistryItemSchema>
|
||||
|
||||
interface ProcessedRegistry {
|
||||
url: string
|
||||
data: Registry
|
||||
items: RegistryItem[]
|
||||
fetchedAt: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function fetchRegistry(url: string): Promise<ProcessedRegistry> {
|
||||
console.log(`📥 Fetching registry from ${url}`)
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const validated = RegistrySchema.parse(data)
|
||||
|
||||
// Items are already in the correct format
|
||||
const items = validated.items
|
||||
|
||||
console.log(`✅ Successfully fetched ${validated.items.length} items from ${validated.name}`)
|
||||
|
||||
return {
|
||||
url,
|
||||
data: validated,
|
||||
items: items,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to fetch ${url}:`, error)
|
||||
return {
|
||||
url,
|
||||
data: { name: "Unknown", homepage: url, items: [] },
|
||||
items: [],
|
||||
fetchedAt: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExternalRegistry() {
|
||||
console.log("🔨 Building external registry index...")
|
||||
|
||||
// Read registries config
|
||||
const configPath = path.join(process.cwd(), "registry", "registry-external.json")
|
||||
const configContent = await fs.readFile(configPath, "utf-8")
|
||||
const config = RegistriesConfigSchema.parse(JSON.parse(configContent))
|
||||
|
||||
// Output will go to content directory
|
||||
|
||||
// Fetch all registries
|
||||
const results = await Promise.all(
|
||||
config.map(url => fetchRegistry(url))
|
||||
)
|
||||
|
||||
// Combine all items from all registries, adding registry name to each item
|
||||
const allItems = results.flatMap(r =>
|
||||
r.items.map(item => ({
|
||||
...item,
|
||||
meta: {
|
||||
...item.meta,
|
||||
registryName: r.data.name,
|
||||
registryHomepage: r.data.homepage,
|
||||
}
|
||||
}))
|
||||
)
|
||||
|
||||
// Create a registry following the standard schema
|
||||
const registry = {
|
||||
name: "External Registries",
|
||||
homepage: "https://ui.shadcn.com/docs/components",
|
||||
items: allItems,
|
||||
}
|
||||
|
||||
// Create data directory
|
||||
const dataDir = path.join(process.cwd(), ".data")
|
||||
await fs.mkdir(dataDir, { recursive: true })
|
||||
|
||||
// Write registry to data directory
|
||||
const outputPath = path.join(dataDir, "external-registries.json")
|
||||
await fs.writeFile(outputPath, JSON.stringify(registry, null, 2))
|
||||
|
||||
// Create search index
|
||||
console.log("🔍 Building search index...")
|
||||
const searchDb = await create({
|
||||
schema: {
|
||||
name: 'string',
|
||||
description: 'string',
|
||||
type: 'string',
|
||||
author: 'string',
|
||||
url: 'string',
|
||||
registryName: 'string',
|
||||
},
|
||||
components: {
|
||||
tokenizer: {
|
||||
stemming: false, // Disable stemming for faster indexing
|
||||
stopWords: false, // Disable stop words for component names
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Prepare items for indexing
|
||||
const searchItems = allItems.map(item => ({
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
type: item.type,
|
||||
author: item.author || '',
|
||||
url: item.meta?.url || '',
|
||||
registryName: item.meta?.registryName || '',
|
||||
}))
|
||||
|
||||
await insertMultiple(searchDb, searchItems)
|
||||
|
||||
// Save search index
|
||||
const indexPath = path.join(dataDir, "external-registries-index.json")
|
||||
const index = await save(searchDb)
|
||||
await fs.writeFile(indexPath, JSON.stringify(index))
|
||||
|
||||
console.log(`✨ External registry built successfully!`)
|
||||
console.log(`📊 Total registries: ${results.length}`)
|
||||
console.log(`📦 Total items: ${allItems.length}`)
|
||||
console.log(`📍 Registry saved to: ${outputPath}`)
|
||||
console.log(`🔍 Search index saved to: ${indexPath}`)
|
||||
}
|
||||
|
||||
buildExternalRegistry().catch(console.error)
|
||||
@@ -35,6 +35,7 @@
|
||||
"www:build": "pnpm --filter=www build",
|
||||
"v4:dev": "pnpm --filter=v4 dev",
|
||||
"v4:build": "pnpm --filter=v4 build",
|
||||
"build:external": "pnpm --filter=v4 build:external",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"preview": "turbo run preview",
|
||||
|
||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -132,6 +132,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.10.0
|
||||
version: 3.10.0(react-hook-form@7.54.2(react@19.1.0))
|
||||
'@orama/orama':
|
||||
specifier: ^3.1.11
|
||||
version: 3.1.11
|
||||
'@radix-ui/react-accessible-icon':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -294,6 +297,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: 0.4.6
|
||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
nuqs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3(next@15.3.1(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||
postcss:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.1
|
||||
@@ -2629,8 +2635,8 @@ packages:
|
||||
resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@orama/orama@3.1.7':
|
||||
resolution: {integrity: sha512-6yB0117ZjsgNevZw3LP+bkrZa9mU/POPVaXgzMPOBbBc35w2P3R+1vMMhEfC06kYCpd5bf0jodBaTkYQW5TVeQ==}
|
||||
'@orama/orama@3.1.11':
|
||||
resolution: {integrity: sha512-Szki0cgFiXE5F9RLx2lUyEtJllnuCSQ4B8RLDwIjXkVit6qZjoDAxH+xhJs29MjKLDz0tbPLdKFa6QrQ/qoGGA==}
|
||||
engines: {node: '>= 20.0.0'}
|
||||
|
||||
'@oxc-transform/binding-darwin-arm64@0.53.0':
|
||||
@@ -7549,6 +7555,24 @@ packages:
|
||||
resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
nuqs@2.4.3:
|
||||
resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': '>=2'
|
||||
next: '>=14.2.0'
|
||||
react: '>=18.2.0 || ^19.0.0-0'
|
||||
react-router: ^6 || ^7
|
||||
react-router-dom: ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
react-router:
|
||||
optional: true
|
||||
react-router-dom:
|
||||
optional: true
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -10060,7 +10084,7 @@ snapshots:
|
||||
'@types/node': 20.5.1
|
||||
chalk: 4.1.2
|
||||
cosmiconfig: 8.3.6(typescript@5.7.3)
|
||||
cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.7.3))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.7.3))(typescript@5.7.3)
|
||||
cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.7.3))(ts-node@10.9.2(@types/node@20.17.16)(typescript@5.7.3))(typescript@5.7.3)
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.merge: 4.6.2
|
||||
lodash.uniq: 4.5.0
|
||||
@@ -11437,7 +11461,7 @@ snapshots:
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.28.0': {}
|
||||
|
||||
'@orama/orama@3.1.7': {}
|
||||
'@orama/orama@3.1.11': {}
|
||||
|
||||
'@oxc-transform/binding-darwin-arm64@0.53.0':
|
||||
optional: true
|
||||
@@ -14549,7 +14573,7 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.7.3))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.7.3))(typescript@5.7.3):
|
||||
cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.7.3))(ts-node@10.9.2(@types/node@20.17.16)(typescript@5.7.3))(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@types/node': 20.5.1
|
||||
cosmiconfig: 8.3.6(typescript@5.7.3)
|
||||
@@ -15972,7 +15996,7 @@ snapshots:
|
||||
fumadocs-core@15.4.2(@types/react@19.1.2)(next@15.3.1(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.6.1
|
||||
'@orama/orama': 3.1.7
|
||||
'@orama/orama': 3.1.11
|
||||
'@shikijs/rehype': 3.4.2
|
||||
'@shikijs/transformers': 3.4.2
|
||||
github-slugger: 2.0.0
|
||||
@@ -18136,6 +18160,13 @@ snapshots:
|
||||
|
||||
npm-to-yarn@3.0.1: {}
|
||||
|
||||
nuqs@2.4.3(next@15.3.1(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
mitt: 3.0.1
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
next: 15.3.1(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
"registry:build": {
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
},
|
||||
"build:external": {
|
||||
"cache": false,
|
||||
"outputs": [".data/external-registries.json", ".data/external-registries-index.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user