Files
Aidan Bleser 4c6406f135 parity checker cli (#2876)
Co-authored-by: Aidan Bleser <aidanbleser35@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com>
2026-09-10 11:01:13 -07:00

314 lines
9.8 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { injectStyleClasses, parseStyleCss } from "@shadcn-svelte/parity/inject-style-classes";
import { rimraf } from "rimraf";
import { PRESET_STYLES, type PresetConfig } from "shadcn-svelte/preset";
import {
componentsJsonSchema,
registryItemSchema,
registrySchema,
type Registry,
type RegistryItemType,
} from "shadcn-svelte/schema";
import { toJSONSchema } from "zod";
import { buildRegistry } from "./registry.js";
import { getColorsData } from "../src/lib/components/colors/colors.js";
import { THEMES } from "../src/lib/registry/themes.js";
interface BuildRegistryItem {
name: string;
type: string;
files: Array<{
path: string;
}>;
}
const execAsync = promisify(exec);
const INTERNAL_REGISTRY_PATH = path.resolve("src", "lib", "registry");
const REGISTRY_PATH = path.resolve("static", "registry");
const STYLE_TEMP_BASE = path.join(REGISTRY_PATH, "temp", "style");
function writeFileWithDirs(
filePath: string,
data: string,
options: Parameters<typeof fs.writeFileSync>[2] = {}
): void {
// Create directory path if it doesn't exist
const dirname = path.dirname(filePath);
fs.mkdirSync(dirname, { recursive: true });
// Write the file
fs.writeFileSync(filePath, data, options);
}
function log(message: string) {
console.log(`[registry]: ${message}`);
}
export async function build(): Promise<void> {
log("📦 Starting...");
log("🔍 Crawling registry (UI, examples, blocks, hooks, lib, fonts)...");
const registry = await buildRegistry();
log(`✨ Found ${registry.length} registry items`);
log("✅ Validating registry...");
validateRegistry(registry);
// build registry styles (each style gets its own temp dir with cn-* classes resolved)
log("🎨 Building registry styles...");
await Promise.all(
PRESET_STYLES.map(async (style) => {
log(` 📐 Processing style: ${style}`);
const styleTempDir = path.join(STYLE_TEMP_BASE, style);
try {
fs.mkdirSync(styleTempDir, { recursive: true });
const styleRegistry = await rewriteRegistryForStyle(registry, style, styleTempDir);
await buildRegistryJson(styleRegistry, style);
await runRegistryBuild(style);
} finally {
rimraf.sync(styleTempDir);
const registryJsonPath = path.resolve(`registry-${style}.json`);
if (fs.existsSync(registryJsonPath)) {
fs.rmSync(registryJsonPath);
}
}
log(`✅ Style ${style} built`);
})
);
log("✅ Registry styles built");
log("🧹 Cleaning up style temp directory...");
rimraf.sync(STYLE_TEMP_BASE);
// ----------------------------------------------------------------------------
// Build __registry__/blocks.ts
// ----------------------------------------------------------------------------
log("🧩 Building __registry__/blocks.ts...");
rimraf.sync(path.resolve("src", "__registry__"));
let blocksIndex = `
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
export const blocks = [
`; // Creates block index files
for (const block of registry) {
if (block.type !== "registry:block" || block.name.startsWith("chart-")) continue;
blocksIndex += `"${block.name}",`;
}
blocksIndex += "\n] as const;\n";
const blocksPath = path.resolve("src", "__registry__", "blocks.ts");
writeFileWithDirs(blocksPath, blocksIndex);
// ----------------------------------------------------------------------------
// Build __registry__/index.js.
// ----------------------------------------------------------------------------
let index = `
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
export const Index = {`;
// TODO: fix later - we should either add these calendar examples to
// the `lib/registry/examples` dir, or... do something else?
const CALENDAR_EXAMPLES = ["02", "13", "22", "24", "29"].map((n) => `calendar-${n}`);
for (const item of registry as BuildRegistryItem[]) {
if (item.type !== "registry:example" && !CALENDAR_EXAMPLES.includes(item.name)) {
continue;
}
const resolveFiles = item.files.map((file: BuildRegistryItem["files"][0]) =>
file.path.replace("src/", "../")
);
index += `
"${item.name}": {
files: [${resolveFiles.map((filePath: string) => `"${filePath.replaceAll(path.sep, "/")}"`)}],
},`;
}
index += `
}
`;
// Write style index.
const registryPath = path.resolve("src", "__registry__", "index.js");
rimraf.sync(registryPath);
writeFileWithDirs(registryPath, index);
// ----------------------------------------------------------------------------
// Build registry/colors/index.json.
// ----------------------------------------------------------------------------
log("🎨 Building registry/colors...");
const colorsTargetPath = path.join(REGISTRY_PATH, "colors");
rimraf.sync(colorsTargetPath);
if (!fs.existsSync(colorsTargetPath)) {
fs.mkdirSync(colorsTargetPath, { recursive: true });
}
const colorsData = getColorsData();
writeFileWithDirs(
path.join(colorsTargetPath, "index.json"),
JSON.stringify(colorsData, null, "\t"),
"utf-8"
);
// ----------------------------------------------------------------------------
// Build registry/colors/[theme].json
// ----------------------------------------------------------------------------
for (const theme of THEMES) {
writeFileWithDirs(
path.join(REGISTRY_PATH, "colors", `${theme.name}.json`),
JSON.stringify(theme, null, "\t"),
"utf-8"
);
}
// ----------------------------------------------------------------------------
// Build static/schema.json
// ----------------------------------------------------------------------------
log("📋 Writing schema files...");
const componentsJSON = toJSONSchema(componentsJsonSchema);
writeFileWithDirs(
path.resolve("static", "schema.json"),
JSON.stringify(componentsJSON, null, "\t")
);
const SCHEMA_DIR = path.resolve("static", "schema");
writeFileWithDirs(
path.resolve(SCHEMA_DIR, "registry.json"),
JSON.stringify(toJSONSchema(registrySchema), null, "\t")
);
writeFileWithDirs(
path.resolve(SCHEMA_DIR, "registry-item.json"),
JSON.stringify(toJSONSchema(registryItemSchema), null, "\t")
);
log("🎉 Done!");
}
function transformContentWithStyle(content: string, styleMap: Record<string, string>): string {
return injectStyleClasses(content, styleMap);
}
const TEXT_EXTENSIONS = new Set([".svelte", ".ts", ".svelte.ts"]);
async function rewriteRegistryForStyle(
registry: Awaited<ReturnType<typeof buildRegistry>>,
style: PresetConfig["style"],
styleTempDir: string
): Promise<Awaited<ReturnType<typeof buildRegistry>>> {
const styleCssPath = path.resolve(INTERNAL_REGISTRY_PATH, "styles", `style-${style}.css`);
const styleCss = fs.readFileSync(styleCssPath, "utf8");
const styleMap = parseStyleCss(styleCss);
const styleItems = await Promise.all(
registry.map(async (item) => {
if (!item.files?.length) return item;
const styleFiles = await Promise.all(
item.files.map(async (file) => {
const srcPath = path.resolve(file.path);
const tempPath = path.join(styleTempDir, file.path);
const tempDir = path.dirname(tempPath);
fs.mkdirSync(tempDir, { recursive: true });
const ext = path.extname(file.path);
if (TEXT_EXTENSIONS.has(ext)) {
let content = fs.readFileSync(srcPath, "utf8");
content = transformContentWithStyle(content, styleMap);
fs.writeFileSync(tempPath, content, "utf8");
} else {
fs.copyFileSync(srcPath, tempPath);
}
return { ...file, path: path.relative(process.cwd(), tempPath) };
})
);
return { ...item, files: styleFiles };
})
);
return styleItems;
}
function validateRegistry(registry: Awaited<ReturnType<typeof buildRegistry>>) {
const selfReferenced = registry.filter(
(item) => item.registryDependencies?.includes(item.name) ?? false
);
const selfReferenceError = selfReferenced
.map((item) => `Registry item '${item.name}' depends on itself`)
.join("\n");
if (selfReferenceError) {
throw new Error(selfReferenceError);
}
}
async function buildRegistryJson(
registry: Awaited<ReturnType<typeof buildRegistry>>,
style: PresetConfig["style"]
) {
// ----------------------------------------------------------------------------
// Build `registry.json` file.
// ----------------------------------------------------------------------------
const result = registrySchema.parse(
{
$schema: "./static/schema/registry.json",
name: `shadcn-svelte/${style}`,
homepage: "https://shadcn-svelte.com",
aliases: {
lib: "$lib/registry/lib",
ui: "$lib/registry/ui",
components: "./components",
hooks: "$lib/registry/hooks",
utils: "$lib/utils",
},
// TODO: remove when moving from `next` to `latest`
overrideDependencies: ["vaul-svelte@next"],
items: registry,
} as Registry,
// maintains the schema defined property order
{ jitless: true }
);
const ITEM_TYPES: RegistryItemType[] = [
"registry:ui",
"registry:hook",
"registry:style",
"registry:lib",
"registry:block",
"registry:font",
];
const filteredItems = result.items.filter((item) => ITEM_TYPES.includes(item.type));
const registryJsonPath = path.resolve(`registry-${style}.json`);
// This file is consumed by the CLI and immediately deleted — no need to format it.
const registryJson = JSON.stringify({ ...result, items: filteredItems }, null, "\t");
fs.writeFileSync(registryJsonPath, registryJson, "utf8");
}
async function runRegistryBuild(style: PresetConfig["style"]) {
const cwd = process.cwd();
const registryJsonPath = path.resolve(cwd, `registry-${style}.json`);
const outputPath = path.resolve(cwd, "static", "registry", "styles", style);
const workspaceCliPath = path.resolve(cwd, "..", "packages", "cli", "dist", "index.mjs");
await execAsync(
`node "${workspaceCliPath}" registry build "${registryJsonPath}" --output "${outputPath}" -c "${cwd}"`,
{ cwd }
);
}
if (process.argv.includes("build-registry")) {
build();
}