Fortura/apps/www/scripts/build-registry.mts
2025-08-20 04:12:49 -06:00

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);
}