UNPKG

shadcn-vue

Version:
1,307 lines (1,293 loc) 99.5 kB
#!/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(); }