shadcn-vue
Version:
Add components to your apps.
3,993 lines • 127 kB
JavaScript
#!/usr/bin/env node
// src/commands/init.ts
import { promises as fs9 } 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";
var BUILD_MISSING_REGISTRY_FILE = "13";
// 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) {
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/registry/schema.ts
import { z as z3 } from "zod";
var registryItemTypeSchema = z3.enum([
"registry:lib",
"registry:block",
"registry:component",
"registry:ui",
"registry:hook",
"registry:page",
"registry:file",
"registry:theme",
"registry:style",
// Internal use only
"registry:example",
"registry:internal"
]);
var registryItemFileSchema = z3.discriminatedUnion("type", [
// Target is required for registry:file and registry:page
z3.object({
path: z3.string(),
content: z3.string().optional(),
type: z3.enum(["registry:file", "registry:page"]),
target: z3.string()
}),
z3.object({
path: z3.string(),
content: z3.string().optional(),
type: registryItemTypeSchema.exclude(["registry:file", "registry:page"]),
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({
theme: z3.record(z3.string(), z3.string()).optional(),
light: z3.record(z3.string(), z3.string()).optional(),
dark: z3.record(z3.string(), z3.string()).optional()
});
var registryItemCssSchema = z3.record(
z3.string(),
z3.lazy(
() => z3.union([
z3.string(),
z3.record(
z3.string(),
z3.union([z3.string(), z3.record(z3.string(), z3.string())])
)
])
)
);
var registryItemSchema = z3.object({
$schema: z3.string().optional(),
extends: z3.string().optional(),
name: z3.string(),
type: registryItemTypeSchema,
title: z3.string().optional(),
author: z3.string().min(2).optional(),
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(),
css: registryItemCssSchema.optional(),
meta: z3.record(z3.string(), z3.any()).optional(),
docs: z3.string().optional(),
categories: z3.array(z3.string()).optional()
});
var registrySchema = z3.object({
name: z3.string(),
homepage: z3.string(),
items: z3.array(registryItemSchema)
});
var registryIndexSchema = z3.array(registryItemSchema);
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: registryItemCssVarsSchema,
cssVarsV4: registryItemCssVarsSchema.optional(),
inlineColorsTemplate: z3.string(),
cssVarsTemplate: z3.string()
});
var registryResolvedItemsTreeSchema = registryItemSchema.pick({
dependencies: true,
devDependencies: true,
files: true,
tailwind: true,
cssVars: true,
css: true,
docs: true
});
// 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/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/registry/api.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;
var registryCache = /* @__PURE__ */ 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;
}
}
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 (path20) => {
const url = getRegistryUrl(path20);
if (registryCache.has(url)) {
return registryCache.get(url);
}
const response = await ofetch(url, { dispatcher: agent, parseResponse: JSON.parse }).catch((error) => {
throw new Error(error.data);
});
registryCache.set(url, response);
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: {
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 getRegistryUrl(path20) {
if (isUrl(path20)) {
const url = new URL(path20);
if (url.pathname.match(/\/chat\/b\//) && !url.pathname.endsWith("/json")) {
url.pathname = `${url.pathname}/json`;
}
return url.toString();
}
return `${REGISTRY_URL}/${path20}`;
}
function isUrl(path20) {
try {
new URL(path20);
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));
}
// src/utils/updaters/update-css.ts
import { promises as fs5 } from "node:fs";
import path7 from "pathe";
import postcss from "postcss";
async function updateCss(css, config, options) {
if (!config.resolvedPaths.tailwindCss || !css || Object.keys(css).length === 0) {
return;
}
options = {
silent: false,
...options
};
const cssFilepath = config.resolvedPaths.tailwindCss;
const cssFilepathRelative = path7.relative(
config.resolvedPaths.cwd,
cssFilepath
);
const cssSpinner = spinner(
`Updating ${highlighter.info(cssFilepathRelative)}`,
{
silent: options.silent
}
).start();
const raw = await fs5.readFile(cssFilepath, "utf8");
const output = await transformCss(raw, css);
await fs5.writeFile(cssFilepath, output, "utf8");
cssSpinner.succeed();
}
async function transformCss(input, css) {
const plugins = [updateCssPlugin(css)];
const result = await postcss(plugins).process(input, {
from: void 0
});
let output = result.css;
output = output.replace(/\/\* ---break--- \*\//g, "");
output = output.replace(/(\n\s*\n)+/g, "\n\n");
output = output.trimEnd();
return output;
}
function updateCssPlugin(css) {
return {
postcssPlugin: "update-css",
Once(root) {
for (const [selector, properties] of Object.entries(css)) {
if (selector.startsWith("@")) {
const atRuleMatch = selector.match(/@([a-z-]+)\s*(.*)/i);
if (!atRuleMatch)
continue;
const [, name, params] = atRuleMatch;
if (name === "keyframes") {
let themeInline = root.nodes?.find(
(node) => node.type === "atrule" && node.name === "theme" && node.params === "inline"
);
if (!themeInline) {
themeInline = postcss.atRule({
name: "theme",
params: "inline",
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(themeInline);
root.insertBefore(
themeInline,
postcss.comment({ text: "---break---" })
);
}
const keyframesRule = postcss.atRule({
name: "keyframes",
params,
raws: { semicolon: true, between: " ", before: "\n " }
});
themeInline.append(keyframesRule);
if (typeof properties === "object") {
for (const [step, stepProps] of Object.entries(properties)) {
processRule(keyframesRule, step, stepProps);
}
}
} else if (name === "utility") {
const utilityAtRule = root.nodes?.find(
(node) => node.type === "atrule" && node.name === name && node.params === params
);
if (!utilityAtRule) {
const atRule = postcss.atRule({
name,
params,
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(atRule);
root.insertBefore(
atRule,
postcss.comment({ text: "---break---" })
);
if (typeof properties === "object") {
for (const [prop, value] of Object.entries(properties)) {
if (typeof value === "string") {
const decl = postcss.decl({
prop,
value,
raws: { semicolon: true, before: "\n " }
});
atRule.append(decl);
} else if (typeof value === "object") {
processRule(atRule, prop, value);
}
}
}
} else {
if (typeof properties === "object") {
for (const [prop, value] of Object.entries(properties)) {
if (typeof value === "string") {
const existingDecl = utilityAtRule.nodes?.find(
(node) => node.type === "decl" && node.prop === prop
);
const decl = postcss.decl({
prop,
value,
raws: { semicolon: true, before: "\n " }
});
existingDecl ? existingDecl.replaceWith(decl) : utilityAtRule.append(decl);
} else if (typeof value === "object") {
processRule(utilityAtRule, prop, value);
}
}
}
}
} else {
processAtRule(root, name, params, properties);
}
} else {
processRule(root, selector, properties);
}
}
}
};
}
function processAtRule(root, name, params, properties) {
let atRule = root.nodes?.find(
(node) => node.type === "atrule" && node.name === name && node.params === params
);
if (!atRule) {
atRule = postcss.atRule({
name,
params,
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(atRule);
root.insertBefore(atRule, postcss.comment({ text: "---break---" }));
}
if (typeof properties === "object") {
for (const [childSelector, childProps] of Object.entries(properties)) {
if (childSelector.startsWith("@")) {
const nestedMatch = childSelector.match(/@([a-z-]+)\s*(.*)/i);
if (nestedMatch) {
const [, nestedName, nestedParams] = nestedMatch;
processAtRule(atRule, nestedName, nestedParams, childProps);
}
} else {
processRule(atRule, childSelector, childProps);
}
}
} else if (typeof properties === "string") {
try {
const parsed = postcss.parse(`.temp{${properties}}`);
const tempRule = parsed.first;
if (tempRule && tempRule.nodes) {
const rule = postcss.rule({
selector: "temp",
raws: { semicolon: true, between: " ", before: "\n " }
});
tempRule.nodes.forEach((node) => {
if (node.type === "decl") {
const clone = node.clone();
clone.raws.before = "\n ";
rule.append(clone);
}
});
if (rule.nodes?.length) {
atRule.append(rule);
}
}
} catch (error) {
console.error("Error parsing at-rule content:", properties, error);
throw error;
}
}
}
function processRule(parent, selector, properties) {
let rule = parent.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!rule) {
rule = postcss.rule({
selector,
raws: { semicolon: true, between: " ", before: "\n " }
});
parent.append(rule);
}
if (typeof properties === "object") {
for (const [prop, value] of Object.entries(properties)) {
if (typeof value === "string") {
const decl = postcss.decl({
prop,
value,
raws: { semicolon: true, before: "\n " }
});
const existingDecl = rule.nodes?.find(
(node) => node.type === "decl" && node.prop === prop
);
existingDecl ? existingDecl.replaceWith(decl) : rule.append(decl);
} else if (typeof value === "object") {
const nestedSelector = prop.startsWith("&") ? selector.replace(/^([^:]+)/, `$1${prop.substring(1)}`) : prop;
processRule(parent, nestedSelector, value);
}
}
} else if (typeof properties === "string") {
try {
const parsed = postcss.parse(`.temp{${properties}}`);
const tempRule = parsed.first;
if (tempRule && tempRule.nodes) {
tempRule.nodes.forEach((node) => {
if (node.type === "decl") {
const clone = node.clone();
clone.raws.before = "\n ";
rule?.append(clone);
}
});
}
} catch (error) {
console.error("Error parsing rule content:", selector, properties, error);
throw error;
}
}
}
// src/utils/updaters/update-css-vars.ts
import { promises as fs6 } from "node:fs";
import path8 from "node:path";
import postcss2 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",
overwriteCssVars: false,
initIndex: true,
...options
};
const cssFilepath = config.resolvedPaths.tailwindCss;
const cssFilepathRelative = path8.relative(
config.resolvedPaths.cwd,
cssFilepath
);
const cssVarsSpinner = spinner(
`Updating CSS variables in ${highlighter.info(cssFilepathRelative)}`,
{
silent: options.silent
}
).start();
const raw = await fs6.readFile(cssFilepath, "utf8");
const output = await transformCssVars(raw, cssVars ?? {}, config, {
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
tailwindVersion: options.tailwindVersion,
tailwindConfig: options.tailwindConfig,
overwriteCssVars: options.overwriteCssVars,
initIndex: options.initIndex
});
await fs6.writeFile(cssFilepath, output, "utf8");
cssVarsSpinner.succeed();
}
async function transformCssVars(input, cssVars, config, options = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
tailwindConfig: void 0,
overwriteCssVars: false,
initIndex: true
}) {
options = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
tailwindConfig: void 0,
overwriteCssVars: false,
initIndex: true,
...options
};
let plugins = [updateCssVarsPlugin(cssVars)];
if (options.cleanupDefaultNextStyles) {
plugins.push(cleanupDefaultNextStylesPlugin());
}
if (options.tailwindVersion === "v4") {
plugins = [];
if (config.resolvedPaths?.cwd) {
const packageInfo = getPackageInfo(config.resolvedPaths.cwd);
if (!packageInfo?.dependencies?.["tailwindcss-animate"] && !packageInfo?.devDependencies?.["tailwindcss-animate"] && options.initIndex) {
plugins.push(addCustomImport({ params: "tw-animate-css" }));
}
}
plugins.push(addCustomVariant({ params: "dark (&:is(.dark *))" }));
if (options.cleanupDefaultNextStyles) {
plugins.push(cleanupDefaultNextStylesPlugin());
}
plugins.push(
updateCssVarsPluginV4(cssVars, {
overwriteCssVars: options.overwriteCssVars
})
);
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 && options.initIndex) {
plugins.push(
updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion })
);
}
const result = await postcss2(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 = postcss2.atRule({
name: "layer",
params: "base",
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss2.comment({ text: "---break---" }));
}
requiredRules.forEach(({ selector, apply }) => {
const existingRule = baseLayer?.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!existingRule) {
baseLayer?.append(
postcss2.rule({
selector,
nodes: [
postcss2.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 = postcss2.atRule({
name: "layer",
params: "base",
nodes: [],
raws: {
semicolon: true,
before: "\n",
between: " "
}
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss2.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 = postcss2.rule({
selector,
raws: { between: " ", before: "\n " }
});
baseLayer.append(ruleNode);
}
}
Object.entries(vars).forEach(([key, value]) => {
const prop = `--${key.replace(/^--/, "")}`;
const newDecl = postcss2.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, options) {
return {
postcssPlugin: "update-css-vars-v4",
Once(root) {
Object.entries(cssVars).forEach(([key, vars]) => {
let selector = key === "light" ? ":root" : `.${key}`;
if (key === "theme") {
selector = "@theme";
const themeNode = upsertThemeNode(root);
Object.entries(vars).forEach(([key2, value]) => {
const prop = `--${key2.replace(/^--/, "")}`;
const newDecl = postcss2.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = themeNode?.nodes?.find(
(node) => node.type === "decl" && node.prop === prop
);
if (options.overwriteCssVars) {
if (existingDecl) {
existingDecl.replaceWith(newDecl);
} else {
themeNode?.append(newDecl);
}
} else {
if (!existingDecl) {
themeNode?.append(newDecl);
}
}
});
return;
}
let ruleNode = root.nodes?.find(
(node) => node.type === "rule" && node.selector === selector
);
if (!ruleNode && Object.keys(vars).length > 0) {
ruleNode = postcss2.rule({
selector,
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(ruleNode);
root.insertBefore(ruleNode, postcss2.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 = postcss2.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = ruleNode?.nodes.find(
(node) => node.type === "decl" && node.prop === prop
);
if (options.overwriteCssVars) {
if (existingDecl) {
existingDecl.replaceWith(newDecl);
} else {
ruleNode?.append(newDecl);
}
} else {
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 = postcss2.decl({
prop: `--radius-${key}`,
value: value2,
raws: { semicolon: true }
});
if (themeNode?.nodes?.find(
(node) => node.type === "decl" && node.prop === cssVarNode2.prop
)) {
continue;
}
themeNode?.append(cssVarNode2);
}
continue;
}
let prop = isLocalHSLValue(value) || isColorValue(value) ? `--color-${variable.replace(/^--/, "")}` : `--${variable.replace(/^--/, "")}`;
if (prop === "--color-sidebar-background") {
prop = "--color-sidebar";
}
let propValue = `var(--${variable})`;
if (prop === "--color-sidebar") {
propValue = "var(--sidebar)";
}
const cssVarNode = postcss2.decl({
prop,
value: propValue,
raws: { semicolon: true }
});
const existingDecl = themeNode?.nodes?.find(
(node) => node.type === "decl" && node.prop === cssVarNode.prop
);
if (!existingDecl) {
if (themeVarNodes?.length) {
themeNode?.insertAfter(
themeVarNodes[themeVarNodes.length - 1],
cssVarNode
);
} else {
themeNode?.append(cssVarNode);
}
}
}
}
};
}
function upsertThemeNode(root) {
let themeNode = root.nodes.find(
(node) => node.type === "atrule" && node.name === "theme" && node.params === "inline"
);
if (!themeNode) {
themeNode = postcss2.atRule({
name: "theme",
params: "inline",
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" }
});
root.append(themeNode);
root.insertBefore(themeNode, postcss2.comment({ text: "---break---" }));
}
return themeNode;
}
function addCustomVariant({ params }) {
return {
postcssPlugin: "add-custom-variant",
Once(root) {
const customVariant = root.nodes.find(
(node) => node.type === "atrule" && node.name === "custom-variant"
);
if (!customVariant) {
const importNodes = root.nodes.filter(
(node) => node.type === "atrule" && node.name === "import"
);
const variantNode = postcss2.atRule({
name: "custom-variant",
params,
raws: { semicolon: true, before: "\n" }
});
if (importNodes.length > 0) {
const lastImport = importNodes[importNodes.length - 1];
root.insertAfter(lastImport, variantNode);
} else {
root.insertAfter(root.nodes[0], variantNode);
}
root.insertBefore(variantNode, postcss2.comment({ text: "---break---" }));
}
}
};
}
function addCustomImport({ params }) {
return {
postcssPlugin: "add-custom-import",
Once(root) {
const importNodes = root.nodes.filter(
(node) => node.type === "atrule" && node.name === "import"
);
const customVariantNode = root.nodes.find(
(node) => node.type === "atrule" && node.name === "custom-variant"
);
const hasImport = importNodes.some(
(node) => node.params.replace(/["']/g, "") === params
);
if (!hasImport) {
const importNode = postcss2.atRule({
name: "import",
params: `"${params}"`,
raws: { semicolon: true, before: "\n" }
});
if (importNodes.length > 0) {
const lastImport = importNodes[importNodes.length - 1];
root.insertAfter(lastImport, importNode);
} else if (customVariantNode) {
root.insertBefore(customVariantNode, importNode);
root.insertBefore(
customVariantNode,
postcss2.comment({ text: "---break---" })
);
} else {
root.prepend(importNode);
root.insertAfter(importNode, postcss2.comment({ text: "---break---" }));
}
}
}
};
}
function updateTailwindConfigPlugin(tailwindConfig) {
return {
postcssPlugin: "update-tailwind-config",
Once(root) {
if (!tailwindConfig?.plugins) {
return;
}
const quoteType = getQuoteType(root);
const quote = quoteType === "single" ? "'" : '"';
const pluginNodes = root.nodes.filter(
(node) => node.type === "atrule" && node.name === "plugin"
);
const lastPluginNode = pluginNodes[pluginNodes.length - 1] || root.nodes[0];
for (const plugin of tailwindConfig.plugins) {
const pluginName = plugin.replace(/^require\(["']|["']\)$/g, "");
if (pluginNodes.some((node) => {
return node.params.replace(/["']/g, "") === pluginName;
})) {
continue;
}
const pluginNode = postcss2.atRule({
name: "plugin",
params: `${quote}${pluginName}${quote}`,
raws: { semicolon: true, before: "\n" }
});
root.insertAfter(lastPluginNode, pluginNode);
root.insertBefore(pluginNode, postcss2.comment({ text: "---break---" }));
}
}
};
}
function updateTailwindConfigKeyframesPlugin(tailwindConfig) {
return {
postcssPlugin: "update-tailwind-config-keyframes",
Once(root) {
if (!tailwindConfig?.theme?.extend?.keyframes) {
return;
}
const themeNode = upsertThemeNode(root);
const existingKeyFrameNodes = themeNode.nodes?.filter(
(node) => node.type === "atrule" && node.name === "keyframes"
);
const keyframeValueSchema = z5.record(
z5.string(),
z5.record(z5.string(), z5.string())
);
for (const [keyframeName, keyframeValue] of Object.entries(
tailwindConfig.theme.extend.keyframes
)) {
if (typeof keyframeName !== "string") {
continue;
}
const parsedKeyframeValue = keyframeValueSchema.safeParse(keyframeValue);
if (!parsedKeyframeValue.success) {
continue;
}
if (existingKeyFrameNodes?.find(
(node) => node.type === "atrule" && node.name === "keyframes" && node.params === keyframeName
)) {
continue;
}
const keyframeNode = postcss2.atRule({
name: "keyframes",
params: keyframeName,
nodes: [],
raws: { semicolon: true, between: " ", before: "\n " }
});
for (const [key, values] of Object.entries(parsedKeyframeValue.data)) {
const rule = postcss2.rule({
selector: key,
nodes: Object.entries(values).map(
([key2, value]) => postcss2.decl({
prop: key2,
value,
raws: { semicolon: true, before: "\n ", between: ": " }
})
),
raws: { semicolon: true, between: " ", before: "\n " }
});
keyframeNode.append(rule);
}
themeNode.append(keyframeNode);
themeNode.insertBefore(
keyframeNode,
postcss2.comment({ text: "---break---" })
);
}
}
};
}
function updateTailwindConfigAnimationPlugin(tailwindConfig) {
return {
postcssPlugin: "update-tailwind-config-animation",
Once(root) {
if (!tailwindConfig?.theme?.extend?.animation) {
return;
}
const themeNode = upsertThemeNode(root);
const existingAnimationNodes = themeNode.nodes?.filter(
(node) => node.type === "decl" && node.prop.startsWith("--animate-")
);
const parsedAnimationValue = z5.record(z5.string(), z5.string()).safeParse(tailwindConfig.theme.extend.animation);
if (!parsedAnimationValue.success) {
return;
}
for (const [key, value] of Object.entries(parsedAnimationValue.data)) {
const prop = `--animate-${key}`;
if (existingAnimationNodes?.find(
(node) => node.prop === prop
)) {
continue;
}
const animationNode = postcss2.decl({
prop,
value,
raws: { semicolon: true, between: ": ", before: "\n " }
});
themeNode.append(animationNode);
}
}
};
}
function getQuoteType(root) {
const firstNode = root.nodes[0];
const raw = firstNode.toString();
if (raw.includes("'")) {
return "single";
}
return "double";
}
function isLocalHSLValue(value) {
if (value.startsWith("hsl") || value.startsWith("rgb") || value.startsWith("#") || value.startsWith("oklch")) {
return false;
}
const chunks = value.split(" ");
return chunks.length === 3 && chunks.slice(1, 3).every((chunk) => chunk.includes("%"));
}
function isColorValue(value) {
return value.startsWith("hsl") || value.startsWith("rgb") || value.startsWith("#") || value.startsWith("oklch");
}
// src/utils/updaters/update-dependencies.ts
import { addDependency } from "nypm";
async function updateDependencies(dependencies, config, options) {
dependencies = Array.from(new Set(dependencies));
if (!dependencies?.length) {
return;
}
options = {
silent: false,
...options
};
const dependenciesSpinner = spinner(`Installing dependencies.`, { silent: options.silent })?.start();
dependenciesSpinner?.start();
await addDependency(dependencies, { cwd: config.resolvedPaths.cwd });
dependenciesSpinner?.succeed();
}
// src/utils/updaters/update-files.ts
import { existsSync, promises as fs7 } from "node:fs";
import { tmpdir as tmpdir2 } from "node:os";
// src/utils/transformers/transform-css-vars.ts
function transformCssVars2(opts) {
return {
type: "codemod",
name: "add prefix to tailwind classes",
transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) {
let transformCount = 0;
const { baseColor, config } = opts;
if (config.tailwind?.cssVariables || !baseColor?.inlineColors)
return transformCount;
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, {
visitLiteral(path20) {
if (path20.parent.value.type !== "ImportDeclaration" && typeof path20.node.value === "string") {
path20.node.value = applyColorMapping(path20.node.value.replace(/"/g, ""), baseColor.inlineColors);
transformCount++;
}
return this.traverse(path20);
}
});
}
if (sfcAST) {
traverseTemplateAST(sfcAST, {
enterNode(node) {
if (node.type === "Literal" && typeof node.value === "string") {
if (!["BinaryExpression", "Property"].includes(node.parent?.type ?? "")) {
node.value = applyColorMapping(node.value.replace(/"/g, ""), baseColor.inlineColors);
transformCount++;
}
} else if (node.type === "VLiteral" && typeof node.value === "string") {
if (node.parent.key.name === "class") {
node.value = `"${applyColorMapping(node.value.replace(/"/g, ""), baseColor.inlineColors)}"`;
transformCount++;
}
}
},
leaveNode() {
}
});
}
return transformCount;
}
};
}
function splitClassName(className) {
if (!className.includes("/") && !className.includes(":"))
return [null, className, null];
const parts = [];
const [rest, alpha] = className.split("/");
if (!rest.includes(":"))
return [null, rest, alpha];
const split = rest.split(":");
const name = split.pop();
const variant = split.join(":");
parts.push(variant ?? null, name ?? null, alpha ?? null);
return parts;
}
var PREFIXES = ["bg-", "text-", "border-", "ring-offset-", "ring-"];
function applyColorMapping(input, mapping) {
if (input.includes(" border "))
input = input.replace(" border ", " border border-border ");
const classNames = input.split(" ");
const lightMode = /* @__PURE__ */ new Set();
const darkMode = /* @__PURE__ */ new Set();
for (const className of classNames) {
const [variant, value, modifier] = splitClassName(className);
const prefix = PREFIXES.find((prefix2) => value?.startsWith(prefix2));
if (!prefix) {
if (!lightMode.has(className))
lightMode.add(className);
continue;
}
const needle = value?.replace(prefix, "");
if (needle && needle in mapping.light) {
lightMode.add(
[variant, `${prefix}${mapping.light[needle]}`].filter(Boolean).join(":") + (modifier ? `/${modifier}` : "")
);
darkMode.add(
["dark", variant, `${prefix}${mapping.dark[needle]}`].filter(Boolean).join(":") + (modifier ? `/${modifier}` : "")
);
continue;
}
if (!lightMode.has(className))
lightMode.add(className);
}
return [...Array.from(lightMode), ...Array.from(darkMode)].join(" ").trim();
}
// src/utils/transformers/transform-import.ts
function transformImport(opts) {
return {
type: "codemod",
name: "modify import based on user config",
transform({ scriptASTs, utils: { traverseScriptAST } }) {
const transformCount = 0;
const { config } = opts;
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, {
visitImportDeclaration(path20) {
if (typeof path20.node.source.value === "string") {
const sourcePath = path20.node.source.value;
if (sourcePath.startsWith("@/registry/")) {
if (config.aliases.ui) {
path20.node.source.value = sourcePath.replace(/^@\/registry\/[^/]+\/ui/, config.aliases.ui);
} else {
path20.node.source.value = sourcePath.replace(/^@\/registry\/[^/]+/, config.aliases.components);
}
}
if (sourcePath === "@/lib/utils") {
const namedImports = path20.node.specifiers?.map((node) => node.local?.name ?? "") ?? [];
const cnImport = namedImports.find((i) => i === "cn");
if (cnImport) {
path20.node.source.value = sourcePath.replace(/^@\/lib\/utils/, config.aliases.utils);
}
}
}
return this.traverse(path20);
}
});
}
return transformCount;
}
};
}
// src/utils/transformers/transform-sfc.ts
import { transform } from "@unovue/detypes";
async function transformSFC(opts) {
if (opts.config?.typescript)
return opts.raw;
return await transformByDetype(opts.raw, opts.filename).then((res) => res);
}
async function transformByDetype(content, filename) {
return await transform(content, filename, {
removeTsComments: true,
prettierOptions: {
proseWrap: "never"
}
});
}
// src/utils/transformers/transform-tw-prefix.ts
function transformTwPrefix(opts) {
return {
type: "codemod",
name: "add prefix to tailwind classes",
transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST, astHelpers } }) {
let transformCount = 0;
const { config } = opts;
const CLASS_IDENTIFIER = ["class", "classes"];
if (!config.tailwind?.prefix)
return transformCount;
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, {
visitCallExpression(path20) {
if (path20.node.callee.type === "Identifier" && path20.node.callee.name === "cva") {
const nodes = path20.node.arguments;
nodes.forEach((node) => {
if (node.type === "Literal" && typeof node.value === "string") {
node.value = applyPrefix(node.value, config.tailwind.prefix);
transformCount++;
} else if (node.type === "ObjectExpression") {
node.properties.forEach((node2) => {
if (node2.type === "Property" && node2.key.type === "Identifier" && node2.key.name === "variants") {
const nodes2 = astHelpers.findAll(node2, { type: "Literal" });
nodes2.forEach((node3) => {
if (typeof node3.value === "string") {
node3.value = applyPrefix(node3.value, config.tailwind.prefix);
transformCount++;
}
});
}
});
}
});
}
return this.traverse(path20);
}
});
}
if (sfcAST) {
traverseTemplateAST(sfcAST, {
enterNode(node) {
if (node.type === "VAttribute" && node.key.type === "VDirectiveKey") {
if (node.key.argument?.type === "VIdentifier") {
if (CLASS_IDENTIFIER.includes(node.key.argument.name)) {
const nodes = astHelpers.findAll(node, { type: "Literal" });
nodes.forEach((node2) => {
if (!["BinaryExpression", "Property"].includes(node2.parent?.type ?? "") && typeof node2.value === "string") {
node2.value = applyPrefix(node2.value, config.tailwind.prefix);
transformCount++;
}
});
}
}
} else if (node.type === "VLiteral" && typeof node.value === "string") {
if (CLASS_IDENTIFIER.includes(node.parent.key.name)) {
node.value = `"${applyPrefix(node.value.replace(/"/g, ""), config.tailwind.prefix)}"`;
transformCount++;
}
}
},
leaveNode() {
}
});
}
return transformCount;
}
};
}
function applyPrefix(input, prefix = "") {
const classNames = input.split(" ");
const prefixed = [];
for (const className of classNames) {
const [variant, value, modifier] = splitClassName(className);
if (variant) {
modifier ? prefixed.push(`${variant}:${prefix}${value}/${modifier}`) : prefixed.push(`${variant}:${prefix}${value}`);
} else {
modifier ? prefixed.push(`${prefix}${value}/${modifier}`) : prefixed.push(`${prefix}${value}`);
}
}
return prefixed.join(" ");
}
// src/utils/transformers/index.ts
import { transform as metaTransform } from "vue-metamorph";
// src/utils/icon-libraries.ts
var ICON_LIBRARIES = {
lucide: {
name: "lucide-vue-next",
package: "lucide-vue-next",
import: "lucide-vue-next"
},
radix: {
name: "@radix-icons/vue",
package: "@radix-icons/vue",
import: "@radix-icons/vue"
}
};
// src/utils/transformers/transform-icons.ts
var SOURCE_LIBRARY = "lucide";
function transformIcons(opts, registryIcons) {
return {
type: "codemod",
name: "modify import of icon library on user config",
transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) {
let transformCount = 0;
const { config } = opts;
if (!config.iconLibrary || !(config.iconLibrary in ICON_LIBRARIES)) {
return transformCount;
}
const sourceLibrary = SOURCE_LIBRARY;
const targetLibrary = config.iconLibrary;
if (sourceLibrary === targetLibrary) {
return transformCount;
}
const targetedIconsMap = /* @__PURE__ */ new Map();
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, {
visitImportDeclaration(path20) {
if (![ICON_LIBRARIES.radix.import, ICON_LIBRARIES.lucide.import].includes(`${path20.node.source.value}`))
return this.traverse(path20);
for (const specifier of path20.node.specifiers ?? []) {
if (specifier.type === "ImportSpecifier") {
const iconName = specifier.imported.name;
const targetedIcon = registryIcons[iconName]?.[targetLibrary];
if (!targetedIcon || targetedIconsMap.has(targetedIcon)) {
continue;
}
targetedIconsMap.set(iconName, targetedIcon);
specifier.imported.name = targetedIcon;
}
}
if (targetedIconsMap.size > 0)
path20.node.source.value = ICON_LIBRARIES[targetLibrary].import;
return this.traverse(path20);
}
});
if (sfcAST) {
traverseTemplateAST(sfcAST, {
enterNode(node) {
if (node.type === "VElement" && targetedIconsMap.has(node.rawName)) {
node.rawName = targetedIconsMap.get(node.rawName) ?? "";
transformCount++;
}
}
});
}
}
return transformCount;
}
};
}
// src/utils/transformers/index.ts
async function transform2(opts) {
const source = await transformSFC(opts);
const registryIcons = await getRegistryIcons();
return metaTransform(source, opts.filename, [
transformImport(opts),
transformCssVars2(opts),
transformTwPrefix(opts),
transformIcons(opts, registryIcons)
]).code;
}
// src/utils/updaters/update-files.ts
import path9, { basename, dirname } from "pathe";
import prompts from "prompts";
function resolveTargetDir(projectInfo, config, target) {
if (target.startsWith("~/")) {
return path9.join(config.resolvedPaths.cwd, target.replace("~/", ""));
}
return path9.join(config.resolvedPaths.cwd, target);
}
async function updateFiles(files, config, options) {
if (!files?.length) {
return;
}
options = {
overwrite: false,
force: false,
silent: false,
...options
};
const filesCreatedSpinner = spinner(`Updating files.`, {
silent: options.silent
})?.start();
const [projectInfo, baseColor] = await Promise.all([
getProjectInfo(config.resolvedPaths.cwd),
getRegistryBaseColor(config.tailwind.baseColor)
]);
const filesCreated = [];
const filesUpdated = [];
const folderSkipped = /* @__PURE__ */ new Map();
const filesSkipped = [];
let tempRoot = "";
if (!config.typescript) {
for (const file of files) {
if (!file.content) {
continue;
}
const dirName = path9.dirname(file.path);
tempRoot = path9.join(tmpdir2(), "shadcn-vue");
const tempDir = path9.join(tempRoot, "registry", config.style, dirName);
const tempPath = path9.join(tempRoot, "registry", config.style, file.path);
await fs7.mkdir(tempDir, { recursive: true });
await fs7.writeFile(tempPath, file.content, "utf-8");
}
await fs7.cp(path9.join(process.cwd(), "node_modules"), tempRoot, { recursive: true });
await fs7.writeFile(path9.join(tempRoot, "tsconfig.json"), `{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
},
"include": ["**/*.vue", "**/*.ts"],
"exclude": ["node_modules"]
}`, "utf8");
}
for (const file of files) {
if (!file.content) {
continue;
}
let targetDir = getRegistryItemFileTargetPath(file, config);
const fileName = basename(file.path);
let filePath = path9.join(targetDir, fileName);
if (file.target) {
filePath = resolveTargetDir(projectInfo, config, file.target);
targetDir = path9.dirname(filePath);
}
if (!config.typescript) {
filePath = filePath.replace(/\.ts?$/, (match) => ".js");
}
const existingFile = existsSync(filePath);
if (file.type === "registry:ui") {
const folderName = basename(dirname(filePath));
const existingFolder = existsSync(dirname(filePath));
if (!existingFolder) {
folderSkipped.set(folderName, false);
}
if (!folderSkipped.has(folderName) && !options.overwrite) {
filesCreatedSpinner.stop();
const { overwrite } = await prompts({
type: "confirm",
name: "overwrite",
message: `The folder ${highlighter.info(folderName)} already exists. Would you like to overwrite?`,
initial: false
});
folderSkipped.set(folderName, !overwrite);
filesCreatedSpinner?.start();
}
if (folderSkipped.get(folderName) === true) {
filesSkipped.push(path9.relative(config.resolvedPaths.cwd, filePath));
continue;
}
} else {
if (existingFile && !options.overwrite) {
filesCreatedSpinner.stop();
const { overwrite } = await prompts({
type: "confirm",
name: "overwrite",
message: `The file ${highlighter.info(
fileName
)} already exists. Would you like to overwrite?`,
initial: false
});
if (!overwrite) {
filesSkipped.push(path9.relative(config.resolvedPaths.cwd, filePath));
continue;
}
filesCreatedSpinner?.start();
}
}
if (!existsSync(targetDir)) {
await fs7.mkdir(targetDir, { recursive: true });
}
const content = await transform2({
filename: path9.join(tempRoot, "registry", config.style, file.path),
raw: file.content,
config,
baseColor
});
await fs7.writeFile(filePath, content, "utf-8");
existingFile ? filesUpdated.push(path9.relative(config.resolvedPaths.cwd, filePath)) : filesCreated.push(path9.relative(config.resolvedPaths.cwd, filePath));
}
if (tempRoot) {
await fs7.rm(tempRoot, { recursive: true });
}
const hasUpdatedFiles = filesCreated.length || filesUpdated.length;
if (!hasUpdatedFiles && !filesSkipped.length) {
filesCreatedSpinner?.info("No files updated.");
}
if (filesCreated.length) {
filesCreatedSpinner?.succeed(
`Created ${filesCreated.length} ${filesCreated.length === 1 ? "file" : "files"}:`
);
if (!options.silent) {
for (const file of filesCreated) {
logger.log(` - ${file}`);
}
}
} else {
filesCreatedSpinner?.stop();
}
if (filesUpdated.length) {
spinner(
`Updated ${filesUpdated.length} ${filesUpdated.length === 1 ? "file" : "files"}:`,
{
silent: options.silent
}
)?.info();
if (!options.silent) {
for (const file of filesUpdated) {
logger.log(` - ${file}`);
}
}
}
if (filesSkipped.length) {
spinner(
`Skipped ${filesSkipped.length} ${filesUpdated.length === 1 ? "file" : "files"}:`,
{
silent: options.silent
}
)?.info();
if (!options.silent) {
for (const file of filesSkipped) {
logger.log(` - ${file}`);
}
}
}
if (!options.silent) {
logger.break();
}
}
// src/utils/add-components.ts
import { z as z6 } from "zod";
async function addComponents(components, config, options) {
options = {
overwrite: false,
silent: false,
isNewProject: false,
style: "index",
...options
};
return await addProjectComponents(components, config, options);
}
async function addProjectComponents(components, config, options) {
const registrySpinner = spinner(`Checking registry.`, {
silent: options.silent
})?.start();
const tree = await registryResolveItemsTree(components, config);
if (!tree) {
registrySpinner?.fail();
return handleError(new Error("Failed to fetch components from registry."));
}
registrySpinner?.succeed();
const tailwindVersion = await getProjectTailwindVersionFromConfig(config);
await updateTailwindConfig(tree.tailwind?.config, config, {
silent: options.silent,
tailwindVersion
});
const overwriteCssVars = await shouldOverwriteCssVars(components, config);
await updateCssVars(tree.cssVars, config, {
cleanupDefaultNextStyles: options.isNewProject,
silent: options.silent,
tailwindVersion,
tailwindConfig: tree.tailwind?.config,
overwriteCssVars,
initIndex: options.style ? options.style === "index" : false
});
await updateCss(tree.css, config, {
silent: options.silent
});
await updateDependencies(tree.dependencies, config, {
silent: options.silent
});
await updateFiles(tree.files, config, {
overwrite: options.overwrite,
silent: options.silent
});
if (tree.docs) {
logger.info(tree.docs);
}
}
async function shouldOverwriteCssVars(components, config) {
const registryItems = await resolveRegistryItems(components, config);
const result = await fetchRegistry(registryItems);
const payload = z6.array(registryItemSchema).parse(result);
return payload.some(
(component) => component.type === "registry:theme" || component.type === "registry:style"
);
}
// src/utils/updaters/update-tailwind-content.ts
import { promises as fs8 } from "node:fs";
import path10 from "pathe";
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
async function updateTailwindContent(content, config, options) {
if (!content) {
return;
}
options = {
silent: false,
...options
};
const tailwindFileRelativePath = path10.relative(
config.resolvedPaths.cwd,
config.resolvedPaths.tailwindConfig
);
const tailwindSpinner = spinner(
`Updating ${highlighter.info(tailwindFileRelativePath)}`,
{
silent: options.silent
}
).start();
const raw = await fs8.readFile(config.resolvedPaths.tailwindConfig, "utf8");
const output = await transformTailwindContent(raw, content, config);
await fs8.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8");
tailwindSpinner?.succeed();
}
async function transformTailwindContent(input, content, config) {
const sourceFile = await _createSourceFile(input, config);
const configObject = sourceFile.getDescendantsOfKind(SyntaxKind2.ObjectLiteralExpression).find(
(node) => node.getProperties().some(
(property) => property.isKind(SyntaxKind2.PropertyAssignment) && property.getName() === "content"
)
);
if (!configObject) {
return input;
}
addTailwindConfigContent(configObject, content);
return sourceFile.getFullText();
}
async function addTailwindConfigContent(configObject, content) {
const quoteChar = _getQuoteChar(configObject);
const existingProperty = configObject.getProperty("content");
if (!existingProperty) {
const newProperty = {
name: "content",
initializer: `[${quoteChar}${content.join(
`${quoteChar}, ${quoteChar}`
)}${quoteChar}]`
};
configObject.addPropertyAssignment(newProperty);
return configObject;
}
if (existingProperty.isKind(SyntaxKind2.PropertyAssignment)) {
const initializer = existingProperty.getInitializer();
if (initializer?.isKind(SyntaxKind2.ArrayLiteralExpression)) {
for (const contentItem of content) {
const newValue = `${quoteChar}${contentItem}${quoteChar}`;
if (initializer.getElements().map((element) => element.getText()).includes(newValue)) {
continue;
}
initializer.addElement(newValue);
}
}
return configObject;
}
return configObject;
}
// src/commands/init.ts
import { Command } from "commander";
import path11 from "pathe";
import prompts2 from "prompts";
import { z as z7 } from "zod";
var initOptionsSchema = z7.object({
cwd: z7.string(),
components: z7.array(z7.string()).optional(),
yes: z7.boolean(),
defaults: z7.boolean(),
force: z7.boolean(),
silent: z7.boolean(),
isNewProject: z7.boolean(),
srcDir: z7.boolean().optional(),
cssVariables: z7.boolean(),
baseColor: z7.string().optional().refine(
(val) => {
if (val) {
return BASE_COLORS.find((color) => color.name === val);
}
return true;
},
{
message: `Invalid base color. Please use '${BASE_COLORS.map(
(color) => color.name
).join("', '")}'`
}
),
style: z7.string()
});
var init = new Command().name("init").description("initialize your project and install dependencies").argument(
"[components...]",
"the components to add or a url to the component."
).option("-y, --yes", "skip confirmation prompt.", true).option("-d, --defaults,", "use default configuration.", false).option("-f, --force", "force overwrite of existing configuration.", false).option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).option("-s, --silent", "mute output.", false).option("--css-variables", "use css variables for theming.", true).option("--no-css-variables", "do not use css variables for theming.").action(async (components, opts) => {
try {
const options = initOptionsSchema.parse({
cwd: path11.resolve(opts.cwd),
isNewProject: false,
components,
style: "index",
...opts
});
if (components.length > 0 && isUrl(components[0])) {
const item = await getRegistryItem(components[0], "");
if (item?.type === "registry:style") {
options.baseColor = "neutral";
options.style = item.extends ?? "index";
}
}
await runInit(options);
logger.log(
`${highlighter.success(
"Success!"
)} Project initialization completed.
You may now add components.`
);
logger.break();
} catch (error) {
logger.break();
handleError(error);
}
});
async function runInit(options) {
let projectInfo;
if (!options.skipPreflight) {
const preflight = await preFlightInit(options);
if (preflight.errors[MISSING_DIR_OR_EMPTY_PROJECT]) {
process.exit(1);
}
projectInfo = preflight.projectInfo;
} else {
projectInfo = await getProjectInfo(options.cwd);
}
const projectConfig = await getProjectConfig(options.cwd, projectInfo);
const config = projectConfig ? await promptForMinimalConfig(projectConfig, options) : await promptForConfig(await getConfig(options.cwd));
if (!options.yes) {
const { proceed } = await prompts2({
type: "confirm",
name: "proceed",
message: `Write configuration to ${highlighter.info(
"components.json"
)}. Proceed?`,
initial: true
});
if (!proceed) {
process.exit(0);
}
}
const componentSpinner = spinner(`Writing components.json.`).start();
const targetPath = path11.resolve(options.cwd, "components.json");
await fs9.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8");
componentSpinner.succeed();
const fullConfig = await resolveConfigPaths(options.cwd, config);
const components = [
...options.style === "none" ? [] : [options.style],
...options.components ?? []
];
await addComponents(components, fullConfig, {
// Init will always overwrite files.
overwrite: true,
silent: options.silent,
style: options.style,
isNewProject: options.isNewProject || projectInfo?.framework.name === "nuxt"
});
if (options.isNewProject && options.srcDir) {
await updateTailwindContent(
["./src/**/*.{js,ts,jsx,tsx,mdx}"],
fullConfig,
{
silent: options.silent
}
);
}
return fullConfig;
}
async function promptForConfig(defaultConfig = null) {
const [styles, baseColors] = await Promise.all([
getRegistryStyles(),
getRegistryBaseColors()
]);
logger.info("");
const options = await prompts2([
{
type: "toggle",
name: "typescript",
message: `Would you like to use ${highlighter.info(
"TypeScript"
)} (recommended)?`,
initial: defaultConfig?.typescript ?? true,
active: "yes",
inactive: "no"
},
{
type: "select",
name: "style",
message: `Which ${highlighter.info("style")} would you like to use?`,
choices: styles.map((style) => ({
title: style.name === "new-york" ? "New York (Recommended)" : style.label,
value: style.name
}))
},
{
type: "select",
name: "tailwindBaseColor",
message: `Which color would you like to use as the ${highlighter.info(
"base color"
)}?`,
choices: baseColors.map((color) => ({
title: color.label,
value: color.name
}))
},
{
type: "text",
name: "tailwindCss",
message: `Where is your ${highlighter.info("global CSS")} file?`,
initial: defaultConfig?.tailwind.css ?? DEFAULT_TAILWIND_CSS
},
{
type: "toggle",
name: "tailwindCssVariables",
message: `Would you like to use ${highlighter.info(
"CSS variables"
)} for theming?`,
initial: defaultConfig?.tailwind.cssVariables ?? true,
active: "yes",
inactive: "no"
},
{
type: "text",
name: "tailwindPrefix",
message: `Are you using a custom ${highlighter.info(
"tailwind prefix eg. tw-"
)}? (Leave blank if not)`,
initial: ""
},
{
type: "text",
name: "tailwindConfig",
message: `Where is your ${highlighter.info(
"tailwind.config.js"
)} located?`,
initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG
},
{
type: "text",
name: "components",
message: `Configure the import alias for ${highlighter.info(
"components"
)}:`,
initial: defaultConfig?.aliases.components ?? DEFAULT_COMPONENTS
},
{
type: "text",
name: "utils",
message: `Configure the import alias for ${highlighter.info("utils")}:`,
initial: defaultConfig?.aliases.utils ?? DEFAULT_UTILS
}
]);
return rawConfigSchema.parse({
$schema: "https://shadcn-vue.com/schema.json",
style: options.style,
tailwind: {
config: options.tailwindConfig,
css: options.tailwindCss,
baseColor: options.tailwindBaseColor,
cssVariables: options.tailwindCssVariables,
prefix: options.tailwindPrefix
},
typescript: options.typescript,
aliases: {
utils: options.utils,
components: options.components,
// TODO: fix this.
lib: options.components.replace(/\/components$/, "/lib"),
composables: options.components.replace(/\/components$/, "/composables")
}
});
}
async function promptForMinimalConfig(defaultConfig, opts) {
let style = defaultConfig.style;
let baseColor = defaultConfig.tailwind.baseColor;
let cssVariables = defaultConfig.tailwind.cssVariables;
if (!opts.defaults) {
const [styles, baseColors, tailwindVersion] = await Promise.all([
getRegistryStyles(),
getRegistryBaseColors(),
getProjectTailwindVersionFromConfig(defaultConfig)
]);
const options = await prompts2([
{
type: tailwindVersion === "v4" ? null : "select",
name: "style",
message: `Which ${highlighter.info("style")} would you like to use?`,
choices: styles.map((style2) => ({
title: style2.name === "new-york" ? "New York (Recommended)" : style2.label,
value: style2.name
})),
initial: 0
},
{
type: opts.baseColor ? null : "select",
name: "tailwindBaseColor",
message: `Which color would you like to use as the ${highlighter.info(
"base color"
)}?`,
choices: baseColors.map((color) => ({
title: color.label,
value: color.name
}))
}
]);
style = options.style ?? "new-york";
baseColor = options.tailwindBaseColor ?? baseColor;
cssVariables = opts.cssVariables;
}
return rawConfigSchema.parse({
$schema: defaultConfig?.$schema,
style,
tailwind: {
...defaultConfig?.tailwind,
baseColor,
cssVariables
},
typescript: defaultConfig.typescript,
aliases: defaultConfig?.aliases,
iconLibrary: defaultConfig?.iconLibrary
});
}
// src/preflights/preflight-add.ts
import fs10 from "fs-extra";
import path12 from "pathe";
async function preFlightAdd(options) {
const errors = {};
if (!fs10.existsSync(options.cwd) || !fs10.existsSync(path12.resolve(options.cwd, "package.json"))) {
errors[MISSING_DIR_OR_EMPTY_PROJECT] = true;
return {
errors,
config: null
};
}
if (!fs10.existsSync(path12.resolve(options.cwd, "components.json"))) {
errors[MISSING_CONFIG] = true;
return {
errors,
config: null
};
}
try {
const config = await getConfig(options.cwd);
return {
errors,
config
};
} catch (error) {
logger.break();
logger.error(
`An invalid ${highlighter.info(
"components.json"
)} file was found at ${highlighter.info(
options.cwd
)}.
Before you can add components, you must create a valid ${highlighter.info(
"components.json"
)} file by running the ${highlighter.info("init")} command.`
);
logger.error(
`Learn more at ${highlighter.info(
"https://shadcn-vue.com/docs/components-json"
)}.`
);
logger.break();
process.exit(1);
}
}
// src/commands/add.ts
import { Command as Command2 } from "commander";
import path13 from "pathe";
import prompts3 from "prompts";
import { z as z8 } from "zod";
var DEPRECATED_COMPONENTS = [
{
name: "toast",
deprecatedBy: "sonner",
message: "The toast component is deprecated. Use the sonner component instead."
},
{
name: "toaster",
deprecatedBy: "sonner",
message: "The toaster component is deprecated. Use the sonner component instead."
}
];
var addOptionsSchema = z8.object({
components: z8.array(z8.string()).optional(),
yes: z8.boolean(),
overwrite: z8.boolean(),
cwd: z8.string(),
all: z8.boolean(),
path: z8.string().optional(),
silent: z8.boolean(),
srcDir: z8.boolean().optional(),
cssVariables: z8.boolean()
});
var add = new Command2().name("add").description("add a component to your project").argument(
"[components...]",
"the components to add or a url to the component."
).option("-y, --yes", "skip confirmation prompt.", false).option("-o, --overwrite", "overwrite existing files.", false).option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).option("-a, --all", "add all available components", false).option("-p, --path <path>", "the path to add the component to.").option("-s, --silent", "mute output.", false).option(
"--src-dir",
"use the src directory when creating a new project.",
false
).option("--css-variables", "use css variables for theming.", true).option("--no-css-variables", "do not use css variables for theming.").action(async (components, opts) => {
try {
const options = addOptionsSchema.parse({
components,
cwd: path13.resolve(opts.cwd),
...opts
});
const isTheme = options.components?.some(
(component) => component.includes("theme-")
);
if (!options.yes && isTheme) {
logger.break();
const { confirm } = await prompts3({
type: "confirm",
name: "confirm",
message: highlighter.warn(
"You are about to install a new theme. \nExisting CSS variables will be overwritten. Continue?"
)
});
if (!confirm) {
logger.break();
logger.log("Theme installation cancelled.");
logger.break();
process.exit(1);
}
}
if (!options.components?.length) {
options.components = await promptForRegistryComponents(options);
}
const projectInfo = await getProjectInfo(options.cwd);
if (projectInfo?.tailwindVersion === "v4") {
const deprecatedComponents = DEPRECATED_COMPONENTS.filter(
(component) => options.components?.includes(component.name)
);
if (deprecatedComponents?.length) {
logger.break();
deprecatedComponents.forEach((component) => {
logger.warn(highlighter.warn(component.message));
});
logger.break();
process.exit(1);
}
}
let { errors, config } = await preFlightAdd(options);
if (errors[MISSING_CONFIG]) {
const { proceed } = await prompts3({
type: "confirm",
name: "proceed",
message: `You need to create a ${highlighter.info(
"components.json"
)} file to add components. Proceed?`,
initial: true
});
if (!proceed) {
logger.break();
process.exit(1);
}
config = await runInit({
cwd: options.cwd,
yes: true,
force: true,
defaults: false,
skipPreflight: false,
silent: true,
isNewProject: false,
srcDir: options.srcDir,
cssVariables: options.cssVariables,
style: "index"
});
}
if (!config) {
throw new Error(
`Failed to read config at ${highlighter.info(options.cwd)}.`
);
}
await addComponents(options.components, config, options);
} catch (error) {
logger.break();
handleError(error);
}
});
async function promptForRegistryComponents(options) {
const registryIndex = await getRegistryIndex();
if (!registryIndex) {
logger.break();
handleError(new Error("Failed to fetch registry index."));
return [];
}
if (options.all) {
return registryIndex.map((entry) => entry.name).filter(
(component) => !DEPRECATED_COMPONENTS.some((c) => c.name === component)
);
}
if (options.components?.length) {
return options.components;
}
const { components } = await prompts3({
type: "multiselect",
name: "components",
message: "Which components would you like to add?",
hint: "Space to select. A to toggle all. Enter to submit.",
instructions: false,
choices: registryIndex.filter(
(entry) => entry.type === "registry:ui" && !DEPRECATED_COMPONENTS.some(
(component) => component.name === entry.name
)
).map((entry) => ({
title: entry.name,
value: entry.name,
selected: options.all ? true : options.components?.includes(entry.name)
}))
});
if (!components?.length) {
logger.warn("No components selected. Exiting.");
logger.info("");
process.exit(1);
}
const result = z8.array(z8.string()).safeParse(components);
if (!result.success) {
logger.error("");
handleError(new Error("Something went wrong. Please try again."));
return [];
}
return result.data;
}
// src/commands/build.ts
import * as fs12 from "node:fs/promises";
// src/preflights/preflight-build.ts
import fs11 from "fs-extra";
import path14 from "pathe";
async function preFlightBuild(options) {
const errors = {};
const resolvePaths = {
cwd: options.cwd,
registryFile: path14.resolve(options.cwd, options.registryFile),
outputDir: path14.resolve(options.cwd, options.outputDir)
};
if (!fs11.existsSync(resolvePaths.registryFile)) {
errors[BUILD_MISSING_REGISTRY_FILE] = true;
}
await fs11.mkdir(resolvePaths.outputDir, { recursive: true });
if (Object.keys(errors).length > 0) {
if (errors[BUILD_MISSING_REGISTRY_FILE]) {
logger.break();
logger.error(
`The path ${highlighter.info(
resolvePaths.registryFile
)} does not exist.`
);
}
logger.break();
process.exit(1);
}
return {
errors,
resolvePaths
};
}
// src/commands/build.ts
import { Command as Command3 } from "commander";
import * as path15 from "pathe";
import { z as z9 } from "zod";
var buildOptionsSchema = z9.object({
cwd: z9.string(),
registryFile: z9.string(),
outputDir: z9.string()
});
var build = new Command3().name("build").description("build components for a shadcn registry").argument("[registry]", "path to registry.json file", "./registry.json").option(
"-o, --output <path>",
"destination directory for json files",
"./public/r"
).option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).action(async (registry, opts) => {
try {
const options = buildOptionsSchema.parse({
cwd: path15.resolve(opts.cwd),
registryFile: registry,
outputDir: opts.output
});
const { resolvePaths } = await preFlightBuild(options);
const content = await fs12.readFile(resolvePaths.registryFile, "utf-8");
const result = registrySchema.safeParse(JSON.parse(content));
if (!result.success) {
logger.error(
`Invalid registry file found at ${highlighter.info(
resolvePaths.registryFile
)}.`
);
process.exit(1);
}
const buildSpinner = spinner("Building registry...");
for (const registryItem of result.data.items) {
if (!registryItem.files) {
continue;
}
buildSpinner.start(`Building ${registryItem.name}...`);
registryItem.$schema = "https://shadcn-vue.com/schema/registry-item.json";
for (const file of registryItem.files) {
file.content = await fs12.readFile(
path15.resolve(resolvePaths.cwd, file.path),
"utf-8"
);
}
const result2 = registryItemSchema.safeParse(registryItem);
if (!result2.success) {
logger.error(
`Invalid registry item found for ${highlighter.info(
registryItem.name
)}.`
);
continue;
}
await fs12.writeFile(
path15.resolve(resolvePaths.outputDir, `${result2.data.name}.json`),
JSON.stringify(result2.data, null, 2)
);
}
buildSpinner.succeed("Building registry.");
} catch (error) {
logger.break();
handleError(error);
}
});
// src/commands/diff.ts
import { existsSync as existsSync2, promises as fs13 } from "node:fs";
import { Command as Command4 } from "commander";
import { diffLines } from "diff";
import path16 from "pathe";
import { z as z10 } from "zod";
var updateOptionsSchema = z10.object({
component: z10.string().optional(),
yes: z10.boolean(),
cwd: z10.string(),
path: z10.string().optional()
});
var diff = new Command4().name("diff").description("check for updates against the registry").argument("[component]", "the component name").option("-y, --yes", "skip confirmation prompt.", false).option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).action(async (name, opts) => {
try {
const options = updateOptionsSchema.parse({
component: name,
...opts
});
const cwd = path16.resolve(options.cwd);
if (!existsSync2(cwd)) {
logger.error(`The path ${cwd} does not exist. Please try again.`);
process.exit(1);
}
const config = await getConfig(cwd);
if (!config) {
logger.warn(
`Configuration is missing. Please run ${highlighter.success(
`init`
)} to create a components.json file.`
);
process.exit(1);
}
const registryIndex = await getRegistryIndex();
if (!registryIndex) {
handleError(new Error("Failed to fetch registry index."));
process.exit(1);
}
if (!options.component) {
const targetDir = config.resolvedPaths.components;
const projectComponents = registryIndex.filter((item) => {
for (const file of item.files ?? []) {
const filePath = path16.resolve(
targetDir,
typeof file === "string" ? file : file.path
);
if (existsSync2(filePath)) {
return true;
}
}
return false;
});
const componentsWithUpdates = [];
for (const component2 of projectComponents) {
const changes2 = await diffComponent(component2, config);
if (changes2.length) {
componentsWithUpdates.push({
name: component2.name,
changes: changes2
});
}
}
if (!componentsWithUpdates.length) {
logger.info("No updates found.");
process.exit(0);
}
logger.info("The following components have updates available:");
for (const component2 of componentsWithUpdates) {
logger.info(`- ${component2.name}`);
for (const change of component2.changes) {
logger.info(` - ${change.filePath}`);
}
}
logger.break();
logger.info(
`Run ${highlighter.success(`diff <component>`)} to see the changes.`
);
process.exit(0);
}
const component = registryIndex.find(
(item) => item.name === options.component
);
if (!component) {
logger.error(
`The component ${highlighter.success(
options.component
)} does not exist.`
);
process.exit(1);
}
const changes = await diffComponent(component, config);
if (!changes.length) {
logger.info(`No updates found for ${options.component}.`);
process.exit(0);
}
for (const change of changes) {
logger.info(`- ${change.filePath}`);
await printDiff(change.patch);
logger.info("");
}
} catch (error) {
handleError(error);
}
});
async function diffComponent(component, config) {
const payload = await fetchTree(config.style, [component]);
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor);
if (!payload) {
return [];
}
const changes = [];
for (const item of payload) {
const targetDir = await getItemTargetPath(config, item);
if (!targetDir) {
continue;
}
for (const file of item.files ?? []) {
const filePath = path16.resolve(
targetDir,
typeof file === "string" ? file : file.path
);
if (!existsSync2(filePath)) {
continue;
}
const fileContent = await fs13.readFile(filePath, "utf8");
if (typeof file === "string" || !file.content) {
continue;
}
const registryContent = await transform2({
filename: file.path,
raw: file.content,
config,
baseColor
});
const patch = diffLines(registryContent, fileContent);
if (patch.length > 1) {
changes.push({
filePath,
patch
});
}
}
}
return changes;
}
async function printDiff(diff2) {
diff2.forEach((part) => {
if (part) {
if (part.added) {
return process.stdout.write(highlighter.success(part.value));
}
if (part.removed) {
return process.stdout.write(highlighter.error(part.value));
}
return process.stdout.write(part.value);
}
});
}
// src/commands/info.ts
import { Command as Command5 } from "commander";
import consola3 from "consola";
var info = new Command5().name("info").description("get information about your project").option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).action(async (opts) => {
logger.info("> project info");
consola3.log(await getProjectInfo(opts.cwd));
logger.break();
logger.info("> components.json");
consola3.log(await getConfig(opts.cwd));
});
// src/migrations/migrate-icons.ts
import { randomBytes } from "node:crypto";
import { promises as fs14 } from "node:fs";
import { tmpdir as tmpdir3 } from "node:os";
import path17 from "pathe";
import prompts4 from "prompts";
import { glob as glob2 } from "tinyglobby";
import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind3 } from "ts-morph";
async function migrateIcons(config) {
if (!config.resolvedPaths.ui) {
throw new Error(
"We could not find a valid `ui` path in your `components.json` file. Please ensure you have a valid `ui` path in your `components.json` file."
);
}
const uiPath = config.resolvedPaths.ui;
const [files, registryIcons] = await Promise.all([
glob2("**/*.{js,ts,jsx,tsx}", {
cwd: uiPath
}),
getRegistryIcons()
]);
if (Object.keys(registryIcons).length === 0) {
throw new Error("Something went wrong fetching the registry icons.");
}
const libraryChoices = Object.entries(ICON_LIBRARIES).map(
([name, iconLibrary]) => ({
title: iconLibrary.name,
value: name
})
);
const migrateOptions = await prompts4([
{
type: "select",
name: "sourceLibrary",
message: `Which icon library would you like to ${highlighter.info(
"migrate from"
)}?`,
choices: libraryChoices
},
{
type: "select",
name: "targetLibrary",
message: `Which icon library would you like to ${highlighter.info(
"migrate to"
)}?`,
choices: libraryChoices
}
]);
if (migrateOptions.sourceLibrary === migrateOptions.targetLibrary) {
throw new Error(
"You cannot migrate to the same icon library. Please choose a different icon library."
);
}
if (!(migrateOptions.sourceLibrary in ICON_LIBRARIES && migrateOptions.targetLibrary in ICON_LIBRARIES)) {
throw new Error("Invalid icon library. Please choose a valid icon library.");
}
const sourceLibrary = ICON_LIBRARIES[migrateOptions.sourceLibrary];
const targetLibrary = ICON_LIBRARIES[migrateOptions.targetLibrary];
const { confirm } = await prompts4({
type: "confirm",
name: "confirm",
initial: true,
message: `We will migrate ${highlighter.info(
files.length
)} files in ${highlighter.info(
`./${path17.relative(config.resolvedPaths.cwd, uiPath)}`
)} from ${highlighter.info(sourceLibrary.name)} to ${highlighter.info(
targetLibrary.name
)}. Continue?`
});
if (!confirm) {
logger.info("Migration cancelled.");
process.exit(0);
}
if (targetLibrary.package) {
await updateDependencies([targetLibrary.package], config, {
silent: false
});
}
const migrationSpinner = spinner(`Migrating icons...`)?.start();
await Promise.all(
files.map(async (file) => {
migrationSpinner.text = `Migrating ${file}...`;
const filePath = path17.join(uiPath, file);
const fileContent = await fs14.readFile(filePath, "utf-8");
const content = await migrateIconsFile(
fileContent,
migrateOptions.sourceLibrary,
migrateOptions.targetLibrary,
registryIcons
);
await fs14.writeFile(filePath, content);
})
);
migrationSpinner.succeed("Migration complete.");
}
async function migrateIconsFile(content, sourceLibrary, targetLibrary, iconsMapping) {
const sourceLibraryImport = ICON_LIBRARIES[sourceLibrary]?.import;
const targetLibraryImport = ICON_LIBRARIES[targetLibrary]?.import;
const dir = await fs14.mkdtemp(path17.join(tmpdir3(), "shadcn-"));
const project = new Project2({
compilerOptions: {}
});
const tempFile = path17.join(
dir,
`shadcn-icons-${randomBytes(4).toString("hex")}.tsx`
);
const sourceFile = project.createSourceFile(tempFile, content, {
scriptKind: ScriptKind2.TSX
});
const targetedIcons = [];
for (const importDeclaration of sourceFile.getImportDeclarations() ?? []) {
if (importDeclaration.getModuleSpecifier()?.getText() !== `"${sourceLibraryImport}"`) {
continue;
}
for (const specifier of importDeclaration.getNamedImports() ?? []) {
const iconName = specifier.getName();
const targetedIcon = Object.values(iconsMapping).find(
(icon) => icon[sourceLibrary] === iconName
)?.[targetLibrary];
if (!targetedIcon || targetedIcons.includes(targetedIcon)) {
continue;
}
targetedIcons.push(targetedIcon);
specifier.remove();
sourceFile.getDescendantsOfKind(SyntaxKind3.JsxSelfClosingElement).filter((node) => node.getTagNameNode()?.getText() === iconName).forEach((node) => node.getTagNameNode()?.replaceWithText(targetedIcon));
}
if (importDeclaration.getNamedImports()?.length === 0) {
importDeclaration.remove();
}
}
if (targetedIcons.length > 0) {
sourceFile.addImportDeclaration({
moduleSpecifier: targetLibraryImport,
namedImports: targetedIcons.map((icon) => ({
name: icon
}))
});
}
return await sourceFile.getText();
}
// src/preflights/preflight-migrate.ts
import fs15 from "fs-extra";
import path18 from "pathe";
async function preFlightMigrate(options) {
const errors = {};
if (!fs15.existsSync(options.cwd) || !fs15.existsSync(path18.resolve(options.cwd, "package.json"))) {
errors[MISSING_DIR_OR_EMPTY_PROJECT] = true;
return {
errors,
config: null
};
}
if (!fs15.existsSync(path18.resolve(options.cwd, "components.json"))) {
errors[MISSING_CONFIG] = true;
return {
errors,
config: null
};
}
try {
const config = await getConfig(options.cwd);
return {
errors,
config
};
} catch (error) {
logger.break();
logger.error(
`An invalid ${highlighter.info(
"components.json"
)} file was found at ${highlighter.info(
options.cwd
)}.
Before you can run a migration, you must create a valid ${highlighter.info(
"components.json"
)} file by running the ${highlighter.info("init")} command.`
);
logger.error(
`Learn more at ${highlighter.info(
"https://shadcn-vue.com/docs/components-json"
)}.`
);
logger.break();
process.exit(1);
}
}
// src/commands/migrate.ts
import { Command as Command6 } from "commander";
import consola4 from "consola";
import path19 from "pathe";
import { z as z11 } from "zod";
var migrations = [
{
name: "icons",
description: "migrate your ui components to a different icon library."
}
];
var migrateOptionsSchema = z11.object({
cwd: z11.string(),
list: z11.boolean(),
migration: z11.string().refine(
(value) => value && migrations.some((migration) => migration.name === value),
{
message: "You must specify a valid migration. Run `shadcn migrate --list` to see available migrations."
}
).optional()
});
var migrate = new Command6().name("migrate").description("run a migration.").argument("[migration]", "the migration to run.").option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
).option("-l, --list", "list all migrations.", false).action(async (migration, opts) => {
try {
const options = migrateOptionsSchema.parse({
cwd: path19.resolve(opts.cwd),
migration,
list: opts.list
});
if (options.list || !options.migration) {
consola4.info("Available migrations:");
for (const migration2 of migrations) {
consola4.info(`- ${migration2.name}: ${migration2.description}`);
}
return;
}
if (!options.migration) {
throw new Error(
"You must specify a migration. Run `shadcn migrate --list` to see available migrations."
);
}
const { errors, config } = await preFlightMigrate(options);
if (errors[MISSING_DIR_OR_EMPTY_PROJECT] || errors[MISSING_CONFIG]) {
throw new Error(
"No `components.json` file found. Ensure you are at the root of your project."
);
}
if (!config) {
throw new Error(
"Something went wrong reading your `components.json` file. Please ensure you have a valid `components.json` file."
);
}
if (options.migration === "icons") {
await migrateIcons(config);
}
} catch (error) {
handleError(error);
}
});
// src/index.ts
import { Command as Command7 } from "commander";
// package.json
var package_default = {
name: "shadcn-vue",
type: "module",
version: "2.0.1",
description: "Add components to your apps.",
publishConfig: {
access: "public"
},
license: "MIT",
repository: {
type: "git",
url: "https://github.com/unovue/shadcn-vue.git",
directory: "packages/cli"
},
keywords: [
"components",
"ui",
"vue",
"nuxt",
"tailwind",
"radix-ui",
"radix-vue",
"reka-ui",
"shadcn",
"shadcn-vue"
],
exports: "./dist/index.js",
bin: "./dist/index.js",
files: [
"dist"
],
scripts: {
dev: "tsup --watch",
build: "tsup",
typecheck: "tsc --noEmit",
clean: "node ./scripts/rimraf.js",
lint: "eslint .",
"lint:fix": "eslint --fix .",
"start:dev": "REGISTRY_URL=http://localhost:5173/r node dist/index.js",
start: "node dist/index.js",
release: "changeset version",
"pub:beta": "pnpm build && pnpm publish --no-git-checks --access public --tag beta",
"pub:next": "pnpm build && pnpm publish --no-git-checks --access public --tag next",
"pub:release": "pnpm build && pnpm publish --no-git-checks --access public",
test: "vitest run",
"test:update": "vitest run -u",
"test:ui": "vitest --ui"
},
peerDependencies: {
"@vitest/ui": "*",
vitest: "*"
},
dependencies: {
"@unovue/detypes": "^0.8.5",
"@vue/compiler-sfc": "^3.5",
commander: "^12.1.0",
consola: "^3.4.0",
cosmiconfig: "^9.0.0",
deepmerge: "^4.3.1",
diff: "^7.0.0",
"fs-extra": "^11.3.0",
"get-tsconfig": "^4.10.0",
"lodash-es": "^4.17.21",
"magic-string": "^0.30.17",
nypm: "^0.5.2",
ofetch: "^1.4.1",
ora: "^8.2.0",
pathe: "^2.0.3",
"pkg-types": "^1.3.1",
postcss: "^8.5.2",
prompts: "^2.4.2",
"reka-ui": "catalog:",
"stringify-object": "^5.0.0",
tailwindcss: "^3.4.16",
tinyexec: "^0.3.2",
tinyglobby: "^0.2.10",
"ts-morph": "^24.0.0",
undici: "^7.3.0",
"vue-metamorph": "3.2.0",
zod: "^3.24.2"
},
devDependencies: {
"@types/diff": "^7.0.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.4",
"@types/prompts": "^2.4.9",
"@types/stringify-object": "^4.0.5",
msw: "^2.7.3",
tsup: "^8.3.6",
"type-fest": "^4.34.1",
typescript: "catalog:"
}
};
// src/index.ts
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
async function main() {
const program = new Command7().name("shadcn-vue").description("add components and dependencies to your project").version(
package_default.version || "1.0.0",
"-v, --version",
"display the version number"
);
program.addCommand(init).addCommand(add).addCommand(diff).addCommand(migrate).addCommand(info).addCommand(build);
program.parse();
}
main();
//# sourceMappingURL=index.js.map