shadcn-vue
Version:
Add components to your apps.
1,493 lines (1,477 loc) • 95.1 kB
JavaScript
#!/usr/bin/env node
import {
BASE_COLORS,
DEFAULT_COMPONENTS,
DEFAULT_TAILWIND_CONFIG,
DEFAULT_TAILWIND_CSS,
DEFAULT_UTILS,
_createSourceFile,
_getQuoteChar,
fetchRegistry,
fetchTree,
getConfig,
getItemTargetPath,
getPackageInfo,
getProjectConfig,
getProjectInfo,
getProjectTailwindVersionFromConfig,
getRegistryBaseColor,
getRegistryBaseColors,
getRegistryIcons,
getRegistryIndex,
getRegistryItem,
getRegistryStyles,
handleError,
highlighter,
isUrl,
logger,
rawConfigSchema,
registryItemSchema,
registryResolveItemsTree,
registrySchema,
resolveConfigPaths,
resolveRegistryItems,
spinner,
updateTailwindConfig
} from "./chunk-MOIE35VS.js";
// src/commands/init.ts
import { promises as fs6 } 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/preflights/preflight-init.ts
import fs from "fs-extra";
import path from "pathe";
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
)}.
To start over, remove the ${highlighter.info(
"components.json"
)} file and run ${highlighter.info("init")} again.`
);
logger.break();
process.exit(1);
}
projectSpinner?.succeed();
const frameworkSpinner = spinner(`Verifying framework.`, {
silent: options.silent
}).start();
const projectInfo = await getProjectInfo(options.cwd);
if (!projectInfo || projectInfo?.framework.name === "manual") {
errors[UNSUPPORTED_FRAMEWORK] = true;
frameworkSpinner?.fail();
logger.break();
if (projectInfo?.framework.links.installation) {
logger.error(
`We could not detect a supported framework at ${highlighter.info(
options.cwd
)}.
Visit ${highlighter.info(
projectInfo?.framework.links.installation
)} to manually configure your project.
Once configured, you can use the cli to add components.`
);
}
logger.break();
process.exit(1);
}
frameworkSpinner?.succeed(
`Verifying framework. Found ${highlighter.info(
projectInfo.framework.label
)}.`
);
let tailwindSpinnerMessage = "Validating Tailwind CSS.";
if (projectInfo.tailwindVersion === "v4") {
tailwindSpinnerMessage = `Validating Tailwind CSS config. Found ${highlighter.info(
"v4"
)}.`;
}
const tailwindSpinner = spinner(tailwindSpinnerMessage, {
silent: options.silent
}).start();
if (projectInfo.tailwindVersion === "v3" && (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile)) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else if (projectInfo.tailwindVersion === "v4" && !projectInfo?.tailwindCssFile) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else if (!projectInfo.tailwindVersion) {
errors[TAILWIND_NOT_CONFIGURED] = true;
tailwindSpinner?.fail();
} else {
tailwindSpinner?.succeed();
}
const tsConfigSpinner = spinner(`Validating import alias.`, {
silent: options.silent
}).start();
if (!projectInfo?.aliasPrefix) {
errors[IMPORT_ALIAS_MISSING] = true;
tsConfigSpinner?.fail();
} else {
tsConfigSpinner?.succeed();
}
if (Object.keys(errors).length > 0) {
if (errors[TAILWIND_NOT_CONFIGURED]) {
logger.break();
logger.error(
`No Tailwind CSS configuration found at ${highlighter.info(
options.cwd
)}.`
);
logger.error(
`It is likely you do not have Tailwind CSS installed or have an invalid configuration.`
);
logger.error(`Install Tailwind CSS then try again.`);
if (projectInfo?.framework.links.tailwind) {
logger.error(
`Visit ${highlighter.info(
projectInfo?.framework.links.tailwind
)} to get started.`
);
}
}
if (errors[IMPORT_ALIAS_MISSING]) {
logger.break();
logger.error(`No import alias found in your tsconfig.json file.`);
if (projectInfo?.framework.links.installation) {
logger.error(
`Visit ${highlighter.info(
projectInfo?.framework.links.installation
)} to learn how to set an import alias.`
);
}
}
logger.break();
process.exit(1);
}
return {
errors,
projectInfo
};
}
// src/utils/updaters/update-css.ts
import { promises as fs2 } from "node:fs";
import path2 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 = path2.relative(
config.resolvedPaths.cwd,
cssFilepath
);
const cssSpinner = spinner(
`Updating ${highlighter.info(cssFilepathRelative)}`,
{
silent: options.silent
}
).start();
const raw = await fs2.readFile(cssFilepath, "utf8");
const output = await transformCss(raw, css);
await fs2.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 fs3 } from "node:fs";
import path3 from "node:path";
import postcss2 from "postcss";
import AtRule from "postcss/lib/at-rule";
import { z } 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 = path3.relative(
config.resolvedPaths.cwd,
cssFilepath
);
const cssVarsSpinner = spinner(
`Updating CSS variables in ${highlighter.info(cssFilepathRelative)}`,
{
silent: options.silent
}
).start();
const raw = await fs3.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 fs3.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 = 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 = 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 = 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 = 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 fs4 } from "node:fs";
import { tmpdir } 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(path15) {
if (path15.parent.value.type !== "ImportDeclaration" && typeof path15.node.value === "string") {
path15.node.value = applyColorMapping(path15.node.value.replace(/"/g, ""), baseColor.inlineColors);
transformCount++;
}
return this.traverse(path15);
}
});
}
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, isRemote } = opts;
const utilsImport = "@/lib/utils";
for (const scriptAST of scriptASTs) {
traverseScriptAST(scriptAST, {
visitImportDeclaration(path15) {
if (typeof path15.node.source.value === "string") {
const sourcePath = path15.node.source.value;
const updatedImport = updateImportAliases(sourcePath, config, isRemote);
path15.node.source.value = updatedImport;
if (updatedImport === utilsImport) {
const namedImports = path15.node.specifiers?.map((node) => node.local?.name ?? "") ?? [];
const cnImport = namedImports.find((i) => i === "cn");
if (cnImport) {
path15.node.source.value = updatedImport === utilsImport ? sourcePath.replace(utilsImport, config.aliases.utils) : config.aliases.utils;
}
}
}
return this.traverse(path15);
}
});
}
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
);
}
// 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(path15) {
if (path15.node.callee.type === "Identifier" && path15.node.callee.name === "cva") {
const nodes = path15.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(path15);
}
});
}
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(path15) {
if (![ICON_LIBRARIES.radix.import, ICON_LIBRARIES.lucide.import].includes(`${path15.node.source.value}`))
return this.traverse(path15);
for (const specifier of path15.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)
path15.node.source.value = ICON_LIBRARIES[targetLibrary].import;
return this.traverse(path15);
}
});
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 path4, { basename, dirname } from "pathe";
import prompts from "prompts";
async function updateFiles(files, config, options) {
if (!files?.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)
]);
const filesCreated = [];
const filesUpdated = [];
const filesSkipped = [];
const folderSkipped = /* @__PURE__ */ new Map();
let tempRoot = "";
if (!config.typescript) {
for (const file of files) {
if (!file.content) {
continue;
}
const dirName = path4.dirname(file.path);
tempRoot = path4.join(tmpdir(), "shadcn-vue");
const tempDir = path4.join(tempRoot, "registry", config.style, dirName);
const tempPath = path4.join(tempRoot, "registry", config.style, file.path);
await fs4.mkdir(tempDir, { recursive: true });
await fs4.writeFile(tempPath, file.content, "utf-8");
}
await fs4.cp(path4.join(process.cwd(), "node_modules"), tempRoot, { recursive: true });
await fs4.writeFile(path4.join(tempRoot, "tsconfig.json"), `{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
},
"include": ["**/*.vue", "**/*.ts"],
"exclude": ["node_modules"]
}`, "utf8");
}
for (const file of files) {
if (!file.content) {
continue;
}
let filePath = resolveFilePath(file, config, {
framework: projectInfo?.framework.name,
commonRoot: findCommonRoot(
files.map((f) => f.path),
file.path
)
});
if (!filePath) {
continue;
}
const fileName = basename(file.path);
const targetDir = path4.dirname(filePath);
if (!config.typescript) {
filePath = filePath.replace(/\.ts?$/, (match) => ".js");
}
const existingFile = existsSync(filePath);
const content = await transform2({
filename: path4.join(tempRoot, "registry", config.style, file.path),
raw: file.content,
config,
baseColor,
isRemote: options.isRemote
});
if (existingFile) {
const existingFileContent = await fs4.readFile(filePath, "utf-8");
const [normalizedExisting, normalizedNew] = await Promise.all([
getNormalizedFileContent(existingFileContent),
getNormalizedFileContent(content)
]);
if (normalizedExist