mirror of
https://github.com/huntabyte/shadcn-svelte.git
synced 2026-09-14 19:33:46 +08:00
4c6406f135
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>
314 lines
9.8 KiB
TypeScript
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();
|
|
}
|