chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
import { exec } from "child_process"
|
||||
import { existsSync, promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { rimraf } from "rimraf"
|
||||
|
||||
import { getAllBlocks } from "@/lib/blocks"
|
||||
import { registry } from "@/registry/index"
|
||||
|
||||
async function buildRegistryIndex() {
|
||||
let index = `/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// @ts-nocheck
|
||||
// This file is autogenerated by scripts/build-registry.ts
|
||||
// Do not edit this file directly.
|
||||
import * as React from "react"
|
||||
|
||||
export const Index: Record<string, any> = {`
|
||||
for (const item of registry.items) {
|
||||
const resolveFiles = item.files?.map(
|
||||
(file) => `registry/creative-tim/${file.path}`
|
||||
)
|
||||
if (!resolveFiles) {
|
||||
continue
|
||||
}
|
||||
|
||||
const componentPath = item.files?.[0]?.path
|
||||
? `@/registry/creative-tim/${item.files[0].path}`
|
||||
: ""
|
||||
|
||||
index += `
|
||||
"${item.name}": {
|
||||
name: "${item.name}",
|
||||
description: "${item.description ?? ""}",
|
||||
type: "${item.type}",
|
||||
registryDependencies: ${JSON.stringify(item.registryDependencies)},
|
||||
files: [${item.files?.map((file) => {
|
||||
const filePath = `registry/creative-tim/${typeof file === "string" ? file : file.path}`
|
||||
const resolvedFilePath = path.resolve(filePath)
|
||||
return typeof file === "string"
|
||||
? `"${resolvedFilePath}"`
|
||||
: `{
|
||||
path: "${filePath}",
|
||||
type: "${file.type}",
|
||||
target: "${file.target ?? ""}"
|
||||
}`
|
||||
})}],
|
||||
component: ${
|
||||
componentPath
|
||||
? `React.lazy(async () => {
|
||||
const mod = await import("${componentPath}")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
})`
|
||||
: "null"
|
||||
},
|
||||
categories: ${JSON.stringify(item.categories)},
|
||||
meta: ${JSON.stringify(item.meta)},
|
||||
},`
|
||||
}
|
||||
|
||||
index += `
|
||||
}`
|
||||
|
||||
console.log(`#️⃣ ${Object.keys(registry.items).length} items found`)
|
||||
|
||||
// Write style index.
|
||||
rimraf.sync(path.join(process.cwd(), "registry/__index__.tsx"))
|
||||
await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index)
|
||||
}
|
||||
|
||||
async function buildRegistryJsonFile() {
|
||||
// 1. Add registry/creative-tim prefix for the build to work
|
||||
const fixedRegistry = {
|
||||
...registry,
|
||||
items: registry.items.map((item) => {
|
||||
const files = item.files?.map((file) => {
|
||||
return {
|
||||
...file,
|
||||
path: `registry/creative-tim/${file.path}`,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...item,
|
||||
files,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// 2. Write the content of the registry to `registry.json`
|
||||
rimraf.sync(path.join(process.cwd(), `registry.json`))
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), `registry.json`),
|
||||
JSON.stringify(fixedRegistry, null, 2)
|
||||
)
|
||||
|
||||
// 3. Format the registry.json file.
|
||||
await exec(`prettier --write registry.json`)
|
||||
|
||||
// 3. Copy the registry.json to the www/public/r/styles/creative-tim directory.
|
||||
await fs.cp(
|
||||
path.join(process.cwd(), "registry.json"),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"../www/public/r/styles/creative-tim/registry.json"
|
||||
),
|
||||
{ recursive: true }
|
||||
)
|
||||
}
|
||||
|
||||
async function buildRegistry() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`pnpm dlx shadcn build registry.json --output public/r`,
|
||||
async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error.message}`)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`)
|
||||
}
|
||||
if (stdout) {
|
||||
console.log(stdout)
|
||||
}
|
||||
|
||||
// Post-process the generated JSON files to remove registry/creative-tim prefix
|
||||
console.log("📝 Cleaning up paths in generated JSON files...")
|
||||
await cleanupGeneratedPaths()
|
||||
|
||||
resolve(undefined)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function cleanupGeneratedPaths() {
|
||||
const publicDir = path.join(process.cwd(), "public/r")
|
||||
|
||||
// Get all JSON files in the public/r directory
|
||||
const files = await fs.readdir(publicDir)
|
||||
const jsonFiles = files.filter(file => file.endsWith('.json'))
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
const filePath = path.join(publicDir, file)
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const json = JSON.parse(content)
|
||||
|
||||
// Clean up the paths in the files array
|
||||
if (json.files) {
|
||||
// Build a mapping of source paths to target paths for this block
|
||||
const pathMappings = new Map<string, string>()
|
||||
|
||||
json.files.forEach((file: any) => {
|
||||
if (typeof file === 'object' && file.path && file.target) {
|
||||
// Map the source registry path to the target path
|
||||
const sourcePath = file.path.replace(/^registry\/creative-tim\//, '')
|
||||
pathMappings.set(sourcePath, file.target)
|
||||
}
|
||||
})
|
||||
|
||||
json.files = json.files.map((file: any) => {
|
||||
if (typeof file === 'object' && file.path) {
|
||||
// Remove only registry/ prefix, keep creative-tim/ for branding
|
||||
let cleanPath = file.path.replace(/^registry\//, '')
|
||||
|
||||
// For UI components, prepend with components/
|
||||
if (cleanPath.startsWith('creative-tim/ui/')) {
|
||||
cleanPath = 'components/' + cleanPath.replace(/^creative-tim\//, '')
|
||||
}
|
||||
// For examples, prepend with components/
|
||||
else if (cleanPath.startsWith('creative-tim/examples/')) {
|
||||
cleanPath = 'components/' + cleanPath.replace(/^creative-tim\//, '')
|
||||
}
|
||||
|
||||
file.path = cleanPath
|
||||
|
||||
// Clean up imports in the content field using the path mappings
|
||||
if (file.content) {
|
||||
file.content = rewriteImports(file.content, pathMappings)
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
}
|
||||
|
||||
// Write the cleaned JSON back
|
||||
await fs.writeFile(filePath, JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
console.log(`✨ Cleaned paths in ${jsonFiles.length} files`)
|
||||
}
|
||||
|
||||
function rewriteImports(content: string, pathMappings: Map<string, string>): string {
|
||||
// Sort mappings by path length (longest first) to handle more specific paths first
|
||||
const sortedMappings = Array.from(pathMappings.entries()).sort(
|
||||
([a], [b]) => b.length - a.length
|
||||
)
|
||||
|
||||
// Handle block-specific imports using the exact path mappings
|
||||
sortedMappings.forEach(([sourcePath, targetPath]) => {
|
||||
if (sourcePath.startsWith('blocks/')) {
|
||||
// Remove the file extension from sourcePath to match import statements
|
||||
const sourcePathNoExt = sourcePath.replace(/\.(tsx?|jsx?)$/, '')
|
||||
|
||||
// Extract the directory from the target path (removing filename)
|
||||
const targetPathNoExt = targetPath.replace(/\.(tsx?|jsx?)$/, '')
|
||||
|
||||
// Create regex to match this specific import path
|
||||
const escapedPath = sourcePathNoExt.replace(/\//g, '\\/')
|
||||
const importRegex = new RegExp(
|
||||
`@\\/registry\\/creative-tim\\/${escapedPath}`,
|
||||
'g'
|
||||
)
|
||||
|
||||
content = content.replace(importRegex, `@/${targetPathNoExt}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Rewrite imports from @/registry/creative-tim/ui/... to @/components/ui/...
|
||||
content = content.replace(
|
||||
/@\/registry\/creative-tim\/ui\//g,
|
||||
'@/components/ui/'
|
||||
)
|
||||
|
||||
// Rewrite imports from @/registry/creative-tim/examples/... to @/components/examples/...
|
||||
content = content.replace(
|
||||
/@\/registry\/creative-tim\/examples\//g,
|
||||
'@/components/examples/'
|
||||
)
|
||||
|
||||
// Rewrite imports from @/registry/creative-tim/lib/... to @/lib/...
|
||||
content = content.replace(
|
||||
/@\/registry\/creative-tim\/lib\//g,
|
||||
'@/lib/'
|
||||
)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
async function buildPackageJson(
|
||||
packageType: "ui" | "blocks" | "agents",
|
||||
fileName: string,
|
||||
description: string
|
||||
) {
|
||||
console.log(`🎨 Building ${fileName} for ${packageType}...`)
|
||||
|
||||
// Filter components by type
|
||||
const typeMap = {
|
||||
ui: "registry:ui",
|
||||
blocks: "registry:block",
|
||||
agents: "registry:agent"
|
||||
}
|
||||
|
||||
const components = registry.items.filter(item => item.type === typeMap[packageType])
|
||||
|
||||
// Aggregate all dependencies and registryDependencies
|
||||
const allDependencies = new Set<string>()
|
||||
const allRegistryDependencies = new Set<string>()
|
||||
const allFiles: any[] = []
|
||||
|
||||
// Build path mappings for import rewriting
|
||||
const pathMappings = new Map<string, string>()
|
||||
|
||||
for (const component of components) {
|
||||
// Collect dependencies
|
||||
if (component.dependencies) {
|
||||
component.dependencies.forEach(dep => allDependencies.add(dep))
|
||||
}
|
||||
|
||||
// Collect registryDependencies
|
||||
if (component.registryDependencies) {
|
||||
component.registryDependencies.forEach(dep => allRegistryDependencies.add(dep))
|
||||
}
|
||||
|
||||
// Collect files and read their content
|
||||
if (component.files) {
|
||||
for (const file of component.files) {
|
||||
const filePath = `registry/creative-tim/${file.path}`
|
||||
const absolutePath = path.join(process.cwd(), filePath)
|
||||
|
||||
// Read file content
|
||||
let content = ""
|
||||
if (existsSync(absolutePath)) {
|
||||
content = await fs.readFile(absolutePath, 'utf-8')
|
||||
}
|
||||
|
||||
// Clean path - remove registry/creative-tim/ prefix for UI components
|
||||
let cleanPath = file.path
|
||||
if (packageType === "ui") {
|
||||
if (cleanPath.startsWith('ui/')) {
|
||||
cleanPath = 'components/' + cleanPath
|
||||
} else if (cleanPath.startsWith('examples/')) {
|
||||
cleanPath = 'components/' + cleanPath
|
||||
}
|
||||
}
|
||||
|
||||
// Build path mapping
|
||||
if (file.target) {
|
||||
pathMappings.set(file.path, file.target)
|
||||
}
|
||||
|
||||
allFiles.push({
|
||||
path: cleanPath,
|
||||
type: file.type,
|
||||
target: file.target ?? "",
|
||||
content: content
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite imports in all file contents
|
||||
allFiles.forEach(file => {
|
||||
if (file.content) {
|
||||
file.content = rewriteImports(file.content, pathMappings)
|
||||
}
|
||||
})
|
||||
|
||||
// Create the JSON structure
|
||||
const packageJson = {
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: fileName.replace('.json', ''),
|
||||
type: typeMap[packageType],
|
||||
description: description,
|
||||
dependencies: Array.from(allDependencies).sort(),
|
||||
registryDependencies: Array.from(allRegistryDependencies).sort(),
|
||||
files: allFiles
|
||||
}
|
||||
|
||||
// Write the JSON file
|
||||
const outputPath = path.join(process.cwd(), "public", "r", fileName)
|
||||
await fs.writeFile(outputPath, JSON.stringify(packageJson, null, 2))
|
||||
|
||||
console.log(`✨ Generated ${fileName} with ${components.length} ${packageType} components`)
|
||||
}
|
||||
|
||||
async function buildAllJson() {
|
||||
console.log("📦 Building all.json...")
|
||||
|
||||
// Combine all components
|
||||
const allDependencies = new Set<string>()
|
||||
const allRegistryDependencies = new Set<string>()
|
||||
const allFiles: any[] = []
|
||||
const pathMappings = new Map<string, string>()
|
||||
|
||||
for (const component of registry.items) {
|
||||
if (component.dependencies) {
|
||||
component.dependencies.forEach(dep => allDependencies.add(dep))
|
||||
}
|
||||
if (component.registryDependencies) {
|
||||
component.registryDependencies.forEach(dep => allRegistryDependencies.add(dep))
|
||||
}
|
||||
|
||||
if (component.files) {
|
||||
for (const file of component.files) {
|
||||
const filePath = `registry/creative-tim/${file.path}`
|
||||
const absolutePath = path.join(process.cwd(), filePath)
|
||||
|
||||
let content = ""
|
||||
if (existsSync(absolutePath)) {
|
||||
content = await fs.readFile(absolutePath, 'utf-8')
|
||||
}
|
||||
|
||||
let cleanPath = file.path
|
||||
if (component.type === "registry:ui") {
|
||||
if (cleanPath.startsWith('ui/')) {
|
||||
cleanPath = 'components/' + cleanPath
|
||||
} else if (cleanPath.startsWith('examples/')) {
|
||||
cleanPath = 'components/' + cleanPath
|
||||
}
|
||||
}
|
||||
|
||||
if (file.target) {
|
||||
pathMappings.set(file.path, file.target)
|
||||
}
|
||||
|
||||
allFiles.push({
|
||||
path: cleanPath,
|
||||
type: file.type,
|
||||
target: file.target ?? "",
|
||||
content: content
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (file.content) {
|
||||
file.content = rewriteImports(file.content, pathMappings)
|
||||
}
|
||||
})
|
||||
|
||||
const allJson = {
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: "all",
|
||||
type: "registry:all",
|
||||
description: "All components from Creative Tim UI (UI components, blocks, and agents)",
|
||||
dependencies: Array.from(allDependencies).sort(),
|
||||
registryDependencies: Array.from(allRegistryDependencies).sort(),
|
||||
files: allFiles
|
||||
}
|
||||
|
||||
// Write to public/r/all.json
|
||||
const outputPath = path.join(process.cwd(), "public/r/all.json")
|
||||
await fs.writeFile(outputPath, JSON.stringify(allJson, null, 2))
|
||||
|
||||
console.log(`✨ Generated all.json with ${registry.items.length} total components`)
|
||||
}
|
||||
|
||||
async function buildPackageRegistries() {
|
||||
console.log("📦 Building package-specific registries...")
|
||||
|
||||
// Build UI components registry
|
||||
await buildPackageJson(
|
||||
"ui",
|
||||
"ui.json",
|
||||
"All UI components from @creative-tim/ui"
|
||||
)
|
||||
|
||||
// Build blocks registry
|
||||
await buildPackageJson(
|
||||
"blocks",
|
||||
"blocks.json",
|
||||
"All blocks from @creative-tim/blocks"
|
||||
)
|
||||
|
||||
// Future: Build agents registry
|
||||
// await buildPackageJson(
|
||||
// "agents",
|
||||
// "agents.json",
|
||||
// "All agent components from @creative-tim/agents"
|
||||
// )
|
||||
}
|
||||
|
||||
async function buildBlocksIndex() {
|
||||
const blocks = await getAllBlocks(["registry:block"])
|
||||
|
||||
const payload = blocks.map((block) => ({
|
||||
name: block.name,
|
||||
description: block.description,
|
||||
categories: block.categories,
|
||||
}))
|
||||
|
||||
rimraf.sync(path.join(process.cwd(), "registry/__blocks__.json"))
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), "registry/__blocks__.json"),
|
||||
JSON.stringify(payload, null, 2)
|
||||
)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
exec(`prettier --write registry/__blocks__.json`, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(undefined)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🗂️ Building registry/__index__.tsx...")
|
||||
await buildRegistryIndex()
|
||||
|
||||
console.log("🗂️ Building registry/__blocks__.json...")
|
||||
await buildBlocksIndex()
|
||||
|
||||
console.log("💅 Building registry.json...")
|
||||
await buildRegistryJsonFile()
|
||||
|
||||
console.log("🏗️ Building registry...")
|
||||
await buildRegistry()
|
||||
|
||||
console.log("📦 Building package registries...")
|
||||
await buildPackageRegistries()
|
||||
|
||||
console.log("📦 Building all.json...")
|
||||
await buildAllJson()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { existsSync } from "fs"
|
||||
import path from "path"
|
||||
import puppeteer from "puppeteer"
|
||||
|
||||
import { getAllBlockIds } from "../lib/blocks"
|
||||
|
||||
const REGISTRY_PATH = path.join(process.cwd(), "public/r")
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Capture screenshots.
|
||||
// ----------------------------------------------------------------------------
|
||||
async function captureScreenshots() {
|
||||
const blockIds = await getAllBlockIds()
|
||||
const blocks = blockIds.filter((block) => {
|
||||
// Check if screenshots already exist
|
||||
const lightPath = path.join(REGISTRY_PATH, `${block}-light.png`)
|
||||
const darkPath = path.join(REGISTRY_PATH, `${block}-dark.png`)
|
||||
return !existsSync(lightPath) || !existsSync(darkPath)
|
||||
})
|
||||
|
||||
if (blocks.length === 0) {
|
||||
console.log("✨ All screenshots exist, nothing to capture")
|
||||
return
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
defaultViewport: {
|
||||
width: 1440,
|
||||
height: 900,
|
||||
deviceScaleFactor: 2,
|
||||
},
|
||||
})
|
||||
|
||||
for (const block of blocks) {
|
||||
const pageUrl = `http://localhost:4000/view/${block}`
|
||||
|
||||
const page = await browser.newPage()
|
||||
await page.goto(pageUrl, {
|
||||
waitUntil: "networkidle2",
|
||||
})
|
||||
|
||||
console.log(`- Capturing ${block}...`)
|
||||
|
||||
for (const theme of ["light", "dark"]) {
|
||||
const screenshotPath = path.join(
|
||||
REGISTRY_PATH,
|
||||
`${block}${theme === "dark" ? "-dark" : "-light"}.png`
|
||||
)
|
||||
|
||||
if (existsSync(screenshotPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set theme and reload page
|
||||
await page.evaluate((currentTheme) => {
|
||||
localStorage.setItem("theme", currentTheme)
|
||||
}, theme)
|
||||
|
||||
await page.reload({ waitUntil: "networkidle2" })
|
||||
|
||||
// Wait for animations to complete
|
||||
if (block.startsWith("chart") || block.startsWith("dashboard")) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
// Hide Tailwind indicator
|
||||
await page.evaluate(() => {
|
||||
const indicator = document.querySelector("[data-tailwind-indicator]")
|
||||
if (indicator) {
|
||||
indicator.remove()
|
||||
}
|
||||
})
|
||||
|
||||
await page.screenshot({
|
||||
path: screenshotPath,
|
||||
})
|
||||
}
|
||||
|
||||
await page.close()
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🔍 Capturing screenshots...")
|
||||
|
||||
await captureScreenshots()
|
||||
|
||||
console.log("✅ Done!")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { registrySchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
const registriesIndexSchema = z.record(
|
||||
z.string().regex(/^@[a-zA-Z0-9][a-zA-Z0-9-_]*$/),
|
||||
z.string().refine((url) => url.includes("{name}"))
|
||||
)
|
||||
|
||||
async function main() {
|
||||
// 1. Validate the registries.json file.
|
||||
const registriesFile = path.join(process.cwd(), "public/r/registries.json")
|
||||
const content = await fs.readFile(registriesFile, "utf-8")
|
||||
const data = JSON.parse(content)
|
||||
const registries = registriesIndexSchema.parse(data)
|
||||
|
||||
// 2. Validate each registry endpoint.
|
||||
const errors: string[] = []
|
||||
for (const [name, url] of Object.entries(registries)) {
|
||||
try {
|
||||
const testUrl = url.replace("{name}", "registry")
|
||||
const response = await fetch(testUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
errors.push(`${name}: HTTP ${response.status}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
registrySchema.parse(json)
|
||||
console.log(`✅ ${name}`)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
errors.push(`${name}: ${error.message}`)
|
||||
continue
|
||||
}
|
||||
|
||||
errors.push(
|
||||
`${name}: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error("\n❌ Validation failed:")
|
||||
errors.forEach((err) => console.error(` ${err}`))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("\n✅ All registries passed validation.")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("❌ Error:", error instanceof Error ? error.message : error)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user