import type { Config } from '@/src/utils/get-config' import type { registryIndexSchema } from '@/src/utils/registry/schema' import { existsSync, promises as fs } from 'node:fs' import { getConfig } from '@/src/utils/get-config' import { handleError } from '@/src/utils/handle-error' import { highlighter } from '@/src/utils/highlighter' import { logger } from '@/src/utils/logger' import { fetchTree, getItemTargetPath, getRegistryBaseColor, getRegistryIndex, } from '@/src/utils/registry' import { transform } from '@/src/utils/transformers' import { Command } from 'commander' import { type Change, diffLines } from 'diff' import path from 'pathe' import { z } from 'zod' const updateOptionsSchema = z.object({ component: z.string().optional(), yes: z.boolean(), cwd: z.string(), path: z.string().optional(), }) export const diff = new Command() .name('diff') .description('check for updates against the registry') .argument('[component]', 'the component name') .option('-y, --yes', 'skip confirmation prompt.', false) .option( '-c, --cwd ', 'the working directory. defaults to the current directory.', process.cwd(), ) .action(async (name, opts) => { try { const options = updateOptionsSchema.parse({ component: name, ...opts, }) const cwd = path.resolve(options.cwd) if (!existsSync(cwd)) { logger.error(`The path ${cwd} does not exist. Please try again.`) process.exit(1) } const config = await getConfig(cwd) if (!config) { logger.warn( `Configuration is missing. Please run ${highlighter.success( `init`, )} to create a components.json file.`, ) process.exit(1) } const registryIndex = await getRegistryIndex() if (!registryIndex) { handleError(new Error('Failed to fetch registry index.')) process.exit(1) } if (!options.component) { const targetDir = config.resolvedPaths.components // Find all components that exist in the project. const projectComponents = registryIndex.filter((item) => { for (const file of item.files ?? []) { const filePath = path.resolve( targetDir, typeof file === 'string' ? file : file.path, ) if (existsSync(filePath)) { return true } } return false }) // Check for updates. const componentsWithUpdates = [] for (const component of projectComponents) { const changes = await diffComponent(component, config) if (changes.length) { componentsWithUpdates.push({ name: component.name, changes, }) } } if (!componentsWithUpdates.length) { logger.info('No updates found.') process.exit(0) } logger.info('The following components have updates available:') for (const component of componentsWithUpdates) { logger.info(`- ${component.name}`) for (const change of component.changes) { logger.info(` - ${change.filePath}`) } } logger.break() logger.info( `Run ${highlighter.success(`diff `)} to see the changes.`, ) process.exit(0) } // Show diff for a single component. const component = registryIndex.find( item => item.name === options.component, ) if (!component) { logger.error( `The component ${highlighter.success( options.component, )} does not exist.`, ) process.exit(1) } const changes = await diffComponent(component, config) if (!changes.length) { logger.info(`No updates found for ${options.component}.`) process.exit(0) } for (const change of changes) { logger.info(`- ${change.filePath}`) await printDiff(change.patch) logger.info('') } } catch (error) { handleError(error) } }) async function diffComponent( component: z.infer[number], config: Config, ) { const payload = await fetchTree(config.style, [component]) const baseColor = await getRegistryBaseColor(config.tailwind.baseColor) if (!payload) { return [] } const changes = [] for (const item of payload) { const targetDir = await getItemTargetPath(config, item) if (!targetDir) { continue } for (const file of item.files ?? []) { const filePath = path.resolve( targetDir, typeof file === 'string' ? file : file.path, ) if (!existsSync(filePath)) { continue } const fileContent = await fs.readFile(filePath, 'utf8') if (typeof file === 'string' || !file.content) { continue } const registryContent = await transform({ filename: file.path, raw: file.content, config, baseColor, }) const patch = diffLines(registryContent as string, fileContent) if (patch.length > 1) { changes.push({ filePath, patch, }) } } } return changes } async function printDiff(diff: Change[]) { diff.forEach((part) => { if (part) { if (part.added) { return process.stdout.write(highlighter.success(part.value)) } if (part.removed) { return process.stdout.write(highlighter.error(part.value)) } return process.stdout.write(part.value) } }) }