-
Notifications
You must be signed in to change notification settings - Fork 0
Add Shadcn component sync tooling and analysis #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ools Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
✅ All checks passed!
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds a Shadcn UI sync toolset to compare ObjectUI base components against the latest Shadcn registry versions, supporting both online updates and offline analysis, with docs and a scheduled GitHub Action.
Changes:
- Added an online sync CLI (
scripts/shadcn-sync.js) to check/diff/update components from the Shadcn registry. - Added an offline analysis CLI (
scripts/component-analysis.js) plus a component manifest (packages/components/shadcn-components.json) to track Shadcn vs custom components. - Added documentation and a scheduled GitHub Action to automate periodic checks, plus new root npm scripts to run the tooling.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/shadcn-sync.js | New online check/diff/update script to sync components from Shadcn registry. |
| scripts/component-analysis.js | New offline analysis script to classify component customization and summarize deps. |
| packages/components/shadcn-components.json | Manifest defining tracked Shadcn components and custom ObjectUI components. |
| packages/components/README_SHADCN_SYNC.md | Component-package documentation for the sync/analysis workflow. |
| packages/components/README.md | Adds “Keeping Components Updated” section and references sync docs. |
| package.json | Adds ESM mode and new shadcn:* scripts for running the tooling. |
| docs/SHADCN_SYNC.md | Full sync guide documentation. |
| docs/SHADCN_QUICK_START.md | Quick-start documentation for common workflows. |
| docs/COMPONENT_SYNC_SUMMARY.md | Bilingual summary of the tooling and workflow. |
| .github/workflows/shadcn-check.yml | Scheduled workflow to run analysis/check and publish results. |
| function log(message, color = 'white') { | ||
| console.log(`${colors[color]}${message}${colors.reset}`); | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log() always wraps output in ANSI color codes, which will leak escape sequences into redirected output files in CI (e.g., > analysis.txt). Consider disabling colors when !process.stdout.isTTY / NO_COLOR is set, or add a --no-color option.
| } else if (line.includes('from "') && !line.includes('react')) { | ||
| const match = line.match(/from ["']([^"']+)/); | ||
| if (match && !match[1].startsWith('.')) { | ||
| analysis.imports.external.push(match[1]); |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This external import detection excludes any line containing the substring react, which will incorrectly drop dependencies like react-hook-form and react-day-picker from the report. Consider only excluding the actual React import (from "react") instead of substring matching.
| } else if (line.includes('from "') && !line.includes('react')) { | |
| const match = line.match(/from ["']([^"']+)/); | |
| if (match && !match[1].startsWith('.')) { | |
| analysis.imports.external.push(match[1]); | |
| } else if (line.includes('from "')) { | |
| const match = line.match(/from ["']([^"']+)/); | |
| if (match) { | |
| const moduleName = match[1]; | |
| if (!moduleName.startsWith('.') && moduleName !== 'react') { | |
| analysis.imports.external.push(moduleName); | |
| } |
| # Check component status | ||
| pnpm shadcn:check | ||
|
|
||
| # Update a specific component | ||
| pnpm shadcn:update button | ||
|
|
||
| # Update all components | ||
| pnpm shadcn:update-all | ||
|
|
||
| # Show diff for a component | ||
| pnpm shadcn:diff button | ||
|
|
||
| # List all components | ||
| pnpm shadcn:list |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The examples use pnpm shadcn:*, but those scripts are defined in the workspace root package.json, not in packages/components/package.json. Clarify that commands should be run from the repo root, or use pnpm -w shadcn:check / pnpm --workspace-root ... in the examples.
| # Check component status | |
| pnpm shadcn:check | |
| # Update a specific component | |
| pnpm shadcn:update button | |
| # Update all components | |
| pnpm shadcn:update-all | |
| # Show diff for a component | |
| pnpm shadcn:diff button | |
| # List all components | |
| pnpm shadcn:list | |
| # NOTE: These scripts are defined in the workspace root package.json. | |
| # Use -w / --workspace-root so they run from anywhere in the monorepo. | |
| # Check component status | |
| pnpm -w shadcn:check | |
| # Update a specific component | |
| pnpm -w shadcn:update button | |
| # Update all components | |
| pnpm -w shadcn:update-all | |
| # Show diff for a component | |
| pnpm -w shadcn:diff button | |
| # List all components | |
| pnpm -w shadcn:list |
| - `input-group` - Input with addons | ||
| - `input-otp` - OTP input (from Shadcn but customized) | ||
| - `item` - Generic item component | ||
| - `kbd` - Keyboard key display | ||
| - `spinner` - Loading spinner |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
input-otp is listed under “Custom ObjectUI Components”, but it’s tracked as a Shadcn component in packages/components/shadcn-components.json (and listed as Shadcn-based in packages/components/README_SHADCN_SYNC.md). Please align the docs/manifest so it’s clear whether input-otp is updatable from Shadcn or not.
| async function fetchUrl(url) { | ||
| return new Promise((resolve, reject) => { | ||
| https.get(url, (res) => { | ||
| let data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => { | ||
| try { | ||
| resolve(JSON.parse(data)); | ||
| } catch (e) { | ||
| resolve(data); | ||
| } | ||
| }); | ||
| }).on('error', reject); |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchUrl doesn’t check res.statusCode (or handle redirects), so HTML/404 responses can be treated as successful fetches. This can make --check produce incorrect results instead of surfacing a network/registry error. Consider rejecting on non-2xx and validating the parsed response shape before returning it.
| async function fetchUrl(url) { | |
| return new Promise((resolve, reject) => { | |
| https.get(url, (res) => { | |
| let data = ''; | |
| res.on('data', (chunk) => { data += chunk; }); | |
| res.on('end', () => { | |
| try { | |
| resolve(JSON.parse(data)); | |
| } catch (e) { | |
| resolve(data); | |
| } | |
| }); | |
| }).on('error', reject); | |
| async function fetchUrl(url, redirectCount = 0) { | |
| const MAX_REDIRECTS = 5; | |
| return new Promise((resolve, reject) => { | |
| https | |
| .get(url, (res) => { | |
| const { statusCode, headers } = res; | |
| let data = ''; | |
| res.on('data', (chunk) => { | |
| data += chunk; | |
| }); | |
| res.on('end', () => { | |
| // Handle HTTP redirects (3xx) | |
| if ( | |
| statusCode && | |
| statusCode >= 300 && | |
| statusCode < 400 && | |
| headers.location | |
| ) { | |
| if (redirectCount >= MAX_REDIRECTS) { | |
| return reject( | |
| new Error( | |
| `Too many redirects while fetching ${url} (>${MAX_REDIRECTS})` | |
| ) | |
| ); | |
| } | |
| const nextUrl = new URL(headers.location, url).toString(); | |
| return resolve(fetchUrl(nextUrl, redirectCount + 1)); | |
| } | |
| // Reject on non-2xx status codes | |
| if (!statusCode || statusCode < 200 || statusCode >= 300) { | |
| const snippet = data && typeof data === 'string' | |
| ? data.slice(0, 200) | |
| : ''; | |
| return reject( | |
| new Error( | |
| `Request to ${url} failed with status ${statusCode} ${res.statusMessage || ''}${ | |
| snippet ? ` - Response: ${snippet}` : '' | |
| }` | |
| ) | |
| ); | |
| } | |
| // Successful 2xx response: try JSON, fall back to raw text | |
| try { | |
| resolve(JSON.parse(data)); | |
| } catch { | |
| resolve(data); | |
| } | |
| }); | |
| }) | |
| .on('error', reject); |
|
|
||
| ## 📚 Learn More | ||
|
|
||
| - [Full Sync Guide](../docs/SHADCN_SYNC.md) - Comprehensive documentation |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Full Sync Guide” link points to ../docs/SHADCN_SYNC.md, but this file already lives under docs/. That relative link will resolve to a non-existent path when viewed on GitHub. Use ./SHADCN_SYNC.md (or SHADCN_SYNC.md) instead.
| - [Full Sync Guide](../docs/SHADCN_SYNC.md) - Comprehensive documentation | |
| - [Full Sync Guide](./SHADCN_SYNC.md) - Comprehensive documentation |
| function log(message, color = 'white') { | ||
| console.log(`${colors[color]}${message}${colors.reset}`); | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This script always prints ANSI color codes via log(). When output is redirected in CI (e.g., > analysis.txt), the file will contain escape sequences. Consider disabling colors when !process.stdout.isTTY / NO_COLOR is set, or add a --no-color flag.
| // Simple heuristic: check if significantly different | ||
| const lineDiff = Math.abs(localLines - shadcnLines); | ||
| const isDifferent = lineDiff > 10 || localContent.includes('ObjectUI') || localContent.includes('data-slot'); | ||
|
|
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--check will mark every tracked component as modified because the heuristic treats the standard ObjectUI copyright header (localContent.includes('ObjectUI')) as a modification. Since all components include this header, the synced state becomes unreachable. Consider stripping the known header block before comparison (and optionally also ignoring data-slot if it’s an expected ObjectUI delta), or switch to a real diff (e.g., normalize headers/imports and compare hashes).
| async function createBackup(componentName) { | ||
| await fs.mkdir(BACKUP_DIR, { recursive: true }); | ||
| const sourcePath = path.join(COMPONENTS_DIR, `${componentName}.tsx`); | ||
| const backupPath = path.join(BACKUP_DIR, `${componentName}.tsx.${Date.now()}.backup`); | ||
|
|
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
componentName is used directly to construct sourcePath/backupPath via path.join(..., ${componentName}.tsx). If --update receives a value with path separators (e.g., ../../x), this can copy/write outside the intended directory. Validate component names (e.g., allowlist from manifest and/or restrict to [a-z0-9-]+) and ensure the resolved path stays within COMPONENTS_DIR/BACKUP_DIR.
| body += '2. Run `pnpm shadcn:analyze` locally for detailed information\n'; | ||
| body += '3. Update components as needed with `pnpm shadcn:update <component>`\n'; | ||
| body += '4. See [SHADCN_SYNC.md](../blob/main/docs/SHADCN_SYNC.md) for detailed guide\n\n'; | ||
| body += '> This issue was automatically created by the Shadcn Components Check workflow.\n'; |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generated issue body links to ../blob/main/docs/SHADCN_SYNC.md, which is a relative URL and won’t reliably resolve from a GitHub issue. Prefer an absolute URL (can be constructed from context.repo) to the docs file.
ObjectUI components are based on Shadcn UI but lack tooling to track versions and sync updates. This adds automated comparison and update capabilities.
Implementation
Component Manifest (
shadcn-components.json)Dual-mode Analysis
shadcn-sync.jsfetches from Shadcn registry, updates components with backupscomponent-analysis.jsdetects customizations without network accessNPM Commands
Analysis Results
Components categorized by update safety:
calendar,sonner,table,toastcard,form,label,skeleton,tabsKey customizations detected:
data-slotattributes across 41 componentsbackdrop-blur, gradients)icon-sm,icon-lg)Example Usage
Documentation
docs/README_SHADCN_TOOLS.md- entry pointdocs/SHADCN_QUICK_START.md- common workflowsdocs/SHADCN_DEMO.md- real-world examplesdocs/COMPONENT_SYNC_SUMMARY.md- bilingual (EN/中文)Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
ui.shadcn.com/usr/local/bin/node node scripts/shadcn-sync.js --check(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.