Skip to main content
Glama
helpers.ts10.2 kB
import { load } from "cheerio"; export const RETRY_ATTEMPTS = 3; export const RETRY_DELAY_MS = 500; export const BASE_URL = "https://ui.shadcn.com"; export const RAW_GITHUB_URL = "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps"; export const BLOCK_PAGES = [ `${BASE_URL}/blocks/sidebar`, `${BASE_URL}/blocks/authentication`, ]; export type PackageManager = "pnpm" | "npm" | "yarn" | "bun"; export interface CommandSet { npm: string; pnpm: string; yarn: string; bun: string; } export interface ComponentDocResource { name: string; description?: string; doc?: string; commands?: CommandSet[]; links?: string[]; isBlock?: boolean; } export const RUNTIME_REPLACEMENTS: Record<Exclude<PackageManager, 'npm'>, string> = { pnpm: 'pnpm dlx', yarn: 'yarn dlx', bun: 'bunx' }; export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/; export const DESCRIPTION_PATTERNS = [ /description:\s*["']([^"']+)["']/, /description:\s*([^\n]+)/, /description\s*:\s*["']([^"']+)["']/ ]; export const FIRST_PARAGRAPH_REGEX = /---\n[\s\S]*?\n---\n\n([^\n]+)/; export const LINKS_REGEX = /links:\n([\s\S]*?)(?=\n\w|$)/; export const CLI_COMMAND_REGEX = /```bash\nnpx shadcn@latest add [^\n]+\n```/; export const USAGE_REGEX = /## Usage\n\n([\s\S]*?)(?=\n## |$)/; export const CODE_BLOCKS_REGEX = /```(?:tsx|ts|jsx|js)([\s\S]*?)```/g; export const CODE_BLOCK_CLEANUP_REGEX = /```(?:tsx|ts|jsx|js)\n|```$/g; export const resourceCache = new Map<string, ComponentDocResource>(); const loadCheerio = (html: string) => load(html, { decodeEntities: true }); export const validateRuntime = (runtime?: string): runtime is PackageManager => !runtime || ['npm', 'pnpm', 'yarn', 'bun'].includes(runtime); export const extractDescription = (frontmatter: string, mdxContent: string): string => { for (const pattern of DESCRIPTION_PATTERNS) { const match = frontmatter.match(pattern); if (match) return match[1].trim(); } const firstParagraphMatch = mdxContent.match(FIRST_PARAGRAPH_REGEX); return firstParagraphMatch ? firstParagraphMatch[1].trim() : ''; }; export const extractLinks = (frontmatter: string): string[] => { const links: string[] = []; const linksMatch = frontmatter.match(LINKS_REGEX); if (linksMatch) { const linksContent = linksMatch[1]; const docLinkMatch = linksContent.match(/doc:\s*([^\n]+)/); const apiLinkMatch = linksContent.match(/api:\s*([^\n]+)/); if (docLinkMatch) links.push(docLinkMatch[1].trim()); if (apiLinkMatch) links.push(apiLinkMatch[1].trim()); } return links; }; export const getCliCommand = (cliCommand: string, runtime?: PackageManager): string => { if (!runtime || runtime === 'npm') return cliCommand; return cliCommand.replace('npx', RUNTIME_REPLACEMENTS[runtime]); }; export const createResponse = (text: string, isError = false, mimeType?: string) => ({ content: [{ type: "text" as const, text, ...(mimeType && { mimeType }) }], ...(isError && { isError }) }); export const handleError = (error: unknown, prefix: string) => createResponse(`${prefix}: ${error instanceof Error ? error.message : String(error)}`, true); export async function fetchWithRetry(url: string, retries = RETRY_ATTEMPTS, delay = RETRY_DELAY_MS): Promise<Response> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } return response; } catch (error) { if (retries <= 1) throw error; await new Promise(resolve => setTimeout(resolve, delay)); return fetchWithRetry(url, retries - 1, delay * 2); // Exponential backoff } } async function cacheResource<T>( key: string, fetchFn: () => Promise<T>, cache: Map<string, T> ): Promise<T> { // Check cache first if (cache.has(key)) { return cache.get(key)!; } try { const data = await fetchFn(); cache.set(key, data); return data; } catch (error) { throw new Error(`Failed to fetch data for key '${key}': ${error instanceof Error ? error.message : String(error)}`); } } export async function fetchAndCache( key: string, fetchFn: () => Promise<any>, transformFn: (data: any) => ComponentDocResource[] ): Promise<ComponentDocResource[]> { try { const rawData = await fetchFn(); const transformedData = transformFn(rawData); transformedData.forEach(data => resourceCache.set(data.name, data)); return transformedData; } catch (error) { throw new Error( `Failed to fetch and transform data for key '${key}': ${ error instanceof Error ? error.message : String(error) }` ); } } export async function fetchAndCacheComponentData(component: string): Promise<ComponentDocResource> { if (!component || typeof component !== 'string') { throw new Error('Invalid component name'); } // Sanitize component name const sanitizedComponent = component.replace(/[^a-zA-Z0-9-_]/g, ''); if (sanitizedComponent !== component) { throw new Error(`Invalid component name: ${component}`); } // Check cache if (resourceCache.has(component)) { return resourceCache.get(component)!; } const docSubPath = `www/content/docs/components`; const url = `${RAW_GITHUB_URL}/${docSubPath}/${component}.mdx`; const transformComponentData = (mdxContent: string): ComponentDocResource[] => { const frontmatterMatch = mdxContent.match(FRONTMATTER_REGEX); const frontmatter = frontmatterMatch ? frontmatterMatch[1] : ''; const description = extractDescription(frontmatter, mdxContent); const links = extractLinks(frontmatter); const cliCommandMatch = mdxContent.match(CLI_COMMAND_REGEX); const cliCommand = cliCommandMatch ? cliCommandMatch[0].replace(/```bash\n|\n```/g, '').trim() : undefined; let commands: CommandSet[] | undefined = undefined; if (cliCommand) { commands = [{ npm: cliCommand, pnpm: getCliCommand(cliCommand, 'pnpm'), yarn: getCliCommand(cliCommand, 'yarn'), bun: getCliCommand(cliCommand, 'bun') }]; } return [{ name: component, description, doc: mdxContent, commands, links: links.length > 0 ? links : undefined, isBlock: false }]; }; const [componentData] = await fetchAndCache( component, async () => { const response = await fetchWithRetry(url); return response.text(); }, transformComponentData ); return componentData; } export type Block = { name: string; command: string; doc: string; description?: string; }; export async function fetchAndCacheBlocks(): Promise<ComponentDocResource[]> { const transformBlocks = (blockPages: Block[][]): ComponentDocResource[] => { const allBlocks = blockPages.flat(); return allBlocks.map((block: Block) => ({ name: block.name, description: block.description, doc: block.doc, commands: [{ npm: block.command, pnpm: getCliCommand(block.command, 'pnpm'), yarn: getCliCommand(block.command, 'yarn'), bun: getCliCommand(block.command, 'bun') }], isBlock: true })); }; return fetchAndCache( 'blocks', async () => Promise.all(BLOCK_PAGES.map(parseBlocksFromPage)), transformBlocks ); } export async function parseBlocksFromPage(url: string): Promise<Block[]> { if (!url || !url.startsWith('https://')) { throw new Error(`Invalid URL: ${url}`); } try { const response = await fetchWithRetry(url); const html = await response.text(); const $ = loadCheerio(html); const blocks: Block[] = []; $('.container-wrapper.flex-1 div[id]').each((_, el) => { const $block = $(el); const id = $block.attr('id'); if (id && !id.startsWith('radix-')) { const anchor = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > a'); const description = anchor.text().trim(); const command = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > div.ml-auto.hidden.items-center.gap-2.md\\:flex > div.flex.h-7.items-center.gap-1.rounded-md.border.p-\\[2px\\] > button > span').text().trim(); const doc = $block.find('code').first().text().trim(); blocks.push({ name: id, description, command, doc }); } }); if (blocks.length === 0) { console.error(`Warning: No blocks found at ${url}`); } return blocks; } catch (error) { throw new Error(`Failed to parse blocks from ${url}: ${error instanceof Error ? error.message : String(error)}`); } } export function parseComponentsFromHtml(html: string): string[] { if (!html || typeof html !== 'string') { throw new Error('Invalid HTML content'); } try { const $ = loadCheerio(html); const components = $('a[href^="/docs/components/"]') .map((_, el) => { const href = $(el).attr('href'); return href?.split('/').pop(); }) .get() .filter((name): name is string => Boolean(name)) .sort(); if (components.length === 0) { console.error('Warning: No components found in HTML'); } return components; } catch (error) { throw new Error(`Failed to parse components: ${error instanceof Error ? error.message : String(error)}`); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/heilgar/shadcn-ui-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server