shadcn-vue
Version:
Add components to your apps.
1,621 lines (1,603 loc) • 110 kB
JavaScript
#!/usr/bin/env node
// src/commands/init.ts
import { promises as fs8 } from "node:fs";
// src/utils/errors.ts
var MISSING_DIR_OR_EMPTY_PROJECT = "1";
var MISSING_CONFIG = "3";
var TAILWIND_NOT_CONFIGURED = "5";
var IMPORT_ALIAS_MISSING = "6";
var UNSUPPORTED_FRAMEWORK = "7";
// src/utils/frameworks.ts
var 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"
}
}
};
// src/utils/resolve-import.ts
import { createPathsMatcher } from "get-tsconfig";
function resolveImport(importPath, config) {
const matcher = createPathsMatcher(config);
if (matcher === null) {
return;
}
const paths = matcher(importPath);
return paths[0];
}
// src/utils/get-config.ts
import { cosmiconfig } from "cosmiconfig";
import { getTsconfig } from "get-tsconfig";
import path from "pathe";
import { z } from "zod";
// src/utils/highlighter.ts
import { colors } from "consola/utils";
var highlighter = {
error: colors.red,
warn: colors.yellow,
info: colors.cyan,
success: colors.green
};
// src/utils/get-config.ts
var TAILWIND_CSS_PATH = {
nuxt: "assets/css/tailwind.css",
vite: "src/assets/index.css",
laravel: "resources/css/app.css",
astro: "src/styles/globals.css"
};
var DEFAULT_COMPONENTS = "@/components";
var DEFAULT_UTILS = "@/lib/utils";
var DEFAULT_TAILWIND_CSS = TAILWIND_CSS_PATH.nuxt;
var DEFAULT_TAILWIND_CONFIG = "tailwind.config.js";
var explorer = cosmiconfig("components", {
searchPlaces: ["components.json"]
});
var 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();
var 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()
})
});
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"
),
// TODO: Make this configurable.
// For now, we assume the lib and hooks directories are one level up from the components directory.
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) {
console.log(error.message);
throw new Error(`Invalid configuration found in ${cwd}/components.json.`);
}
}
async function getTargetStyleFromConfig(cwd, fallback) {
const projectInfo = await getProjectInfo(cwd);
return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback;
}
// src/utils/get-package-info.ts
import fs from "fs-extra";
import path2 from "pathe";
function getPackageInfo(cwd = "", shouldThrow = true) {
const packageJsonPath = path2.join(cwd, "package.json");
return fs.readJSONSync(packageJsonPath, {
throws: shouldThrow
});
}
// src/utils/get-project-info.ts
import fs2 from "fs-extra";
import { parseTsconfig } from "get-tsconfig";
import path3 from "pathe";
import { glob } from "tinyglobby";
import { z as z2 } from "zod";
var PROJECT_SHARED_IGNORE = [
"**/node_modules/**",
".nuxt",
"public",
"dist",
"build"
];
var TS_CONFIG_SCHEMA = z2.object({
compilerOptions: z2.object({
paths: z2.record(z2.string().or(z2.array(z2.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 fs2.readFile(path3.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;
}
// src/utils/logger.ts
import consola from "consola";
var 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("");
}
};
// src/utils/spinner.ts
import ora from "ora";
function spinner(text, options) {
return ora({
text,
isSilent: options?.silent
});
}
// src/preflights/preflight-init.ts
import fs3 from "fs-extra";
import path4 from "pathe";
async function preFlightInit(options) {
const errors = {};
if (!fs3.existsSync(options.cwd) || !fs3.existsSync(path4.resolve(options.cwd, "package.json"))) {
errors[MISSING_DIR_OR_EMPTY_PROJECT] = true;
return {
errors,
projectInfo: null
};
}
const projectSpinner = spinner(`Preflight checks.`, {
silent: options.silent
}).start();
if (fs3.existsSync(path4.resolve(options.cwd, "components.json")) && !options.force) {
projectSpinner?.fail();
logger.break();
logger.error(
`A ${highlighter.info(
"components.json"
)} file already exists at ${highlighter.info(
options.cwd
)}.
To start over, remove the ${highlighter.info(
"components.json"
)} file and run ${highlighter.info("init")} again.`
);
logger.break();
process.exit(1);
}
projectSpinner?.succeed();
const frameworkSpinner = spinner(`Verifying framework.`, {
silent: options.silent
}).start();
const projectInfo = await getProjectInfo(options.cwd);
if (!projectInfo || projectInfo?.framework.name === "manual") {
errors[UNSUPPORTED_FRAMEWORK] = true;
frameworkSpinner?.fail();
logger.break();
if (projectInfo?.framework.links.installation) {
logger.error(
`We could not detect a supported framework at ${highlighter.info(
options.cwd
)}.
Visit ${highlighter.info(
projectInfo?.framework.links.installation
)} to manually configure your project.
Once configured, you can use the cli to add components.`
);
}
logger.break();
process.exit(1);
}
frameworkSpinner?.succeed(
`Verifying framework. Found ${highlighter.info(
projectInfo.framework.label
)}.`
);
let tailwindSpinnerMessage = "Validating Tailwind CSS.";
if (projectInfo.tailwindVersion === "v4") {
tailwindSpinnerMessage = `Validating Tailwind CSS config. Found ${highlighter.info(
"v4"
)}.`;
}
const tailwindSpinner = spinner(tailwindSpinnerMessage, {
silent: options.silent
}).start();
if (projectInfo.tailwindVersion === "v3" && (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile)) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else if (projectInfo.tailwindVersion === "v4" && !projectInfo?.tailwindCssFile) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else if (!projectInfo.tailwindVersion) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else {
tailwindSpinner?.succeed();
}
const tsConfigSpinner = spinner(`Validating import alias.`, {
silent: options.silent
}).start();
if (!projectInfo?.aliasPrefix) {
errors[IMPORT_ALIAS_MISSING] = true;
tsConfigSpinner?.fail();
} else {
tsConfigSpinner?.succeed();
}
if (Object.keys(errors).length > 0) {
if (errors[TAILWIND_NOT_CONFIGURED]) {
logger.break();
logger.error(
`No Tailwind CSS configuration found at ${highlighter.info(
options.cwd
)}.`
);
logger.error(
`It is likely you do not have Tailwind CSS installed or have an invalid configuration.`
);
logger.error(`Install Tailwind CSS then try again.`);
if (projectInfo?.framework.links.tailwind) {
logger.error(
`Visit ${highlighter.info(
projectInfo?.framework.links.tailwind
)} to get started.`
);
}
}
if (errors[IMPORT_ALIAS_MISSING]) {
logger.break();
logger.error(`No import alias found in your tsconfig.json file.`);
if (projectInfo?.framework.links.installation) {
logger.error(
`Visit ${highlighter.info(
projectInfo?.framework.links.installation
)} to learn how to set an import alias.`
);
}
}
logger.break();
process.exit(1);
}
return {
errors,
projectInfo
};
}
// src/utils/handle-error.ts
import { consola as consola2 } from "consola";
function handleError(error) {
consola2.log("this is error: ", error);
if (typeof error === "string") {
consola2.error(error);
process.exit(1);
}
if (error instanceof Error) {
consola2.error(error.message);
process.exit(1);
}
consola2.error("Something went wrong. Please try again.");
process.exit(1);
}
// src/utils/registry/schema.ts
import { z as z3 } from "zod";
var registryItemTypeSchema = z3.enum([
"registry:style",
"registry:lib",
"registry:example",
"registry:block",
"registry:component",
"registry:ui",
"registry:hook",
"registry:theme",
"registry:page"
]);
var registryItemFileSchema = z3.object({
path: z3.string(),
content: z3.string().optional(),
type: registryItemTypeSchema,
target: z3.string().optional()
});
var registryItemTailwindSchema = z3.object({
config: z3.object({
content: z3.array(z3.string()).optional(),
theme: z3.record(z3.string(), z3.any()).optional(),
plugins: z3.array(z3.string()).optional()
}).optional()
});
var registryItemCssVarsSchema = z3.object({
light: z3.record(z3.string(), z3.string()).optional(),
dark: z3.record(z3.string(), z3.string()).optional()
});
var registryItemSchema = z3.object({
name: z3.string(),
type: registryItemTypeSchema,
description: z3.string().optional(),
dependencies: z3.array(z3.string()).optional(),
devDependencies: z3.array(z3.string()).optional(),
registryDependencies: z3.array(z3.string()).optional(),
files: z3.array(registryItemFileSchema).optional(),
tailwind: registryItemTailwindSchema.optional(),
cssVars: registryItemCssVarsSchema.optional(),
meta: z3.record(z3.string(), z3.any()).optional(),
docs: z3.string().optional()
});
var registryIndexSchema = z3.array(
registryItemSchema.extend({
files: z3.array(z3.union([z3.string(), registryItemFileSchema])).optional()
})
);
var stylesSchema = z3.array(
z3.object({
name: z3.string(),
label: z3.string()
})
);
var iconsSchema = z3.record(
z3.string(),
z3.record(z3.string(), z3.string())
);
var registryBaseColorSchema = z3.object({
inlineColors: z3.object({
light: z3.record(z3.string(), z3.string()),
dark: z3.record(z3.string(), z3.string())
}),
cssVars: z3.object({
light: z3.record(z3.string(), z3.string()),
dark: z3.record(z3.string(), z3.string())
}),
cssVarsV4: z3.object({
light: z3.record(z3.string(), z3.string()),
dark: z3.record(z3.string(), z3.string())
}).optional(),
inlineColorsTemplate: z3.string(),
cssVarsTemplate: z3.string()
});
var registryResolvedItemsTreeSchema = registryItemSchema.pick({
dependencies: true,
devDependencies: true,
files: true,
tailwind: true,
cssVars: true,
docs: true
});
// src/utils/updaters/update-tailwind-config.ts
import { promises as fs4 } from "node:fs";
import { tmpdir } from "node:os";
import deepmerge from "deepmerge";
import path5 from "pathe";
import objectToString from "stringify-object";
import {
Project,
QuoteKind,
ScriptKind,
SyntaxKind
} from "ts-morph";
async function updateTailwindConfig(tailwindConfig, config, options) {
if (!tailwindConfig) {
return;
}
options = {
silent: false,
tailwindVersion: "v3",
...options
};
if (options.tailwindVersion === "v4") {
return;
}
const tailwindFileRelativePath = path5.relative(
config.resolvedPaths.cwd,
config.resolvedPaths.tailwindConfig
);
const tailwindSpinner = spinner(
`Updating ${highlighter.info(tailwindFileRelativePath)}`,
{
silent: options.silent
}
).start();
const raw = await fs4.readFile(config.resolvedPaths.tailwindConfig, "utf8");
const output = await transformTailwindConfig(raw, tailwindConfig, config);
await fs4.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 fs4.mkdtemp(path5.join(tmpdir(), "shadcn-"));
const resolvedPath = config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts";
const tempFile = path5.join(dir, `shadcn-${path5.basename(resolvedPath)}`);
const project = new Project({
compilerOptions: {}
});
const sourceFile = project.createSourceFile(tempFile, input, {
// Note: .js and .mjs can still be valid for TS projects.
// We can't infer TypeScript from config.tsx.
scriptKind: path5.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, {
// Need to escape the name with " so that deepmerge doesn't mishandle the key
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;
}
// src/utils/registry/index.ts
import deepmerge2 from "deepmerge";
import { ofetch } from "ofetch";
import path6 from "pathe";
import { ProxyAgent } from "undici";
import { z as z4 } from "zod";
var REGISTRY_URL = process.env.REGISTRY_URL ?? "https://shadcn-vue.com/r";
var agent = process.env.https_proxy ? new ProxyAgent(process.env.https_proxy) : void 0;
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 {};
}
}
var 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 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 path6.join(
config.resolvedPaths[parent],
type
);
}
async function fetchRegistry(paths) {
try {
const results = await Promise.all(
paths.map(async (path17) => {
const url = getRegistryUrl(path17);
const response = await ofetch(url, { dispatcher: agent, parseResponse: JSON.parse }).catch((error) => {
throw new Error(error.data);
});
return response;
})
);
return results;
} catch (error) {
logger.error("\n");
handleError(error);
return [];
}
}
function getRegistryItemFileTargetPath(file, config, override) {
if (override) {
return override;
}
if (file.type === "registry:ui") {
const folder = file.path.split("/")[1];
return path6.join(config.resolvedPaths.ui, folder);
}
if (file.type === "registry:lib") {
return config.resolvedPaths.lib;
}
if (file.type === "registry:block" || file.type === "registry:component") {
return config.resolvedPaths.components;
}
if (file.type === "registry:hook") {
return config.resolvedPaths.composables;
}
if (file.type === "registry:page") {
return config.resolvedPaths.components;
}
return config.resolvedPaths.components;
}
async function registryResolveItemsTree(names, config) {
try {
const index = await getRegistryIndex();
if (!index) {
return null;
}
if (names.includes("index")) {
names.unshift("index");
}
const registryDependencies = /* @__PURE__ */ new Set();
for (const name of names) {
const itemRegistryDependencies = await resolveRegistryDependencies(
name,
config
);
itemRegistryDependencies.forEach((dep) => registryDependencies.add(dep));
}
const uniqueRegistryDependencies = Array.from(registryDependencies);
const result = await fetchRegistry(uniqueRegistryDependencies);
const payload = z4.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);
}
}
}
let tailwind = {};
payload.forEach((item) => {
tailwind = deepmerge2(tailwind, item.tailwind ?? {});
});
let cssVars = {};
payload.forEach((item) => {
cssVars = deepmerge2(cssVars, item.cssVars ?? {});
});
let docs = "";
payload.forEach((item) => {
if (item.docs) {
docs += `${item.docs}
`;
}
});
return registryResolvedItemsTreeSchema.parse({
dependencies: Array.from(new Set(payload.flatMap((item) => item.dependencies ?? []))),
devDependencies: Array.from(new Set(payload.flatMap((item) => item.devDependencies ?? []))),
files: deepmerge2.all(payload.map((item) => item.files ?? [])),
tailwind,
cssVars,
docs
});
} catch (error) {
handleError(error);
return null;
}
}
async function resolveRegistryDependencies(url, config) {
const visited = /* @__PURE__ */ new Set();
const payload = [];
const style = config.resolvedPaths?.cwd ? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style) : config.style;
async function resolveDependencies(itemUrl) {
const url2 = getRegistryUrl(
isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json`
);
if (visited.has(url2)) {
return;
}
visited.add(url2);
try {
const [result] = await fetchRegistry([url2]);
const item = registryItemSchema.parse(result);
payload.push(url2);
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: {
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 = {
light: {
...baseColor.cssVars.light,
...theme.cssVars.light
},
dark: {
...baseColor.cssVars.dark,
...theme.cssVars.dark
}
};
if (tailwindVersion === "v4" && baseColor.cssVarsV4) {
theme.cssVars = {
light: {
...theme.cssVars.light,
...baseColor.cssVarsV4.light
},
dark: {
...theme.cssVars.dark,
...baseColor.cssVarsV4.dark
}
};
}
}
return theme;
}
function getRegistryUrl(path17) {
if (isUrl(path17)) {
const url = new URL(path17);
if (url.pathname.match(/\/chat\/b\//) && !url.pathname.endsWith("/json")) {
url.pathname = `${url.pathname}/json`;
}
return url.toString();
}
return `${REGISTRY_URL}/${path17}`;
}
function isUrl(path17) {
try {
new URL(path17);
return true;
} catch (error) {
return false;
}
}
// src/utils/updaters/update-css-vars.ts
import { promises as fs5 } from "node:fs";
import path7 from "node:path";
import postcss from "postcss";
import AtRule from "postcss/lib/at-rule";
import { z as z5 } from "zod";
async function updateCssVars(cssVars, config, options) {
if (!config.resolvedPaths.tailwindCss || !Object.keys(cssVars ?? {}).length) {
return;
}
options = {
cleanupDefaultNextStyles: false,
silent: false,
tailwindVersion: "v3",
...options
};
const cssFilepath = config.resolvedPaths.tailwindCss;
const cssFilepathRelative = path7.relative(
config.resolvedPaths.cwd,
cssFilepath
);
const cssVarsSpinner = spinner(
`Updating ${highlighter.info(cssFilepathRelative)}`,
{
silent: options.silent
}
).start();
const raw = await fs5.readFile(cssFilepath, "utf8");
const output = await transformCssVars(raw, cssVars ?? {}, config, {
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
tailwindVersion: options.tailwindVersion,
tailwindConfig: options.tailwindConfig
});
await fs5.writeFile(cssFilepath, output, "utf8");
cssVarsSpinner.succeed();
}
async function transformCssVars(input, cssVars, config, options = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
tailwindConfig: void 0
}) {
options = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
tailwindConfig: void 0,
...options
};
let plugins = [updateCssVarsPlugin(cssVars)];
if (options.cleanupDefaultNextStyles) {
plugins.push(cleanupDefaultNextStylesPlugin());
}
if (options.tailwindVersion === "v4") {
plugins = [addCustomVariant({ params: "dark (&:is(.dark *))" })];
if (options.cleanupDefaultNextStyles) {
plugins.push(cleanupDefaultNextStylesPlugin());
}
plugins.push(updateCssVarsPluginV4(cssVars));
plugins.push(updateThemePlugin(cssVars));
if (options.tailwindConfig) {
plugins.push(updateTailwindConfigPlugin(options.tailwindConfig));
plugins.push(updateTailwindConfigAnimationPlugin(options.tailwindConfig));
plugins.push(updateTailwindConfigKeyframesPlugin(options.tailwindConfig));
}
}
if (config.tailwind.cssVariables) {
plugins.push(
updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion })
);
}
const result = await postcss(plugins).process(input, {
from: void 0
});
let output = result.css;
output = output.replace(/\/\* ---break--- \*\//g, "");
if (options.tailwindVersion === "v4") {
output = output.replace(/(\n\s*\n)+/g, "\n\n");
}
return output;
}
function updateBaseLayerPlugin({
tailwindVersion
}) {
return {
postcssPlugin: "update-base-layer",
Once(root) {
const requiredRules = [
{
selector: "*",
apply: tailwindVersion === "v4" ? "border-border outline-ring/50" : "border-border"
},
{ selector: "body", apply: "bg-background text-foreground" }
];
let baseLayer = root.nodes.find(
(node) => node.type === "atrule" && node.name === "layer" && node.params === "base" && requiredRules.every(
({ selector, apply }) => node.nodes?.some(
(rule) => rule.type === "rule" && rule.selector === selector && rule.nodes.some(
(applyRule) => applyRule.type === "atrule" && applyRule.name === "apply" && applyRule.params === apply
)
)
)
);
if (!baseLayer) {
baseLayer = postcss.atRule({
name: "layer",
params: "base",
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss.comment({ text: "---break---" }));
}
requiredRules.forEach(({ selector, apply }) => {
const existingRule = baseLayer?.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!existingRule) {
baseLayer?.append(
postcss.rule({
selector,
nodes: [
postcss.atRule({
name: "apply",
params: apply,
raws: { semicolon: true, before: "\n " }
})
],
raws: { semicolon: true, between: " ", before: "\n " }
})
);
}
});
}
};
}
function updateCssVarsPlugin(cssVars) {
return {
postcssPlugin: "update-css-vars",
Once(root) {
let baseLayer = root.nodes.find(
(node) => node.type === "atrule" && node.name === "layer" && node.params === "base"
);
if (!(baseLayer instanceof AtRule)) {
baseLayer = postcss.atRule({
name: "layer",
params: "base",
nodes: [],
raws: {
semicolon: true,
before: "\n",
between: " "
}
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss.comment({ text: "---break---" }));
}
if (baseLayer !== void 0) {
Object.entries(cssVars).forEach(([key, vars]) => {
const selector = key === "light" ? ":root" : `.${key}`;
addOrUpdateVars(baseLayer, selector, vars);
});
}
}
};
}
function removeConflictVars(root) {
const rootRule = root.nodes.find(
(node) => node.type === "rule" && node.selector === ":root"
);
if (rootRule) {
const propsToRemove = ["--background", "--foreground"];
rootRule.nodes.filter(
(node) => node.type === "decl" && propsToRemove.includes(node.prop)
).forEach((node) => node.remove());
if (rootRule.nodes.length === 0) {
rootRule.remove();
}
}
}
function cleanupDefaultNextStylesPlugin() {
return {
postcssPlugin: "cleanup-default-next-styles",
Once(root) {
const bodyRule = root.nodes.find(
(node) => node.type === "rule" && node.selector === "body"
);
if (bodyRule) {
bodyRule.nodes.find(
(node) => node.type === "decl" && node.prop === "color" && ["rgb(var(--foreground-rgb))", "var(--foreground)"].includes(
node.value
)
)?.remove();
bodyRule.nodes.find((node) => {
return node.type === "decl" && node.prop === "background" && (node.value.startsWith("linear-gradient") || node.value === "var(--background)");
})?.remove();
bodyRule.nodes.find(
(node) => node.type === "decl" && node.prop === "font-family" && node.value === "Arial, Helvetica, sans-serif"
)?.remove();
if (bodyRule.nodes.length === 0) {
bodyRule.remove();
}
}
removeConflictVars(root);
const darkRootRule = root.nodes.find(
(node) => node.type === "atrule" && node.params === "(prefers-color-scheme: dark)"
);
if (darkRootRule) {
removeConflictVars(darkRootRule);
if (darkRootRule.nodes.length === 0) {
darkRootRule.remove();
}
}
}
};
}
function addOrUpdateVars(baseLayer, selector, vars) {
let ruleNode = baseLayer.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!ruleNode) {
if (Object.keys(vars).length > 0) {
ruleNode = postcss.rule({
selector,
raws: { between: " ", before: "\n " }
});
baseLayer.append(ruleNode);
}
}
Object.entries(vars).forEach(([key, value]) => {
const prop = `--${key.replace(/^--/, "")}`;
const newDecl = postcss.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = ruleNode?.nodes.find(
(node) => node.type === "decl" && node.prop === prop
);
existingDecl ? existingDecl.replaceWith(newDecl) : ruleNode?.append(newDecl);
});
}
function updateCssVarsPluginV4(cssVars) {
return {
postcssPlugin: "update-css-vars-v4",
Once(root) {
Object.entries(cssVars).forEach(([key, vars]) => {
const selector = key === "light" ? ":root" : `.${key}`;
let ruleNode = root.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!ruleNode && Object.keys(vars).length > 0) {
ruleNode = postcss.rule({
selector,
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(ruleNode);
root.insertBefore(ruleNode, postcss.comment({ text: "---break---" }));
}
Object.entries(vars).forEach(([key2, value]) => {
let prop = `--${key2.replace(/^--/, "")}`;
if (prop === "--sidebar-background") {
prop = "--sidebar";
}
if (isLocalHSLValue(value)) {
value = `hsl(${value})`;
}
const newDecl = postcss.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = ruleNode?.nodes.find(
(node) => node.type === "decl" && node.prop === prop
);
if (!existingDecl) {
ruleNode?.append(newDecl);
}
});
});
}
};
}
function updateThemePlugin(cssVars) {
return {
postcssPlugin: "update-theme",
Once(root) {
const variables = Array.from(
new Set(
Object.keys(cssVars).flatMap(
(key) => Object.keys(cssVars[key] || {})
)
)
);
if (!variables.length) {
return;
}
const themeNode = upsertThemeNode(root);
const themeVarNodes = themeNode.nodes?.filter(
(node) => node.type === "decl" && node.prop.startsWith("--")
);
for (const variable of variables) {
const value = Object.values(cssVars).find((vars) => vars[variable])?.[variable];
if (!value) {
continue;
}
if (variable === "radius") {
const radiusVariables = {
sm: "calc(var(--radius) - 4px)",
md: "calc(var(--radius) - 2px)",
lg: "var(--radius)",
xl: "calc(var(--radius) + 4px)"
};
for (const [key, value2] of Object.entries(radiusVariables)) {
const cssVarNode2 = postcss.decl({
prop: `--radius-${key}`,