shadcn-vue
Version:
Add components to your apps.
1,307 lines (1,293 loc) • 99.5 kB
JavaScript
#!/usr/bin/env node
import { BASE_COLORS, DEFAULT_COMPONENTS, DEFAULT_TAILWIND_CONFIG, DEFAULT_TAILWIND_CSS, DEFAULT_UTILS, _createSourceFile, _getQuoteChar, clearRegistryCache, fetchRegistry, fetchTree, findCommonRoot, findPackageRoot, getConfig, getItemTargetPath, getPackageInfo, getProjectConfig, getProjectInfo, getProjectTailwindVersionFromConfig, getRegistryBaseColor, getRegistryBaseColors, getRegistryIcons, getRegistryIndex, getRegistryItem, getRegistryParentMap, getRegistryStyles, getRegistryTypeAliasMap, getTSConfig, getWorkspaceConfig, handleError, highlighter, isUrl, logger, rawConfigSchema, registryGetTheme, registryItemSchema, registryResolveItemsTree, registrySchema, resolveConfigPaths, resolveRegistryItems, resolveTree, spinner, updateTailwindConfig } from "./registry-ByrmygPr.js";
import { Command } from "commander";
import * as path$1 from "pathe";
import path, { basename, dirname } from "pathe";
import prompts from "prompts";
import { z } from "zod";
import { existsSync, promises } from "node:fs";
import fs from "fs-extra";
import { glob } from "tinyglobby";
import consola from "consola";
import { tmpdir } from "node:os";
import { Project, ScriptKind, SyntaxKind } from "ts-morph";
import postcss from "postcss";
import AtRule from "postcss/lib/at-rule";
import { addDependency } from "nypm";
import { transform } from "vue-metamorph";
import { transform as transform$1 } from "@unovue/detypes";
import * as fs$1 from "node:fs/promises";
import { diffLines } from "diff";
import { randomBytes } from "node:crypto";
//#region src/utils/errors.ts
const MISSING_DIR_OR_EMPTY_PROJECT = "1";
const MISSING_CONFIG = "3";
const TAILWIND_NOT_CONFIGURED = "5";
const IMPORT_ALIAS_MISSING = "6";
const UNSUPPORTED_FRAMEWORK = "7";
const BUILD_MISSING_REGISTRY_FILE = "13";
//#endregion
//#region src/preflights/preflight-init.ts
async function preFlightInit(options) {
const errors = {};
if (!fs.existsSync(options.cwd) || !fs.existsSync(path.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 (fs.existsSync(path.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)}.\nTo 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)}.\nVisit ${highlighter.info(projectInfo?.framework.links.installation)} to manually configure your project.\nOnce 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
};
}
//#endregion
//#region src/utils/updaters/update-css.ts
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 = path.relative(config.resolvedPaths.cwd, cssFilepath);
const cssSpinner = spinner(`Updating ${highlighter.info(cssFilepathRelative)}`, { silent: options.silent }).start();
const raw = await promises.readFile(cssFilepath, "utf8");
const output = await transformCss(raw, css);
await promises.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$1, params] = atRuleMatch;
if (name$1 === "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$1 === "utility") {
const utilityAtRule = root.nodes?.find((node) => node.type === "atrule" && node.name === name$1 && node.params === params);
if (!utilityAtRule) {
const atRule = postcss.atRule({
name: name$1,
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$1, params, properties);
} else processRule(root, selector, properties);
}
};
}
function processAtRule(root, name$1, params, properties) {
let atRule = root.nodes?.find((node) => node.type === "atrule" && node.name === name$1 && node.params === params);
if (!atRule) {
atRule = postcss.atRule({
name: name$1,
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;
}
}
//#endregion
//#region src/utils/updaters/update-css-vars.ts
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 = path.relative(config.resolvedPaths.cwd, cssFilepath);
const cssVarsSpinner = spinner(`Updating CSS variables in ${highlighter.info(cssFilepathRelative)}`, { silent: options.silent }).start();
const raw = await promises.readFile(cssFilepath, "utf8");
const output = await transformCssVars$1(raw, cssVars ?? {}, config, {
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
tailwindVersion: options.tailwindVersion,
tailwindConfig: options.tailwindConfig,
overwriteCssVars: options.overwriteCssVars,
initIndex: options.initIndex
});
await promises.writeFile(cssFilepath, output, "utf8");
cssVarsSpinner.succeed();
}
async function transformCssVars$1(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 postcss(plugins).process(input, { from: void 0 });
let output = result.css;
output = output.replace(/\/\* ---break--- \*\//g, "");
if (options.tailwindVersion === "v4") output = output.replace(/(\n\s*\n)+/g, "\n\n");
return output;
}
function updateBaseLayerPlugin({ tailwindVersion }) {
return {
postcssPlugin: "update-base-layer",
Once(root) {
const requiredRules = [{
selector: "*",
apply: tailwindVersion === "v4" ? "border-border outline-ring/50" : "border-border"
}, {
selector: "body",
apply: "bg-background text-foreground"
}];
let baseLayer = root.nodes.find((node) => node.type === "atrule" && node.name === "layer" && node.params === "base" && requiredRules.every(({ selector, apply }) => node.nodes?.some((rule) => rule.type === "rule" && rule.selector === selector && rule.nodes.some((applyRule) => applyRule.type === "atrule" && applyRule.name === "apply" && applyRule.params === apply))));
if (!baseLayer) {
baseLayer = postcss.atRule({
name: "layer",
params: "base",
raws: {
semicolon: true,
between: " ",
before: "\n"
}
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss.comment({ text: "---break---" }));
}
requiredRules.forEach(({ selector, apply }) => {
const existingRule = baseLayer?.nodes?.find((node) => node.type === "rule" && node.selector === selector);
if (!existingRule) baseLayer?.append(postcss.rule({
selector,
nodes: [postcss.atRule({
name: "apply",
params: apply,
raws: {
semicolon: true,
before: "\n "
}
})],
raws: {
semicolon: true,
between: " ",
before: "\n "
}
}));
});
}
};
}
function updateCssVarsPlugin(cssVars) {
return {
postcssPlugin: "update-css-vars",
Once(root) {
let baseLayer = root.nodes.find((node) => node.type === "atrule" && node.name === "layer" && node.params === "base");
if (!(baseLayer instanceof AtRule)) {
baseLayer = postcss.atRule({
name: "layer",
params: "base",
nodes: [],
raws: {
semicolon: true,
before: "\n",
between: " "
}
});
root.append(baseLayer);
root.insertBefore(baseLayer, postcss.comment({ text: "---break---" }));
}
if (baseLayer !== void 0) Object.entries(cssVars).forEach(([key, vars]) => {
const selector = key === "light" ? ":root" : `.${key}`;
addOrUpdateVars(baseLayer, selector, vars);
});
}
};
}
function removeConflictVars(root) {
const rootRule = root.nodes.find((node) => node.type === "rule" && node.selector === ":root");
if (rootRule) {
const propsToRemove = ["--background", "--foreground"];
rootRule.nodes.filter((node) => node.type === "decl" && propsToRemove.includes(node.prop)).forEach((node) => node.remove());
if (rootRule.nodes.length === 0) rootRule.remove();
}
}
function cleanupDefaultNextStylesPlugin() {
return {
postcssPlugin: "cleanup-default-next-styles",
Once(root) {
const bodyRule = root.nodes.find((node) => node.type === "rule" && node.selector === "body");
if (bodyRule) {
bodyRule.nodes.find((node) => node.type === "decl" && node.prop === "color" && ["rgb(var(--foreground-rgb))", "var(--foreground)"].includes(node.value))?.remove();
bodyRule.nodes.find((node) => {
return node.type === "decl" && node.prop === "background" && (node.value.startsWith("linear-gradient") || node.value === "var(--background)");
})?.remove();
bodyRule.nodes.find((node) => node.type === "decl" && node.prop === "font-family" && node.value === "Arial, Helvetica, sans-serif")?.remove();
if (bodyRule.nodes.length === 0) bodyRule.remove();
}
removeConflictVars(root);
const darkRootRule = root.nodes.find((node) => node.type === "atrule" && node.params === "(prefers-color-scheme: dark)");
if (darkRootRule) {
removeConflictVars(darkRootRule);
if (darkRootRule.nodes.length === 0) darkRootRule.remove();
}
}
};
}
function addOrUpdateVars(baseLayer, selector, vars) {
let ruleNode = baseLayer.nodes?.find((node) => node.type === "rule" && node.selector === selector);
if (!ruleNode) {
if (Object.keys(vars).length > 0) {
ruleNode = postcss.rule({
selector,
raws: {
between: " ",
before: "\n "
}
});
baseLayer.append(ruleNode);
}
}
Object.entries(vars).forEach(([key, value]) => {
const prop = `--${key.replace(/^--/, "")}`;
const newDecl = postcss.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = ruleNode?.nodes.find((node) => node.type === "decl" && node.prop === prop);
existingDecl ? existingDecl.replaceWith(newDecl) : ruleNode?.append(newDecl);
});
}
function updateCssVarsPluginV4(cssVars, 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(([key$1, value]) => {
const prop = `--${key$1.replace(/^--/, "")}`;
const newDecl = postcss.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 = postcss.rule({
selector,
nodes: [],
raws: {
semicolon: true,
between: " ",
before: "\n"
}
});
root.append(ruleNode);
root.insertBefore(ruleNode, postcss.comment({ text: "---break---" }));
}
Object.entries(vars).forEach(([key$1, value]) => {
let prop = `--${key$1.replace(/^--/, "")}`;
if (prop === "--sidebar-background") prop = "--sidebar";
if (isLocalHSLValue(value)) value = `hsl(${value})`;
const newDecl = postcss.decl({
prop,
value,
raws: { semicolon: true }
});
const existingDecl = ruleNode?.nodes.find((node) => node.type === "decl" && node.prop === prop);
if (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, value$1] of Object.entries(radiusVariables)) {
const cssVarNode$1 = postcss.decl({
prop: `--radius-${key}`,
value: value$1,
raws: { semicolon: true }
});
if (themeNode?.nodes?.find((node) => node.type === "decl" && node.prop === cssVarNode$1.prop)) continue;
themeNode?.append(cssVarNode$1);
}
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 = postcss.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 = postcss.atRule({
name: "theme",
params: "inline",
nodes: [],
raws: {
semicolon: true,
between: " ",
before: "\n"
}
});
root.append(themeNode);
root.insertBefore(themeNode, postcss.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 = postcss.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, postcss.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 = postcss.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, postcss.comment({ text: "---break---" }));
} else {
root.prepend(importNode);
root.insertAfter(importNode, postcss.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 = postcss.atRule({
name: "plugin",
params: `${quote}${pluginName}${quote}`,
raws: {
semicolon: true,
before: "\n"
}
});
root.insertAfter(lastPluginNode, pluginNode);
root.insertBefore(pluginNode, postcss.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 = z.record(z.string(), z.record(z.string(), z.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 = postcss.atRule({
name: "keyframes",
params: keyframeName,
nodes: [],
raws: {
semicolon: true,
between: " ",
before: "\n "
}
});
for (const [key, values] of Object.entries(parsedKeyframeValue.data)) {
const rule = postcss.rule({
selector: key,
nodes: Object.entries(values).map(([key$1, value]) => postcss.decl({
prop: key$1,
value,
raws: {
semicolon: true,
before: "\n ",
between: ": "
}
})),
raws: {
semicolon: true,
between: " ",
before: "\n "
}
});
keyframeNode.append(rule);
}
themeNode.append(keyframeNode);
themeNode.insertBefore(keyframeNode, postcss.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 = z.record(z.string(), z.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 = postcss.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");
}
//#endregion
//#region src/utils/updaters/update-dependencies.ts
async function updateDependencies(dependencies$1, config, options) {
dependencies$1 = Array.from(new Set(dependencies$1));
if (!dependencies$1?.length) return;
options = {
silent: false,
...options
};
const dependenciesSpinner = spinner(`Installing dependencies.`, { silent: options.silent })?.start();
dependenciesSpinner?.start();
await addDependency(dependencies$1, {
cwd: config.resolvedPaths.cwd,
silent: true,
dev: options?.dev
});
dependenciesSpinner?.succeed();
}
//#endregion
//#region src/utils/transformers/transform-css-vars.ts
function transformCssVars(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(path$2) {
if (path$2.parent.value.type !== "ImportDeclaration" && typeof path$2.node.value === "string") {
path$2.node.value = applyColorMapping(path$2.node.value.replace(/"/g, ""), baseColor.inlineColors);
transformCount++;
}
return this.traverse(path$2);
} });
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$1 = split.pop();
const variant = split.join(":");
parts.push(variant ?? null, name$1 ?? null, alpha ?? null);
return parts;
}
const 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 = new Set();
const darkMode = new Set();
for (const className of classNames) {
const [variant, value, modifier] = splitClassName(className);
const prefix = PREFIXES.find((prefix$1) => value?.startsWith(prefix$1));
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();
}
//#endregion
//#region 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, isRemote } = opts;
const utilsImport = "@/lib/utils";
for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitImportDeclaration(path$2) {
if (typeof path$2.node.source.value === "string") {
const sourcePath = path$2.node.source.value;
const updatedImport = updateImportAliases(sourcePath, config, isRemote);
path$2.node.source.value = updatedImport;
if (updatedImport === utilsImport) {
const namedImports = path$2.node.specifiers?.map((node) => node.local?.name ?? "") ?? [];
const cnImport = namedImports.find((i) => i === "cn");
if (cnImport) path$2.node.source.value = updatedImport === utilsImport ? sourcePath.replace(utilsImport, config.aliases.utils) : config.aliases.utils;
}
}
return this.traverse(path$2);
} });
return transformCount;
}
};
}
function updateImportAliases(moduleSpecifier, config, isRemote = false) {
if (!moduleSpecifier.startsWith("@/") && !isRemote) return moduleSpecifier;
if (isRemote && moduleSpecifier.startsWith("@/")) moduleSpecifier = moduleSpecifier.replace(/^@\//, `@/registry/new-york/`);
if (!moduleSpecifier.startsWith("@/registry/")) {
const alias = config.aliases.components.split("/")[0];
return moduleSpecifier.replace(/^@\//, `${alias}/`);
}
if (moduleSpecifier.match(/^@\/registry\/(.+)\/ui/)) return moduleSpecifier.replace(/^@\/registry\/(.+)\/ui/, config.aliases.ui ?? `${config.aliases.components}/ui`);
if (config.aliases.components && moduleSpecifier.match(/^@\/registry\/(.+)\/components/)) return moduleSpecifier.replace(/^@\/registry\/(.+)\/components/, config.aliases.components);
if (config.aliases.lib && moduleSpecifier.match(/^@\/registry\/(.+)\/lib/)) return moduleSpecifier.replace(/^@\/registry\/(.+)\/lib/, config.aliases.lib);
if (config.aliases.composables && moduleSpecifier.match(/^@\/registry\/(.+)\/composables/)) return moduleSpecifier.replace(/^@\/registry\/(.+)\/composables/, config.aliases.composables);
return moduleSpecifier.replace(/^@\/registry\/[^/]+/, config.aliases.components);
}
//#endregion
//#region src/utils/transformers/transform-sfc.ts
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$1(content, filename, {
removeTsComments: true,
prettierOptions: { proseWrap: "never" }
});
}
//#endregion
//#region src/utils/transformers/transform-tw-prefix.ts
async function transformTwPrefix(opts) {
const tailwindVersion = await getProjectTailwindVersionFromConfig(opts.config);
return {
type: "codemod",
name: "add prefix to tailwind classes",
transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST, astHelpers } }) {
let transformCount = 0;
const { config } = opts;
if (!config.tailwind?.prefix) return transformCount;
function isVariantProperty(node) {
if (node.type === "Property") {
if (node.key?.type === "Identifier") {
const keyName = node.key.name;
return [
"variant",
"size",
"color",
"type",
"state"
].includes(keyName);
}
if (node.key?.type === "Literal" && typeof node.key.value === "string") {
const keyName = node.key.value;
return [
"variant",
"size",
"color",
"type",
"state"
].includes(keyName);
}
}
return false;
}
function traverseExpression(expression) {
if (expression.type === "CallExpression" && expression.callee?.type === "Identifier" && expression.callee.name === "cn") expression.arguments.forEach((arg) => {
if (arg.type === "Literal" && typeof arg.value === "string") {
arg.value = applyPrefix(arg.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
} else if (arg.type === "ConditionalExpression") {
if (arg.consequent?.type === "Literal" && typeof arg.consequent.value === "string") {
arg.consequent.value = applyPrefix(arg.consequent.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
if (arg.alternate?.type === "Literal" && typeof arg.alternate.value === "string") {
arg.alternate.value = applyPrefix(arg.alternate.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
} else if (arg.type === "BinaryExpression") {
if (arg.right?.type === "Literal" && typeof arg.right.value === "string") {
arg.right.value = applyPrefix(arg.right.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
} else if (arg.type === "ObjectExpression") arg.properties.forEach((prop) => {
if (prop.type === "Property" && prop.value?.type === "Literal" && typeof prop.value.value === "string") {
if (!isVariantProperty(prop)) {
prop.value.value = applyPrefix(prop.value.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
}
});
else {
const literals = astHelpers.findAll(arg, { type: "Literal" });
literals.forEach((literal) => {
if (typeof literal.value === "string") {
let shouldTransform = true;
let parent = literal.parent;
while (parent) {
if (isVariantProperty(parent)) {
shouldTransform = false;
break;
}
parent = parent.parent;
}
if (shouldTransform) {
literal.value = applyPrefix(literal.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
}
});
}
});
else if (expression.type === "ConditionalExpression") {
if (expression.consequent) traverseExpression(expression.consequent);
if (expression.alternate) traverseExpression(expression.alternate);
} else if (expression.type === "BinaryExpression") {
if (expression.left) traverseExpression(expression.left);
if (expression.right) traverseExpression(expression.right);
}
}
for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitCallExpression(path$2) {
if (path$2.node.callee.type === "Identifier" && path$2.node.callee.name === "cva") {
const args = path$2.node.arguments;
if (args[0]?.type === "Literal" && typeof args[0].value === "string") {
args[0].value = applyPrefix(args[0].value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
if (args[1]?.type === "ObjectExpression") {
const variantsProperty = args[1].properties.find((prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "variants");
if (variantsProperty && variantsProperty.type === "Property" && variantsProperty.value.type === "ObjectExpression") {
const allProperties = astHelpers.findAll(variantsProperty.value, { type: "Property" });
allProperties.forEach((prop) => {
if (prop.value?.type === "Literal" && typeof prop.value.value === "string") {
prop.value.value = applyPrefix(prop.value.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
});
}
}
}
if (path$2.node.callee.type === "Identifier" && path$2.node.callee.name === "cn") path$2.node.arguments.forEach((arg) => {
if (arg.type === "Literal" && typeof arg.value === "string") {
arg.value = applyPrefix(arg.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
} else if (arg.type === "ConditionalExpression") {
if (arg.consequent?.type === "Literal" && typeof arg.consequent.value === "string") {
arg.consequent.value = applyPrefix(arg.consequent.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
if (arg.alternate?.type === "Literal" && typeof arg.alternate.value === "string") {
arg.alternate.value = applyPrefix(arg.alternate.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
} else if (arg.type === "BinaryExpression") {
if (arg.right?.type === "Literal" && typeof arg.right.value === "string") {
arg.right.value = applyPrefix(arg.right.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
} else if (arg.type === "ObjectExpression") arg.properties.forEach((prop) => {
if (prop.type === "Property" && prop.value?.type === "Literal" && typeof prop.value.value === "string") {
if (!isVariantProperty(prop)) {
prop.value.value = applyPrefix(prop.value.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
}
});
else {
const literals = astHelpers.findAll(arg, { type: "Literal" });
literals.forEach((literal) => {
if (typeof literal.value === "string") {
let shouldTransform = true;
let parent = literal.parent;
while (parent) {
if (isVariantProperty(parent)) {
shouldTransform = false;
break;
}
parent = parent.parent;
}
if (shouldTransform) {
literal.value = applyPrefix(literal.value, config.tailwind.prefix, tailwindVersion);
transformCount++;
}
}
});
}
});
return this.traverse(path$2);
} });
if (sfcAST) traverseTemplateAST(sfcAST, {
enterNode(node) {
if (node.type === "VAttribute" && node.key.type === "VDirectiveKey") {
if (node.key.argument?.type === "VIdentifier") {
const argName = node.key.argument.name;
if ([
"class",
"className",
"classes",
"classNames"
].includes(argName)) {
if (node.value?.type === "VExpressionContainer" && node.value.expression) traverseExpression(node.value.expression);
}
}
} else if (node.type === "VLiteral" && typeof node.value === "string") {
if (node.parent?.type === "VAttribute" && node.parent.key?.type === "VIdentifier" && [
"class",
"className",
"classes",
"classNames"
].includes(node.parent.key.name)) {
const cleanValue = node.value.replace(/"/g, "");
const prefixedValue = applyPrefix(cleanValue, config.tailwind.prefix, tailwindVersion);
node.value = `"${prefixedValue}"`;
transformCount++;
}
}
},
leaveNode() {}
});
return transformCount;
}
};
}
function applyPrefix(input, prefix = "", tailwindVersion) {
if (tailwindVersion === "v3") return input.split(" ").map((className) => {
const [variant, value, modifier] = splitClassName(className);
if (variant) return modifier ? `${variant}:${prefix}${value}/${modifier}` : `${variant}:${prefix}${value}`;
else return modifier ? `${prefix}${value}/${modifier}` : `${prefix}${value}`;
}).join(" ");
return input.split(" ").map((className) => className.indexOf(`${prefix}:`) === 0 ? className : `${prefix}:${className.trim()}`).join(" ");
}
//#endregion
//#region src/utils/icon-libraries.ts
const 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"
}
};
//#endregion
//#region src/utils/transformers/transform-icons.ts
const 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 = new Map();
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, { visitImportDeclaration(path$2) {
if (![ICON_LIBRARIES.radix.import, ICON_LIBRARIES.lucide.import].includes(`${path$2.node.source.value}`)) return this.traverse(path$2);
for (const specifier of path$2.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) path$2.node.source.value = ICON_LIBRARIES[targetLibrary].import;
return this.traverse(path$2);
} });
if (sfcAST) traverseTemplateAST(sfcAST, { enterNode(node) {
if (node.type === "VElement" && targetedIconsMap.has(node.rawName)) {
node.rawName = targetedIconsMap.get(node.rawName) ?? "";
transformCount++;
}
} });
}
return transformCount;
}
};
}
//#endregion
//#region src/utils/transformers/index.ts
async function transform$2(opts) {
const source = await transformSFC(opts);
const registryIcons = await getRegistryIcons();
return transform(source, opts.filename, [
transformImport(opts),
transformCssVars(opts),
await transformTwPrefix(opts),
transformIcons(opts, registryIcons)
]).code;
}
//#endregion
//#region src/utils/updaters/update-files.ts
async function updateFiles(files$1, config, options) {
if (!files$1?.length) return {
filesCreated: [],
filesUpdated: [],
filesSkipped: []
};
options = {
overwrite: false,
force: false,
silent: false,
isRemote: 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)]);
let filesCreated = [];
let filesUpdated = [];
let filesSkipped = [];
const folderSkipped = new Map();
let tempRoot = "";
if (!config.typescript) {
for (const file of files$1) {
if (!file.content) continue;
const dirName = path.dirname(file.path);
tempRoot = path.join(tmpdir(), "shadcn-vue");
const tempDir = path.join(tempRoot, "registry", config.style, dirName);
const tempPath = path.join(tempRoot, "registry", config.style, file.path);
await promises.mkdir(tempDir, { recursive: true });
await promises.writeFile(tempPath, file.content, "utf-8");
}
await promises.cp(path.join(process.cwd(), "node_modules"), tempRoot, {
recursive: true,
filter: (src) => !src.includes("/.bin/")
});
await promises.writeFile(path.join(tempRoot, "tsconfig.json"), `{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
},
"include": ["**/*.vue", "**/*.ts"],
"exclude": ["node_modules"]
}`, "utf8");
}
for (const file of files$1) {
if (!file.content) continue;
let filePath = resolveFilePath(file, config, {
framework: projectInfo?.framework.name,
commonRoot: findCommonRoot$1(files$1.map((f) => f.path), file.path)
});
if (!filePath) continue;
const fileName = basename(file.path);
const targetDir = path.dirname(filePath);
if (!config.typescript) filePath = filePath.replace(/\.ts?$/, (match) => ".js");
const existingFile = existsSync(filePath);
const content = await transform$2({
filename: path.join(tempRoot, "registry", config.style, file.path),
raw: file.content,
config,
baseColor,
isRemote: options.isRemote
});
if (existingFile) {
const existingFileContent = await promises.readFile(filePath, "utf-8");
const [normalizedExisting, normalizedNew] = await Promise.all([getNormalizedFileContent(existingFileContent), getNormalizedFileContent(content)]);
if (normalizedExisting === normalizedNew) {
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath));
continue;
}
}
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();
}