shadcn-vue
Version:
Add components to your apps.
1,038 lines (1,025 loc) • 37.5 kB
JavaScript
import path from "pathe";
import { z } from "zod";
import { promises } from "node:fs";
import fs from "fs-extra";
import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
import { glob } from "tinyglobby";
import { cosmiconfig } from "cosmiconfig";
import { colors } from "consola/utils";
import consola from "consola";
import ora from "ora";
import deepmerge from "deepmerge";
import { ofetch } from "ofetch";
import { ProxyAgent } from "undici";
import { tmpdir } from "node:os";
import objectToString from "stringify-object";
import { Project, QuoteKind, ScriptKind, SyntaxKind } from "ts-morph";
//#region src/utils/frameworks.ts
const FRAMEWORKS = {
vite: {
name: "vite",
label: "Vite",
links: {
installation: "https://shadcn-vue.com/docs/installation/vite",
tailwind: "https://tailwindcss.com/docs/guides/vite"
}
},
nuxt: {
name: "nuxt",
label: "Nuxt",
links: {
installation: "https://shadcn-vue.com/docs/installation/nuxt",
tailwind: "https://tailwindcss.com/docs/guides/nuxtjs"
}
},
astro: {
name: "astro",
label: "Astro",
links: {
installation: "https://shadcn-vue.com/docs/installation/astro",
tailwind: "https://tailwindcss.com/docs/guides/astro"
}
},
laravel: {
name: "laravel",
label: "Laravel",
links: {
installation: "https://shadcn-vue.com/docs/installation/laravel",
tailwind: "https://tailwindcss.com/docs/guides/laravel"
}
},
manual: {
name: "manual",
label: "Manual",
links: {
installation: "https://shadcn-vue.com/docs/installation/manual",
tailwind: "https://tailwindcss.com/docs/installation"
}
}
};
//#endregion
//#region src/utils/resolve-import.ts
function resolveImport(importPath, config) {
const matcher = createPathsMatcher(config);
if (matcher === null) return;
const paths = matcher(importPath);
return paths[0];
}
//#endregion
//#region src/utils/highlighter.ts
const highlighter = {
error: colors.red,
warn: colors.yellow,
info: colors.cyan,
success: colors.green
};
//#endregion
//#region src/utils/get-config.ts
/** @deprecated */
const TAILWIND_CSS_PATH = {
nuxt: "assets/css/tailwind.css",
vite: "src/assets/index.css",
laravel: "resources/css/app.css",
astro: "src/styles/globals.css"
};
const DEFAULT_COMPONENTS = "@/components";
const DEFAULT_UTILS = "@/lib/utils";
const DEFAULT_TAILWIND_CSS = TAILWIND_CSS_PATH.nuxt;
const DEFAULT_TAILWIND_CONFIG = "tailwind.config.js";
const explorer = cosmiconfig("components", { searchPlaces: ["components.json"] });
const rawConfigSchema = z.object({
$schema: z.string().optional(),
style: z.string(),
typescript: z.boolean().default(true),
tailwind: z.object({
config: z.string().optional(),
css: z.string(),
baseColor: z.string(),
cssVariables: z.boolean().default(true),
prefix: z.string().default("").optional()
}),
aliases: z.object({
components: z.string(),
composables: z.string().optional(),
utils: z.string(),
ui: z.string().optional(),
lib: z.string().optional()
}),
iconLibrary: z.string().optional()
}).strict();
const configSchema = rawConfigSchema.extend({ resolvedPaths: z.object({
cwd: z.string(),
tailwindConfig: z.string(),
tailwindCss: z.string(),
utils: z.string(),
components: z.string(),
composables: z.string(),
lib: z.string(),
ui: z.string()
}) });
const workspaceConfigSchema = z.record(configSchema);
async function getConfig(cwd) {
const config = await getRawConfig(cwd);
if (!config) return null;
if (!config.iconLibrary) config.iconLibrary = config.style === "new-york" ? "radix" : "lucide";
return await resolveConfigPaths(cwd, config);
}
function getTSConfig(cwd, tsconfigName) {
const parsedConfig = getTsconfig(path.resolve(cwd, "package.json"), tsconfigName);
if (parsedConfig === null) throw new Error(`Failed to find ${highlighter.info(tsconfigName)}`);
return parsedConfig;
}
async function resolveConfigPaths(cwd, config) {
const tsconfigType = config.typescript ? "tsconfig.json" : "jsconfig.json";
const tsConfig = getTSConfig(cwd, tsconfigType);
return configSchema.parse({
...config,
resolvedPaths: {
cwd,
tailwindConfig: config.tailwind.config ? path.resolve(cwd, config.tailwind.config) : "",
tailwindCss: path.resolve(cwd, config.tailwind.css),
utils: await resolveImport(config.aliases.utils, tsConfig),
components: await resolveImport(config.aliases.components, tsConfig),
ui: config.aliases.ui ? await resolveImport(config.aliases.ui, tsConfig) : path.resolve(await resolveImport(config.aliases.components, tsConfig) ?? cwd, "ui"),
lib: config.aliases.lib ? await resolveImport(config.aliases.lib, tsConfig) : path.resolve(await resolveImport(config.aliases.utils, tsConfig) ?? cwd, ".."),
composables: config.aliases.composables ? await resolveImport(config.aliases.composables, tsConfig) : path.resolve(await resolveImport(config.aliases.components, tsConfig) ?? cwd, "..", "composables")
}
});
}
async function getRawConfig(cwd) {
try {
const configResult = await explorer.search(cwd);
if (!configResult) return null;
return rawConfigSchema.parse(configResult.config);
} catch (error) {
throw new Error(`Invalid configuration found in ${cwd}/components.json.`);
}
}
async function getWorkspaceConfig(config) {
let resolvedAliases = {};
for (const key of Object.keys(config.aliases)) {
if (!isAliasKey(key, config)) continue;
const resolvedPath = config.resolvedPaths[key];
const packageRoot = await findPackageRoot(config.resolvedPaths.cwd, resolvedPath);
if (!packageRoot) {
resolvedAliases[key] = config;
continue;
}
resolvedAliases[key] = await getConfig(packageRoot);
}
const result = workspaceConfigSchema.safeParse(resolvedAliases);
if (!result.success) return null;
return result.data;
}
async function findPackageRoot(cwd, resolvedPath) {
const commonRoot = findCommonRoot(cwd, resolvedPath);
const relativePath = path.relative(commonRoot, resolvedPath);
const packageRoots = await glob("**/package.json", {
cwd: commonRoot,
deep: 3,
ignore: [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/public/**"
]
});
const matchingPackageRoot = packageRoots.map((pkgPath) => path.dirname(pkgPath)).find((pkgDir) => relativePath.startsWith(pkgDir));
return matchingPackageRoot ? path.join(commonRoot, matchingPackageRoot) : null;
}
function isAliasKey(key, config) {
return Object.keys(config.resolvedPaths).filter((key$1) => key$1 !== "utils").includes(key);
}
function findCommonRoot(cwd, resolvedPath) {
const parts1 = cwd.split(path.sep);
const parts2 = resolvedPath.split(path.sep);
const commonParts = [];
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
if (parts1[i] !== parts2[i]) break;
commonParts.push(parts1[i]);
}
return commonParts.join(path.sep);
}
async function getTargetStyleFromConfig(cwd, fallback) {
const projectInfo = await getProjectInfo(cwd);
return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback;
}
//#endregion
//#region src/utils/get-package-info.ts
function getPackageInfo(cwd = "", shouldThrow = true) {
const packageJsonPath = path.join(cwd, "package.json");
return fs.readJSONSync(packageJsonPath, { throws: shouldThrow });
}
//#endregion
//#region src/utils/get-project-info.ts
const PROJECT_SHARED_IGNORE = [
"**/node_modules/**",
".nuxt",
"public",
"dist",
"build"
];
const TS_CONFIG_SCHEMA = z.object({ compilerOptions: z.object({ paths: z.record(z.string().or(z.array(z.string()))) }) });
async function getProjectInfo(cwd) {
const [configFiles, typescript, tailwindConfigFile, tailwindCssFile, tailwindVersion, aliasPrefix, packageJson] = await Promise.all([
glob("**/{nuxt,vite,astro}.config.*|composer.json", {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE
}),
isTypeScriptProject(cwd),
getTailwindConfigFile(cwd),
getTailwindCssFile(cwd),
getTailwindVersion(cwd),
getTsConfigAliasPrefix(cwd),
getPackageInfo(cwd, false)
]);
const type = {
framework: FRAMEWORKS.manual,
typescript,
tailwindConfigFile,
tailwindCssFile,
tailwindVersion,
aliasPrefix
};
if (configFiles.find((file) => file.startsWith("nuxt.config."))?.length) {
type.framework = FRAMEWORKS.nuxt;
return type;
}
if (configFiles.find((file) => file.startsWith("astro.config."))?.length) {
type.framework = FRAMEWORKS.astro;
return type;
}
if (configFiles.find((file) => file.startsWith("composer.json"))?.length) {
type.framework = FRAMEWORKS.laravel;
return type;
}
if (configFiles.find((file) => file.startsWith("vite.config."))?.length) {
type.framework = FRAMEWORKS.vite;
return type;
}
return type;
}
async function getTailwindVersion(cwd) {
const [packageInfo, config] = await Promise.all([getPackageInfo(cwd), getConfig(cwd)]);
if (config?.tailwind?.config === "") return "v4";
if (!packageInfo?.dependencies?.tailwindcss && !packageInfo?.devDependencies?.tailwindcss) return null;
if (/^(?:\^|~)?3(?:\.\d+)*(?:-.*)?$/.test(packageInfo?.dependencies?.tailwindcss || packageInfo?.devDependencies?.tailwindcss || "")) return "v3";
return "v4";
}
async function getTailwindCssFile(cwd) {
const [files, tailwindVersion] = await Promise.all([glob(["**/*.css", "**/*.scss"], {
cwd,
deep: 5,
ignore: PROJECT_SHARED_IGNORE
}), getTailwindVersion(cwd)]);
if (!files.length) return null;
const needle = tailwindVersion === "v4" ? `@import "tailwindcss"` : "@tailwind base";
for (const file of files) {
const contents = await fs.readFile(path.resolve(cwd, file), "utf8");
if (contents.includes(`@import "tailwindcss"`) || contents.includes(`@import 'tailwindcss'`) || contents.includes(`@tailwind base`)) return file;
}
return null;
}
async function getTailwindConfigFile(cwd) {
const files = await glob("tailwind.config.*", {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE
});
if (!files.length) return null;
return files[0];
}
async function getTsConfigAliasPrefix(cwd) {
const isTypescript = await isTypeScriptProject(cwd);
const tsconfigType = isTypescript ? "tsconfig.json" : "jsconfig.json";
const tsConfig = getTSConfig(cwd, tsconfigType);
const parsedTsConfig = parseTsconfig(tsConfig.path);
const aliasPaths = parsedTsConfig.compilerOptions?.paths ?? {};
for (const [alias, paths] of Object.entries(aliasPaths)) if (paths.includes("./*") || paths.includes("./src/*") || paths.includes("./app/*") || paths.includes("./resources/js/*")) {
const cleanAlias = alias.replace(/\/\*$/, "") ?? null;
return cleanAlias === "#build" ? "@" : cleanAlias;
}
return Object.keys(aliasPaths)?.[0]?.replace(/\/\*$/, "") ?? null;
}
async function isTypeScriptProject(cwd) {
const files = await glob("tsconfig.*", {
cwd,
deep: 1,
ignore: PROJECT_SHARED_IGNORE
});
return files.length > 0;
}
async function getProjectConfig(cwd, defaultProjectInfo = null) {
const [existingConfig, projectInfo] = await Promise.all([getConfig(cwd), !defaultProjectInfo ? getProjectInfo(cwd) : Promise.resolve(defaultProjectInfo)]);
if (existingConfig) return existingConfig;
if (!projectInfo || !projectInfo.tailwindCssFile || projectInfo.tailwindVersion === "v3" && !projectInfo.tailwindConfigFile) return null;
const config = {
$schema: "https://shadcn-vue.com/schema.json",
typescript: projectInfo.typescript,
style: "new-york",
tailwind: {
config: projectInfo.tailwindConfigFile ?? "",
baseColor: "zinc",
css: projectInfo.tailwindCssFile,
cssVariables: true,
prefix: ""
},
iconLibrary: "lucide",
aliases: {
components: `${projectInfo.aliasPrefix}/components`,
ui: `${projectInfo.aliasPrefix}/components/ui`,
composables: `${projectInfo.aliasPrefix}/composables`,
lib: `${projectInfo.aliasPrefix}/lib`,
utils: `${projectInfo.aliasPrefix}/lib/utils`
}
};
return await resolveConfigPaths(cwd, config);
}
async function getProjectTailwindVersionFromConfig(config) {
if (!config.resolvedPaths?.cwd) return "v3";
const projectInfo = await getProjectInfo(config.resolvedPaths.cwd);
if (!projectInfo?.tailwindVersion) return null;
return projectInfo.tailwindVersion;
}
//#endregion
//#region src/utils/logger.ts
const logger = {
error(...args) {
consola.log(highlighter.error(args.join(" ")));
},
warn(...args) {
consola.log(highlighter.warn(args.join(" ")));
},
info(...args) {
consola.log(highlighter.info(args.join(" ")));
},
success(...args) {
consola.log(highlighter.success(args.join(" ")));
},
log(...args) {
consola.log(args.join(" "));
},
break() {
consola.log("");
}
};
//#endregion
//#region src/utils/spinner.ts
function spinner(text, options) {
return ora({
text,
isSilent: options?.silent
});
}
//#endregion
//#region src/registry/schema.ts
const registryItemTypeSchema = z.enum([
"registry:lib",
"registry:block",
"registry:component",
"registry:ui",
"registry:hook",
"registry:composable",
"registry:page",
"registry:file",
"registry:theme",
"registry:style",
"registry:example",
"registry:internal"
]);
const registryItemFileSchema = z.discriminatedUnion("type", [z.object({
path: z.string(),
content: z.string().optional(),
type: z.enum(["registry:file", "registry:page"]),
target: z.string()
}), z.object({
path: z.string(),
content: z.string().optional(),
type: registryItemTypeSchema.exclude(["registry:file", "registry:page"]),
target: z.string().optional()
})]);
const registryItemTailwindSchema = z.object({ config: z.object({
content: z.array(z.string()).optional(),
theme: z.record(z.string(), z.any()).optional(),
plugins: z.array(z.string()).optional()
}).optional() });
const registryItemCssVarsSchema = z.object({
theme: z.record(z.string(), z.string()).optional(),
light: z.record(z.string(), z.string()).optional(),
dark: z.record(z.string(), z.string()).optional()
});
const registryItemCssSchema = z.record(z.string(), z.lazy(() => z.union([z.string(), z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())]))])));
const registryItemSchema = z.object({
$schema: z.string().optional(),
extends: z.string().optional(),
name: z.string(),
type: registryItemTypeSchema,
title: z.string().optional(),
author: z.string().min(2).optional(),
description: z.string().optional(),
dependencies: z.array(z.string()).optional(),
devDependencies: z.array(z.string()).optional(),
registryDependencies: z.array(z.string()).optional(),
files: z.array(registryItemFileSchema).optional(),
tailwind: registryItemTailwindSchema.optional(),
cssVars: registryItemCssVarsSchema.optional(),
css: registryItemCssSchema.optional(),
meta: z.record(z.string(), z.any()).optional(),
docs: z.string().optional(),
categories: z.array(z.string()).optional()
});
const registrySchema = z.object({
name: z.string(),
homepage: z.string(),
items: z.array(registryItemSchema)
});
const registryIndexSchema = z.array(registryItemSchema);
const stylesSchema = z.array(z.object({
name: z.string(),
label: z.string()
}));
const iconsSchema = z.record(z.string(), z.record(z.string(), z.string()));
const registryBaseColorSchema = z.object({
inlineColors: z.object({
light: z.record(z.string(), z.string()),
dark: z.record(z.string(), z.string())
}),
cssVars: registryItemCssVarsSchema,
cssVarsV4: registryItemCssVarsSchema.optional(),
inlineColorsTemplate: z.string(),
cssVarsTemplate: z.string()
});
const registryResolvedItemsTreeSchema = registryItemSchema.pick({
dependencies: true,
devDependencies: true,
files: true,
tailwind: true,
cssVars: true,
css: true,
docs: true
});
//#endregion
//#region src/utils/handle-error.ts
function handleError(error) {
logger.error(`Something went wrong. Please check the error below for more details.`);
logger.error(`If the problem persists, please open an issue on GitHub.`);
logger.error("");
if (typeof error === "string") {
logger.error(error);
logger.break();
process.exit(1);
}
if (error instanceof z.ZodError) {
logger.error("Validation failed:");
for (const [key, value] of Object.entries(error.flatten().fieldErrors)) logger.error(`- ${highlighter.info(key)}: ${value}`);
logger.break();
process.exit(1);
}
if (error instanceof Error) {
logger.error(error.message);
logger.break();
process.exit(1);
}
logger.break();
process.exit(1);
}
//#endregion
//#region src/utils/updaters/update-tailwind-config.ts
async function updateTailwindConfig(tailwindConfig, config, options) {
if (!tailwindConfig) return;
options = {
silent: false,
tailwindVersion: "v3",
...options
};
if (options.tailwindVersion === "v4") return;
const tailwindFileRelativePath = path.relative(config.resolvedPaths.cwd, config.resolvedPaths.tailwindConfig);
const tailwindSpinner = spinner(`Updating ${highlighter.info(tailwindFileRelativePath)}`, { silent: options.silent }).start();
const raw = await promises.readFile(config.resolvedPaths.tailwindConfig, "utf8");
const output = await transformTailwindConfig(raw, tailwindConfig, config);
await promises.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8");
tailwindSpinner?.succeed();
}
async function transformTailwindConfig(input, tailwindConfig, config) {
const sourceFile = await _createSourceFile(input, config);
const configObject = sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression).find((node) => node.getProperties().some((property) => property.isKind(SyntaxKind.PropertyAssignment) && property.getName() === "content"));
if (!configObject) return input;
const quoteChar = _getQuoteChar(configObject);
addTailwindConfigProperty(configObject, {
name: "darkMode",
value: "class"
}, { quoteChar });
tailwindConfig.plugins?.forEach((plugin) => {
addTailwindConfigPlugin(configObject, plugin);
});
if (tailwindConfig.theme) await addTailwindConfigTheme(configObject, tailwindConfig.theme);
return sourceFile.getFullText();
}
function addTailwindConfigProperty(configObject, property, { quoteChar }) {
const existingProperty = configObject.getProperty("darkMode");
if (!existingProperty) {
const newProperty = {
name: property.name,
initializer: `[${quoteChar}${property.value}${quoteChar}]`
};
if (property.name === "darkMode") {
configObject.insertPropertyAssignment(0, newProperty);
return configObject;
}
configObject.addPropertyAssignment(newProperty);
return configObject;
}
if (existingProperty.isKind(SyntaxKind.PropertyAssignment)) {
const initializer = existingProperty.getInitializer();
const newValue = `${quoteChar}${property.value}${quoteChar}`;
if (initializer?.isKind(SyntaxKind.StringLiteral)) {
const initializerText = initializer.getText();
initializer.replaceWithText(`[${initializerText}, ${newValue}]`);
return configObject;
}
if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) {
if (initializer.getElements().map((element) => element.getText()).includes(newValue)) return configObject;
initializer.addElement(newValue);
}
return configObject;
}
return configObject;
}
async function addTailwindConfigTheme(configObject, theme) {
if (!configObject.getProperty("theme")) configObject.addPropertyAssignment({
name: "theme",
initializer: "{}"
});
nestSpreadProperties(configObject);
const themeProperty = configObject.getPropertyOrThrow("theme")?.asKindOrThrow(SyntaxKind.PropertyAssignment);
const themeInitializer = themeProperty.getInitializer();
if (themeInitializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
const themeObjectString = themeInitializer.getText();
const themeObject = await parseObjectLiteral(themeObjectString);
const result = deepmerge(themeObject, theme, { arrayMerge: (dst, src) => src });
const resultString = objectToString(result).replace(/'\.\.\.(.*)'/g, "...$1").replace(/'"/g, "'").replace(/"'/g, "'").replace(/'\[/g, "[").replace(/\]'/g, "]").replace(/'\\'/g, "'").replace(/\\'/g, "'").replace(/\\''/g, "'").replace(/''/g, "'");
themeInitializer.replaceWithText(resultString);
}
unnestSpreadProperties(configObject);
}
function addTailwindConfigPlugin(configObject, plugin) {
const existingPlugins = configObject.getProperty("plugins");
if (!existingPlugins) {
configObject.addPropertyAssignment({
name: "plugins",
initializer: `[${plugin}]`
});
return configObject;
}
if (existingPlugins.isKind(SyntaxKind.PropertyAssignment)) {
const initializer = existingPlugins.getInitializer();
if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) {
if (initializer.getElements().map((element) => {
return element.getText().replace(/["']/g, "");
}).includes(plugin.replace(/["']/g, ""))) return configObject;
initializer.addElement(plugin);
}
return configObject;
}
return configObject;
}
async function _createSourceFile(input, config) {
const dir = await promises.mkdtemp(path.join(tmpdir(), "shadcn-"));
const resolvedPath = config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts";
const tempFile = path.join(dir, `shadcn-${path.basename(resolvedPath)}`);
const project = new Project({ compilerOptions: {} });
const sourceFile = project.createSourceFile(tempFile, input, { scriptKind: path.extname(resolvedPath) === ".ts" ? ScriptKind.TS : ScriptKind.JS });
return sourceFile;
}
function _getQuoteChar(configObject) {
return configObject.getFirstDescendantByKind(SyntaxKind.StringLiteral)?.getQuoteKind() === QuoteKind.Single ? "'" : "\"";
}
function nestSpreadProperties(obj) {
const properties = obj.getProperties();
for (let i = 0; i < properties.length; i++) {
const prop = properties[i];
if (prop.isKind(SyntaxKind.SpreadAssignment)) {
const spreadAssignment = prop.asKindOrThrow(SyntaxKind.SpreadAssignment);
const spreadText = spreadAssignment.getExpression().getText();
obj.insertPropertyAssignment(i, {
name: `"___${spreadText.replace(/^\.\.\./, "")}"`,
initializer: `"...${spreadText.replace(/^\.\.\./, "")}"`
});
spreadAssignment.remove();
} else if (prop.isKind(SyntaxKind.PropertyAssignment)) {
const propAssignment = prop.asKindOrThrow(SyntaxKind.PropertyAssignment);
const initializer = propAssignment.getInitializer();
if (initializer && initializer.isKind(SyntaxKind.ObjectLiteralExpression)) nestSpreadProperties(initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression));
else if (initializer && initializer.isKind(SyntaxKind.ArrayLiteralExpression)) nestSpreadElements(initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression));
}
}
}
function nestSpreadElements(arr) {
const elements = arr.getElements();
for (let j = 0; j < elements.length; j++) {
const element = elements[j];
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) nestSpreadProperties(element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression));
else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) nestSpreadElements(element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression));
else if (element.isKind(SyntaxKind.SpreadElement)) {
const spreadText = element.getText();
arr.removeElement(j);
arr.insertElement(j, `"${spreadText}"`);
}
}
}
function unnestSpreadProperties(obj) {
const properties = obj.getProperties();
for (let i = 0; i < properties.length; i++) {
const prop = properties[i];
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
const propAssignment = prop;
const initializer = propAssignment.getInitializer();
if (initializer && initializer.isKind(SyntaxKind.StringLiteral)) {
const value = initializer.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
if (value.startsWith("...")) {
obj.insertSpreadAssignment(i, { expression: value.slice(3) });
propAssignment.remove();
}
} else if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) unnestSpreadProperties(initializer);
else if (initializer && initializer.isKind(SyntaxKind.ArrayLiteralExpression)) unnsetSpreadElements(initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression));
}
}
}
function unnsetSpreadElements(arr) {
const elements = arr.getElements();
for (let j = 0; j < elements.length; j++) {
const element = elements[j];
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) unnestSpreadProperties(element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression));
else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) unnsetSpreadElements(element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression));
else if (element.isKind(SyntaxKind.StringLiteral)) {
const spreadText = element.getText();
const spreadTest = /^['"](\.\.\..*)['"]$/g;
if (spreadTest.test(spreadText)) {
arr.removeElement(j);
arr.insertElement(j, spreadText.replace(spreadTest, "$1"));
}
}
}
}
async function parseObjectLiteral(objectLiteralString) {
const sourceFile = await _createSourceFile(`const theme = ${objectLiteralString}`, null);
const statement = sourceFile.getStatements()[0];
if (statement?.getKind() === SyntaxKind.VariableStatement) {
const declaration = statement.getDeclarationList()?.getDeclarations()[0];
const initializer = declaration.getInitializer();
if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) return await parseObjectLiteralExpression(initializer);
}
throw new Error("Invalid input: not an object literal");
}
function parseObjectLiteralExpression(node) {
const result = {};
for (const property of node.getProperties()) if (property.isKind(SyntaxKind.PropertyAssignment)) {
const name = property.getName().replace(/'/g, "");
if (property.getInitializer()?.isKind(SyntaxKind.ObjectLiteralExpression)) result[name] = parseObjectLiteralExpression(property.getInitializer());
else if (property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression)) result[name] = parseArrayLiteralExpression(property.getInitializer());
else result[name] = parseValue(property.getInitializer());
}
return result;
}
function parseArrayLiteralExpression(node) {
const result = [];
for (const element of node.getElements()) if (element.isKind(SyntaxKind.ObjectLiteralExpression)) result.push(parseObjectLiteralExpression(element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)));
else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) result.push(parseArrayLiteralExpression(element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)));
else result.push(parseValue(element));
return result;
}
function parseValue(node) {
switch (node.getKind()) {
case SyntaxKind.StringLiteral: return node.getText();
case SyntaxKind.NumericLiteral: return Number(node.getText());
case SyntaxKind.TrueKeyword: return true;
case SyntaxKind.FalseKeyword: return false;
case SyntaxKind.NullKeyword: return null;
case SyntaxKind.ArrayLiteralExpression: return node.getElements().map(parseValue);
case SyntaxKind.ObjectLiteralExpression: return parseObjectLiteralExpression(node);
default: return node.getText();
}
}
function buildTailwindThemeColorsFromCssVars(cssVars) {
const result = {};
for (const key of Object.keys(cssVars)) {
const parts = key.split("-");
const colorName = parts[0];
const subType = parts.slice(1).join("-");
if (subType === "") if (typeof result[colorName] === "object") result[colorName].DEFAULT = `hsl(var(--${key}))`;
else result[colorName] = `hsl(var(--${key}))`;
else {
if (typeof result[colorName] !== "object") result[colorName] = { DEFAULT: `hsl(var(--${colorName}))` };
result[colorName][subType] = `hsl(var(--${key}))`;
}
}
for (const [colorName, value] of Object.entries(result)) if (typeof value === "object" && value.DEFAULT === `hsl(var(--${colorName}))` && !(colorName in cssVars)) delete value.DEFAULT;
return result;
}
//#endregion
//#region src/registry/api.ts
const REGISTRY_URL = process.env.REGISTRY_URL ?? "https://shadcn-vue.com/r";
const agent = process.env.https_proxy ? new ProxyAgent(process.env.https_proxy) : void 0;
const registryCache = new Map();
async function getRegistryIndex() {
try {
const [result] = await fetchRegistry(["index.json"]);
return registryIndexSchema.parse(result);
} catch (error) {
logger.error("\n");
handleError(error);
}
}
async function getRegistryStyles() {
try {
const [result] = await fetchRegistry(["styles/index.json"]);
return stylesSchema.parse(result);
} catch (error) {
logger.error("\n");
handleError(error);
return [];
}
}
async function getRegistryIcons() {
try {
const [result] = await fetchRegistry(["icons/index.json"]);
return iconsSchema.parse(result);
} catch (error) {
handleError(error);
return {};
}
}
async function getRegistryItem(name, style) {
try {
const [result] = await fetchRegistry([isUrl(name) ? name : `styles/${style}/${name}.json`]);
return registryItemSchema.parse(result);
} catch (error) {
logger.break();
handleError(error);
return null;
}
}
const BASE_COLORS = [
{
name: "neutral",
label: "Neutral"
},
{
name: "gray",
label: "Gray"
},
{
name: "zinc",
label: "Zinc"
},
{
name: "stone",
label: "Stone"
},
{
name: "slate",
label: "Slate"
}
];
async function getRegistryBaseColors() {
return BASE_COLORS;
}
async function getRegistryBaseColor(baseColor) {
try {
const [result] = await fetchRegistry([`colors/${baseColor}.json`]);
return registryBaseColorSchema.parse(result);
} catch (error) {
handleError(error);
}
}
async function resolveTree(index, names) {
const tree = [];
for (const name of names) {
const entry = index.find((entry$1) => entry$1.name === name);
if (!entry) continue;
tree.push(entry);
if (entry.registryDependencies) {
const dependencies = await resolveTree(index, entry.registryDependencies);
tree.push(...dependencies);
}
}
return tree.filter((component, index$1, self) => self.findIndex((c) => c.name === component.name) === index$1);
}
async function fetchTree(style, tree) {
try {
const paths = tree.map((item) => `styles/${style}/${item.name}.json`);
const result = await fetchRegistry(paths);
return registryIndexSchema.parse(result);
} catch (error) {
handleError(error);
}
}
async function getItemTargetPath(config, item, override) {
if (override) return override;
if (item.type === "registry:ui") return config.resolvedPaths.ui ?? config.resolvedPaths.components;
const [parent, type] = item.type?.split(":") ?? [];
if (!(parent in config.resolvedPaths)) return null;
return path.join(config.resolvedPaths[parent], type);
}
async function fetchRegistry(paths) {
try {
const results = await Promise.all(paths.map(async (path$1) => {
const url = getRegistryUrl(path$1);
if (registryCache.has(url)) return registryCache.get(url);
const fetchPromise = ofetch(url, {
dispatcher: agent,
parseResponse: JSON.parse
}).catch((error) => {
const status = error.status || error.statusCode;
if (status === 401) throw new Error(`You are not authorized to access the component at ${highlighter.info(url)}.\nIf this is a remote registry, you may need to authenticate.`);
if (status === 404) throw new Error(`The component at ${highlighter.info(url)} was not found.\nIt may not exist at the registry. Please make sure it is a valid component.`);
if (status === 403) throw new Error(`You do not have access to the component at ${highlighter.info(url)}.\nIf this is a remote registry, you may need to authenticate or a token.`);
const message = error.data?.error || error.message || `HTTP ${status || "Unknown"} Error`;
throw new Error(`Failed to fetch from ${highlighter.info(url)}.\n${message}`);
});
registryCache.set(url, fetchPromise);
return fetchPromise;
}));
return results;
} catch (error) {
logger.error("\n");
handleError(error);
return [];
}
}
async function registryResolveItemsTree(names, config) {
try {
const index = await getRegistryIndex();
if (!index) return null;
if (names.includes("index")) names.unshift("index");
let registryItems = await resolveRegistryItems(names, config);
let result = await fetchRegistry(registryItems);
const payload = z.array(registryItemSchema).parse(result);
if (!payload) return null;
if (names.includes("index")) {
if (config.tailwind.baseColor) {
const theme = await registryGetTheme(config.tailwind.baseColor, config);
if (theme) payload.unshift(theme);
}
}
payload.sort((a, b) => {
if (a.type === "registry:theme") return -1;
return 1;
});
let tailwind = {};
payload.forEach((item) => {
tailwind = deepmerge(tailwind, item.tailwind ?? {});
});
let cssVars = {};
payload.forEach((item) => {
cssVars = deepmerge(cssVars, item.cssVars ?? {});
});
let css = {};
payload.forEach((item) => {
css = deepmerge(css, item.css ?? {});
});
let docs = "";
payload.forEach((item) => {
if (item.docs) docs += `${item.docs}\n`;
});
return registryResolvedItemsTreeSchema.parse({
dependencies: deepmerge.all(payload.map((item) => item.dependencies ?? [])),
devDependencies: deepmerge.all(payload.map((item) => item.devDependencies ?? [])),
files: deepmerge.all(payload.map((item) => item.files ?? [])),
tailwind,
cssVars,
css,
docs
});
} catch (error) {
handleError(error);
return null;
}
}
async function resolveRegistryDependencies(url, config) {
const visited = new Set();
const payload = [];
const style = config.resolvedPaths?.cwd ? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style) : config.style;
async function resolveDependencies(itemUrl) {
const url$1 = getRegistryUrl(isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json`);
if (visited.has(url$1)) return;
visited.add(url$1);
try {
const [result] = await fetchRegistry([url$1]);
const item = registryItemSchema.parse(result);
payload.push(url$1);
if (item.registryDependencies) for (const dependency of item.registryDependencies) await resolveDependencies(dependency);
} catch (error) {
console.error(`Error fetching or parsing registry item at ${itemUrl}:`, error);
}
}
await resolveDependencies(url);
return Array.from(new Set(payload));
}
async function registryGetTheme(name, config) {
const [baseColor, tailwindVersion] = await Promise.all([getRegistryBaseColor(name), getProjectTailwindVersionFromConfig(config)]);
if (!baseColor) return null;
const theme = {
name,
type: "registry:theme",
tailwind: { config: { theme: { extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
colors: {}
} } } },
cssVars: {
theme: {},
light: { radius: "0.5rem" },
dark: {}
}
};
if (config.tailwind.cssVariables) {
theme.tailwind.config.theme.extend.colors = {
...theme.tailwind.config.theme.extend.colors,
...buildTailwindThemeColorsFromCssVars(baseColor.cssVars.dark ?? {})
};
theme.cssVars = {
theme: {
...baseColor.cssVars.theme,
...theme.cssVars.theme
},
light: {
...baseColor.cssVars.light,
...theme.cssVars.light
},
dark: {
...baseColor.cssVars.dark,
...theme.cssVars.dark
}
};
if (tailwindVersion === "v4" && baseColor.cssVarsV4) theme.cssVars = {
theme: {
...baseColor.cssVarsV4.theme,
...theme.cssVars.theme
},
light: {
...theme.cssVars.light,
...baseColor.cssVarsV4.light
},
dark: {
...theme.cssVars.dark,
...baseColor.cssVarsV4.dark
}
};
}
return theme;
}
function clearRegistryCache() {
registryCache.clear();
}
function getRegistryUrl(path$1) {
if (isUrl(path$1)) {
const url = new URL(path$1);
if (url.pathname.match(/\/chat\/b\//) && !url.pathname.endsWith("/json")) url.pathname = `${url.pathname}/json`;
return url.toString();
}
return `${REGISTRY_URL}/${path$1}`;
}
function isUrl(path$1) {
try {
new URL(path$1);
return true;
} catch (error) {
return false;
}
}
async function resolveRegistryItems(names, config) {
const registryDependencies = [];
for (const name of names) {
const itemRegistryDependencies = await resolveRegistryDependencies(name, config);
registryDependencies.push(...itemRegistryDependencies);
}
return Array.from(new Set(registryDependencies));
}
function getRegistryTypeAliasMap() {
return new Map([
["registry:ui", "ui"],
["registry:lib", "lib"],
["registry:hook", "hooks"],
["registry:composable", "composable"],
["registry:block", "components"],
["registry:component", "components"]
]);
}
function getRegistryParentMap(registryItems) {
const map = new Map();
registryItems.forEach((item) => {
if (!item.registryDependencies) return;
item.registryDependencies.forEach((dependency) => {
map.set(dependency, item);
});
});
return map;
}
//#endregion
export { BASE_COLORS, DEFAULT_COMPONENTS, DEFAULT_TAILWIND_CONFIG, DEFAULT_TAILWIND_CSS, DEFAULT_UTILS, _createSourceFile, _getQuoteChar, clearRegistryCache, fetchRegistry, fetchTree, findCommonRoot, findPackageRoot, getConfig, getItemTargetPath, getPackageInfo, getProjectConfig, getProjectInfo, getProjectTailwindVersionFromConfig, getRegistryBaseColor, getRegistryBaseColors, getRegistryIcons, getRegistryIndex, getRegistryItem, getRegistryParentMap, getRegistryStyles, getRegistryTypeAliasMap, getTSConfig, getWorkspaceConfig, handleError, highlighter, iconsSchema, isUrl, logger, rawConfigSchema, registryBaseColorSchema, registryGetTheme, registryIndexSchema, registryItemCssSchema, registryItemCssVarsSchema, registryItemFileSchema, registryItemSchema, registryItemTailwindSchema, registryItemTypeSchema, registryResolveItemsTree, registryResolvedItemsTreeSchema, registrySchema, resolveConfigPaths, resolveRegistryItems, resolveTree, spinner, stylesSchema, updateTailwindConfig };
//# sourceMappingURL=registry-ByrmygPr.js.map