UNPKG

shadcn-vue

Version:
1,493 lines (1,477 loc) 95.1 kB
#!/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