287 lines
9.3 KiB
TypeScript
287 lines
9.3 KiB
TypeScript
import { exec } from 'child_process';
|
|
import { promises as fs } from 'fs';
|
|
import path from 'path';
|
|
import { rimraf } from 'rimraf';
|
|
|
|
const REGISTRY_JSON_PATH = path.join(
|
|
process.cwd(),
|
|
'public',
|
|
'r',
|
|
'registry.json',
|
|
);
|
|
|
|
/**
|
|
* Replace registry paths with component paths.
|
|
* @param inputStr - The input string to process.
|
|
* @returns The processed string with registry paths replaced.
|
|
*/
|
|
function replaceRegistryPaths(inputStr: string): string {
|
|
return inputStr.replace(/(['"])([\s\S]*?)\1/g, (match, quote, content) => {
|
|
if (content.startsWith('@/registry/')) {
|
|
const rest = content.slice('@/registry/'.length);
|
|
return `${quote}@/components/animate-ui/${rest}${quote}`;
|
|
} else if (content.startsWith('@workspace/ui/')) {
|
|
const rest = content.slice('@workspace/ui/'.length);
|
|
return `${quote}@/${rest}${quote}`;
|
|
}
|
|
return match;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Function to build the merged registry.json file.
|
|
* It searches for all registry-item.json files in the registry directory,
|
|
* removes the $schema property, and merges them into the base registry.json items array.
|
|
*/
|
|
async function buildRegistryFile() {
|
|
const registryJsonContent = await fs.readFile(REGISTRY_JSON_PATH, 'utf-8');
|
|
const registryData = JSON.parse(registryJsonContent);
|
|
const registryFolderPath = path.join(process.cwd(), 'registry');
|
|
const newItems = await getRegistryItemsFromFolder(registryFolderPath);
|
|
|
|
registryData.items = [
|
|
{
|
|
name: 'index',
|
|
type: 'registry:style',
|
|
dependencies: [
|
|
'tw-animate-css',
|
|
'class-variance-authority',
|
|
'lucide-react',
|
|
],
|
|
registryDependencies: ['utils'],
|
|
cssVars: {},
|
|
files: [],
|
|
},
|
|
...newItems,
|
|
];
|
|
|
|
await fs.writeFile(REGISTRY_JSON_PATH, JSON.stringify(registryData, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Recursively search for registry-item.json files in a given directory.
|
|
* @param dir - Directory to search in.
|
|
* @returns An array of registry item objects.
|
|
*/
|
|
async function getRegistryItemsFromFolder(dir: string) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const items: any[] = [];
|
|
// Read directory entries with file type information
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
// Define the expected path of registry-item.json in the current directory
|
|
const registryItemPath = path.join(fullPath, 'registry-item.json');
|
|
try {
|
|
// Check if registry-item.json exists in this directory
|
|
await fs.access(registryItemPath);
|
|
// Read and parse the registry item file
|
|
const content = await fs.readFile(registryItemPath, 'utf-8');
|
|
const item = JSON.parse(content);
|
|
// Remove the $schema property if it exists
|
|
if (item.$schema) {
|
|
delete item.$schema;
|
|
}
|
|
items.push(item);
|
|
} catch {
|
|
// If registry-item.json does not exist in the current directory,
|
|
// recursively search in the subdirectories
|
|
const subItems = await getRegistryItemsFromFolder(fullPath);
|
|
items.push(...subItems);
|
|
}
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/**
|
|
* Function to build the registry index file.
|
|
* This function reads the registry.json items and builds a dynamic index file.
|
|
*/
|
|
async function buildRegistryIndex() {
|
|
const registryJsonContent = await fs.readFile(REGISTRY_JSON_PATH, 'utf-8');
|
|
const registryItems = JSON.parse(registryJsonContent);
|
|
|
|
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.mts
|
|
// Do not edit this file directly.
|
|
import * as React from "react"
|
|
|
|
export const index: Record<string, any> = {`;
|
|
|
|
// Remove duplicates: only keep the last item with a given name
|
|
const uniqueItemsMap = new Map<string, (typeof registryItems.items)[0]>();
|
|
// Use the base items from registry.json merged file
|
|
for (const item of registryItems.items) {
|
|
if (uniqueItemsMap.has(item.name)) {
|
|
console.warn(
|
|
`Duplicate item name detected: ${item.name}. Overwriting previous entry.`,
|
|
);
|
|
}
|
|
uniqueItemsMap.set(item.name, item);
|
|
}
|
|
|
|
// Process only unique items
|
|
for (const item of uniqueItemsMap.values()) {
|
|
// Skip items without files
|
|
if (!item.files) continue;
|
|
|
|
console.log('Processing item:', item.name);
|
|
// Define the component path from the first file if exists
|
|
const componentPath = item.files[0]?.path ? `@/${item.files[0].path}` : '';
|
|
|
|
// Read files and add content preserving newlines
|
|
const filesWithContent = await Promise.all(
|
|
item.files.map(async (file: any) => {
|
|
const filePath = typeof file === 'string' ? file : file.path;
|
|
const resolvedFilePath = path.resolve(filePath);
|
|
|
|
try {
|
|
// Read the file content (preserving newlines)
|
|
const content = await fs.readFile(resolvedFilePath, 'utf-8');
|
|
const processedContent = replaceRegistryPaths(content).trim(); // Trim leading/trailing spaces
|
|
|
|
return {
|
|
path: filePath,
|
|
type: file.type || 'unknown',
|
|
target: file.target || '',
|
|
content: processedContent, // Keep original formatting (newlines will be \n in JSON)
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error reading file ${filePath}:`, error);
|
|
return {
|
|
path: filePath,
|
|
type: file.type || 'unknown',
|
|
target: file.target || '',
|
|
content: '',
|
|
};
|
|
}
|
|
}),
|
|
);
|
|
|
|
index += `
|
|
"${item.name}": {
|
|
name: ${JSON.stringify(item.name)},
|
|
description: ${JSON.stringify(item.description ?? '')},
|
|
type: "${item.type}",
|
|
dependencies: ${JSON.stringify(item.dependencies)},
|
|
devDependencies: ${JSON.stringify(item.devDependencies)},
|
|
registryDependencies: ${JSON.stringify(item.registryDependencies)},
|
|
files: ${JSON.stringify(filesWithContent, null, 2)},
|
|
keywords: ${JSON.stringify(item.meta?.keywords ?? [])},
|
|
component: ${
|
|
componentPath
|
|
? `(function() {
|
|
const LazyComp = 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}";
|
|
const Comp = mod.default || mod[exportName];
|
|
if (mod.animations) {
|
|
(LazyComp as any).animations = mod.animations;
|
|
}
|
|
return { default: Comp };
|
|
});
|
|
LazyComp.demoProps = ${JSON.stringify(item?.meta?.demoProps ?? {})};
|
|
return LazyComp;
|
|
})()`
|
|
: 'null'
|
|
},
|
|
command: 'https://animate-ui.com/r/${item.name}',
|
|
},`;
|
|
}
|
|
|
|
index += `
|
|
}`;
|
|
|
|
// Remove the previous registry index file and write the new one.
|
|
rimraf.sync(path.join(process.cwd(), '__registry__/index.tsx'));
|
|
await fs.writeFile(path.join(process.cwd(), '__registry__/index.tsx'), index);
|
|
}
|
|
|
|
/**
|
|
* Function to build the registry.
|
|
* It clears the previous registry directory, builds the registry files,
|
|
* and replaces specific path strings in the generated files.
|
|
*/
|
|
async function buildRegistry() {
|
|
// 1. Ensure 'public/r' exists
|
|
await fs.mkdir('public/r', { recursive: true });
|
|
|
|
// 2. Remove everything except registry.json
|
|
const entries = await fs.readdir('public/r');
|
|
await Promise.all(
|
|
entries.map(async (entry) => {
|
|
if (entry === 'registry.json') return;
|
|
const entryPath = path.join('public/r', entry);
|
|
await fs.rm(entryPath, { recursive: true, force: true });
|
|
}),
|
|
);
|
|
|
|
// 2. Build the registry using the shadcn build command
|
|
await new Promise((resolve, reject) => {
|
|
const process = exec(
|
|
`pnpm dlx shadcn build public/r/registry.json --output ./public/r/`,
|
|
);
|
|
|
|
process.on('exit', (code) => {
|
|
if (code === 0) {
|
|
resolve(undefined);
|
|
} else {
|
|
reject(new Error(`Process exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
// 3. Replace `@/registry/animate-ui/` with `@/components/animate-ui/` in all files
|
|
const files = await fs.readdir(path.join(process.cwd(), 'public/r'));
|
|
|
|
await Promise.all(
|
|
files.map(async (file) => {
|
|
const content = await fs.readFile(
|
|
path.join(process.cwd(), 'public/r', file),
|
|
'utf-8',
|
|
);
|
|
|
|
const registryItem = JSON.parse(content);
|
|
|
|
// Replace `@/registry` in file contents
|
|
registryItem.files = registryItem.files?.map((file: any) => {
|
|
if (
|
|
file.content?.includes('@/registry') ||
|
|
file.content?.includes('@workspace/ui/')
|
|
) {
|
|
file.content = replaceRegistryPaths(file.content);
|
|
}
|
|
return file;
|
|
});
|
|
|
|
// Write the updated file back to disk
|
|
await fs.writeFile(
|
|
path.join(process.cwd(), 'public/r', file),
|
|
JSON.stringify(registryItem, null, 2),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Execute the build process in the following order:
|
|
// 1. Build the merged registry.json file with new items from registry-item.json files.
|
|
// 2. Build the registry index.
|
|
// 3. Build the registry.
|
|
try {
|
|
console.log('🔨 Building merged registry file...');
|
|
await buildRegistryFile();
|
|
console.log('🗂️ Building registry/__index__.tsx...');
|
|
await buildRegistryIndex();
|
|
console.log('🏗️ Building registry...');
|
|
await buildRegistry();
|
|
} catch (error) {
|
|
console.error(error);
|
|
process.exit(1);
|
|
}
|