UNPKG

shadcn-vue

Version:
1,273 lines (1,260 loc) 90 kB
#!/usr/bin/env node import { BASE_COLORS, BUILTIN_REGISTRIES, DEFAULT_COMPONENTS, DEFAULT_TAILWIND_CONFIG, DEFAULT_TAILWIND_CSS, DEFAULT_UTILS, DEPRECATED_COMPONENTS, ICON_LIBRARIES, RegistryNotConfiguredError, _createSourceFile, _getQuoteChar, buildUrlAndHeadersForRegistryItem, clearRegistryContext, configWithDefaults, createConfig, fetchRegistryItems, fetchTree, findCommonRoot, findExistingEnvFile, findPackageRoot, getConfig, getItemTargetPath, getNewEnvKeys, getPackageInfo, getProjectConfig, getProjectInfo, getProjectTailwindVersionFromConfig, getRegistriesConfig, getRegistriesIndex, getRegistry, getRegistryBaseColor, getRegistryBaseColors, getRegistryIcons, getRegistryItems, getRegistryStyles, getShadcnRegistryIndex, getWorkspaceConfig, handleError, highlighter, isUniversalRegistryItem, logger, mergeEnvContent, parseRegistryAndItemFromString, resolveConfigPaths, resolveRegistryItems, resolveRegistryTree, resolveTree, spinner, transform, updateFiles, updateTailwindConfig } from "./registry-CVURNCtV.js"; import { rawConfigSchema, registryItemSchema, registrySchema } from "./schema-PrLX5K_R.js"; import { server } from "./mcp-ebpb1d4Z.js"; import { Command } from "commander"; import path, { isAbsolute, join, normalize, resolve, sep } from "pathe"; import prompts from "prompts"; import z$1, { z } from "zod"; import { existsSync, promises } from "fs"; import deepmerge from "deepmerge"; import fsExtra from "fs-extra"; import { glob } from "tinyglobby"; import consola from "consola"; import fs from "fs/promises"; import { tmpdir } from "os"; import { Project, ScriptKind, SyntaxKind } from "ts-morph"; import { randomBytes } from "crypto"; import postcss from "postcss"; import AtRule from "postcss/lib/at-rule"; import { addDependency, detectPackageManager } from "nypm"; import { diffLines } from "diff"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { x } from "tinyexec"; //#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 (!fsExtra.existsSync(options.cwd) || !fsExtra.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 (fsExtra.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/is-safe-target.ts function isSafeTarget(targetPath, cwd) { if (targetPath.includes("\0")) return false; let decodedPath; try { decodedPath = targetPath; let prevPath = ""; while (decodedPath !== prevPath && decodedPath.includes("%")) { prevPath = decodedPath; decodedPath = decodeURIComponent(decodedPath); } } catch { return false; } const normalizedTarget = normalize(decodedPath); const normalizedRoot = normalize(cwd); const hasPathTraversal = (path$1) => { return path$1.replace(/\[\.\.\..*?\]/g, "").includes(".."); }; if (hasPathTraversal(normalizedTarget) || hasPathTraversal(decodedPath) || hasPathTraversal(targetPath)) return false; const cleanPath = (path$1) => path$1.replace(/\[\.\.\..*?\]/g, ""); const cleanTarget = cleanPath(targetPath); const cleanDecoded = cleanPath(decodedPath); if ([ /\.\.[/\\]/, /[/\\]\.\./, /\.\./, /\.\.%/, /\0/, /[\x01-\x1F]/ ].some((pattern) => pattern.test(cleanTarget) || pattern.test(cleanDecoded))) return false; if ((targetPath.includes("~") || decodedPath.includes("~")) && (targetPath.includes("../") || decodedPath.includes("../"))) return false; if (/^[a-z]:[/\\]/i.test(decodedPath)) { if (process.platform === "win32") return decodedPath.toLowerCase().startsWith(cwd.toLowerCase()); return false; } const absoluteTarget = isAbsolute(normalizedTarget) ? normalizedTarget : resolve(normalizedRoot, normalizedTarget); const safeRoot = normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep; return absoluteTarget === normalizedRoot || absoluteTarget.startsWith(safeRoot); } //#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(); let output = await transformCss(await promises.readFile(cssFilepath, "utf8"), css); await promises.writeFile(cssFilepath, output, "utf8"); cssSpinner.succeed(); } async function transformCss(input, css) { const result = await postcss([updateCssPlugin(css)]).process(input, { from: void 0 }); let output = result.css; const root = result.root; if (root.nodes && root.nodes.length > 0) { const lastNode = root.nodes[root.nodes.length - 1]; if (lastNode.type === "atrule" && !lastNode.nodes && !output.trimEnd().endsWith(";")) output = `${output.trimEnd()};`; } 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 === "import") { if (!root.nodes?.find((node) => node.type === "atrule" && node.name === "import" && node.params === params)) { const importRule = postcss.atRule({ name: "import", params, raws: { semicolon: true } }); const importNodes = root.nodes?.filter((node) => node.type === "atrule" && node.name === "import"); if (importNodes && importNodes.length > 0) { const lastImport = importNodes[importNodes.length - 1]; importRule.raws.before = "\n"; root.insertAfter(lastImport, importRule); } else { if (!root.nodes || root.nodes.length === 0) importRule.raws.before = ""; else importRule.raws.before = ""; root.prepend(importRule); } } } else if (name$1 === "plugin") { let quotedParams = params; if (params && !params.startsWith("\"") && !params.startsWith("'")) quotedParams = `"${params}"`; const normalizeParams = (p) => { if (p.startsWith("\"") && p.endsWith("\"")) return p.slice(1, -1); if (p.startsWith("'") && p.endsWith("'")) return p.slice(1, -1); return p; }; if (!root.nodes?.find((node) => { if (node.type !== "atrule" || node.name !== "plugin") return false; return normalizeParams(node.params) === normalizeParams(params); })) { const pluginRule = postcss.atRule({ name: "plugin", params: quotedParams, raws: { semicolon: true, before: "\n" } }); const importNodes = root.nodes?.filter((node) => node.type === "atrule" && node.name === "import"); const pluginNodes = root.nodes?.filter((node) => node.type === "atrule" && node.name === "plugin"); if (pluginNodes && pluginNodes.length > 0) { const lastPlugin = pluginNodes[pluginNodes.length - 1]; root.insertAfter(lastPlugin, pluginRule); } else if (importNodes && importNodes.length > 0) { const lastImport = importNodes[importNodes.length - 1]; root.insertAfter(lastImport, pluginRule); root.insertBefore(pluginRule, postcss.comment({ text: "---break---" })); root.insertAfter(pluginRule, postcss.comment({ text: "---break---" })); } else { root.prepend(pluginRule); root.insertBefore(pluginRule, postcss.comment({ text: "---break---" })); root.insertAfter(pluginRule, postcss.comment({ text: "---break---" })); } } } else if (typeof properties === "object" && Object.keys(properties).length === 0) { if (!root.nodes?.find((node) => node.type === "atrule" && node.name === name$1 && node.params === params)) { const newAtRule = postcss.atRule({ name: name$1, params, raws: { semicolon: true } }); root.append(newAtRule); root.insertBefore(newAtRule, postcss.comment({ text: "---break---" })); } } else 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 if (name$1 === "property") processRule(root, selector, properties); 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 tempRule = postcss.parse(`.temp{${properties}}`).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 (prop.startsWith("@") && typeof value === "object" && value !== null && Object.keys(value).length === 0) { const atRuleMatch = prop.match(/@([a-z-]+)\s*(.*)/i); if (atRuleMatch) { const [, atRuleName, atRuleParams] = atRuleMatch; const atRule = postcss.atRule({ name: atRuleName, params: atRuleParams, raws: { semicolon: true, before: "\n " } }); rule.append(atRule); } } else 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") processRule(parent, prop.startsWith("&") ? selector.replace(/^([^:]+)/, `$1${prop.substring(1)}`) : prop, value); } else if (typeof properties === "string") try { const tempRule = postcss.parse(`.temp{${properties}}`).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 output = await transformCssVars(await promises.readFile(cssFilepath, "utf8"), 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(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 })); let output = (await postcss(plugins).process(input, { from: void 0 })).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 }) => { if (!baseLayer?.nodes?.find((node) => node.type === "rule" && node.selector === selector)) 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") { for (const [key, value$1] of Object.entries({ sm: "calc(var(--radius) - 4px)", md: "calc(var(--radius) - 2px)", lg: "var(--radius)", xl: "calc(var(--radius) + 4px)" })) { 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 } }); if (!themeNode?.nodes?.find((node) => node.type === "decl" && node.prop === cssVarNode.prop)) 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) { if (!root.nodes.find((node) => node.type === "atrule" && node.name === "custom-variant")) { 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"); if (!importNodes.some((node) => node.params.replace(/["']/g, "") === params)) { 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 quote = getQuoteType(root) === "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) { if (root.nodes[0].toString().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") || value.startsWith("var(--color-"); } //#endregion //#region src/utils/updaters/update-dependencies.ts async function updateDependencies(dependencies$1, devDependencies$1, config, options) { dependencies$1 = Array.from(new Set(dependencies$1)); devDependencies$1 = Array.from(new Set(devDependencies$1)); if (!dependencies$1?.length && !devDependencies$1?.length) return; options = { silent: false, ...options }; const dependenciesSpinner = spinner(`Installing dependencies.`, { silent: options.silent })?.start(); dependenciesSpinner?.start(); if (dependencies$1?.length) await addDependency(dependencies$1, { cwd: config.resolvedPaths.cwd, silent: true, dev: false }); if (devDependencies$1?.length) await addDependency(devDependencies$1, { cwd: config.resolvedPaths.cwd, silent: true, dev: true }); dependenciesSpinner?.succeed(); } //#endregion //#region src/utils/updaters/update-env-vars.ts async function updateEnvVars(envVars, config, options) { if (!envVars || Object.keys(envVars).length === 0) return { envVarsAdded: [], envFileUpdated: null, envFileCreated: null }; options = { silent: false, ...options }; const envSpinner = spinner(`Adding environment variables.`, { silent: options.silent })?.start(); const projectRoot = config.resolvedPaths.cwd; let envFilePath = path.join(projectRoot, ".env.local"); const existingEnvFile = findExistingEnvFile(projectRoot); if (existingEnvFile) envFilePath = existingEnvFile; const envFileExists = existsSync(envFilePath); const envFileName = path.basename(envFilePath); const newEnvContent = Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n"); let envVarsAdded = []; let envFileUpdated = null; let envFileCreated = null; if (envFileExists) { const existingContent = await promises.readFile(envFilePath, "utf-8"); const mergedContent = mergeEnvContent(existingContent, newEnvContent); envVarsAdded = getNewEnvKeys(existingContent, newEnvContent); if (envVarsAdded.length > 0) { await promises.writeFile(envFilePath, mergedContent, "utf-8"); envFileUpdated = path.relative(projectRoot, envFilePath); envSpinner?.succeed(`Added the following variables to ${highlighter.info(envFileName)}:`); if (!options.silent) for (const key of envVarsAdded) logger.log(` ${highlighter.success("+")} ${key}`); } else envSpinner?.stop(); } else { await promises.writeFile(envFilePath, `${newEnvContent}\n`, "utf-8"); envFileCreated = path.relative(projectRoot, envFilePath); envVarsAdded = Object.keys(envVars); envSpinner?.succeed(`Added the following variables to ${highlighter.info(envFileName)}:`); if (!options.silent) for (const key of envVarsAdded) logger.log(` ${highlighter.success("+")} ${key}`); } if (!options.silent && envVarsAdded.length > 0) logger.break(); return { envVarsAdded, envFileUpdated, envFileCreated }; } //#endregion //#region src/utils/add-components.ts async function addComponents(components, config, options) { options = { overwrite: false, silent: false, isNewProject: false, baseStyle: true, ...options }; const workspaceConfig = await getWorkspaceConfig(config); if (workspaceConfig && workspaceConfig.ui && workspaceConfig.ui.resolvedPaths.cwd !== config.resolvedPaths.cwd) return await addWorkspaceComponents(components, config, workspaceConfig, { ...options, isRemote: components?.length === 1 && !!components[0].match(/\/chat\/b\//) }); return await addProjectComponents(components, config, options); } async function addProjectComponents(components, config, options) { if (!options.baseStyle && !components.length) return; const registrySpinner = spinner(`Checking registry.`, { silent: options.silent })?.start(); const tree = await resolveRegistryTree(components, configWithDefaults(config)); if (!tree) { registrySpinner?.fail(); return handleError(/* @__PURE__ */ new Error("Failed to fetch components from registry.")); } try { validateFilesTarget(tree.files ?? [], config.resolvedPaths.cwd); } catch (error) { registrySpinner?.fail(); return handleError(error); } registrySpinner?.succeed(); const tailwindVersion = await getProjectTailwindVersionFromConfig(config); await updateTailwindConfig(tree.tailwind?.config, config, { silent: options.silent, tailwindVersion }); const overwriteCssVars = await shouldOverwriteCssVars(components, config); await updateCssVars(tree.cssVars, config, { cleanupDefaultNextStyles: options.isNewProject, silent: options.silent, tailwindVersion, tailwindConfig: tree.tailwind?.config, overwriteCssVars, initIndex: options.baseStyle }); await updateCss(tree.css, config, { silent: options.silent }); await updateEnvVars(tree.envVars, config, { silent: options.silent }); await updateDependencies(tree.dependencies, tree.devDependencies, config, { silent: options.silent }); await updateFiles(tree.files, config, { overwrite: options.overwrite, silent: options.silent, path: options.path }); if (tree.docs) logger.info(tree.docs); } async function addWorkspaceComponents(components, config, workspaceConfig, options) { if (!options.baseStyle && !components.length) return; const registrySpinner = spinner(`Checking registry.`, { silent: options.silent })?.start(); const tree = await resolveRegistryTree(components, configWithDefaults(config)); if (!tree) { registrySpinner?.fail(); return handleError(/* @__PURE__ */ new Error("Failed to fetch components from registry.")); } try { validateFilesTarget(tree.files ?? [], config.resolvedPaths.cwd); } catch (error) { registrySpinner?.fail(); return handleError(error); } registrySpinner?.succeed(); const filesCreated = []; const filesUpdated = []; const filesSkipped = []; const rootSpinner = spinner(`Installing components.`)?.start(); const mainTargetConfig = workspaceConfig.ui; const tailwindVersion = await getProjectTailwindVersionFromConfig(mainTargetConfig); const workspaceRoot = findCommonRoot(config.resolvedPaths.cwd, mainTargetConfig.resolvedPaths.ui); if (tree.tailwind?.config) { await updateTailwindConfig(tree.tailwind?.config, mainTargetConfig, { silent: true, tailwindVersion }); filesUpdated.push(path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindConfig)); } if (tree.cssVars) { const overwriteCssVars = await shouldOverwriteCssVars(components, config); await updateCssVars(tree.cssVars, mainTargetConfig, { silent: true, tailwindVersion, tailwindConfig: tree.tailwind?.config, overwriteCssVars }); filesUpdated.push(path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)); } if (tree.css) { await updateCss(tree.css, mainTargetConfig, { silent: true }); filesUpdated.push(path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)); } if (tree.envVars) await updateEnvVars(tree.envVars, mainTargetConfig, { silent: true }); await updateDependencies(tree.dependencies, tree.devDependencies, mainTargetConfig, { silent: true }); const filesByType = /* @__PURE__ */ new Map(); for (const file of tree.files ?? []) { const type$1 = file.type || "registry:ui"; if (!filesByType.has(type$1)) filesByType.set(type$1, []); filesByType.get(type$1).push(file); } for (const type$1 of Array.from(filesByType.keys())) { const typeFiles = filesByType.get(type$1); let targetConfig = type$1 === "registry:ui" ? workspaceConfig.ui : config; const typeWorkspaceRoot = findCommonRoot(config.resolvedPaths.cwd, targetConfig.resolvedPaths.ui || targetConfig.resolvedPaths.cwd); const packageRoot = await findPackageRoot(typeWorkspaceRoot, targetConfig.resolvedPaths.cwd) ?? targetConfig.resolvedPaths.cwd; const files$1 = await updateFiles(typeFiles, targetConfig, { overwrite: options.overwrite, silent: true, rootSpinner, isRemote: options.isRemote, isWorkspace: true, path: options.path }); filesCreated.push(...files$1.filesCreated.map((file) => path.relative(typeWorkspaceRoot, path.join(packageRoot, file)))); filesUpdated.push(...files$1.filesUpdated.map((file) => path.relative(typeWorkspaceRoot, path.join(packageRoot, file)))); filesSkipped.push(...files$1.filesSkipped.map((file) => path.relative(typeWorkspaceRoot, path.join(packageRoot, file)))); } rootSpinner?.succeed(); filesCreated.sort(); filesUpdated.sort(); filesSkipped.sort(); if (!(filesCreated.length || filesUpdated.length) && !filesSkipped.length) spinner(`No files updated.`, { silent: options.silent })?.info(); if (filesCreated.length) { spinner(`Created ${filesCreated.length} ${filesCreated.length === 1 ? "file" : "files"}:`, { silent: options.silent })?.succeed(); for (const file of filesCreated) logger.log(` - ${file}`); } if (filesUpdated.length) { spinner(`Updated ${filesUpdated.length} ${filesUpdated.length === 1 ? "file" : "files"}:`, { silent: options.silent })?.info(); for (const file of filesUpdated) logger.log(` - ${file}`); } if (filesSkipped.length) { spinner(`Skipped ${filesSkipped.length} ${filesUpdated.length === 1 ? "file" : "files"}: (use --overwrite to overwrite)`, { silent: options.silent })?.info(); for (const file of filesSkipped) logger.log(` - ${file}`); } if (tree.docs) logger.info(tree.docs); } async function shouldOverwriteCssVars(components, config) { const result = await getRegistryItems(components, { config }); return z.array(registryItemSchema).parse(result).some((component) => component.type === "registry:theme" || component.type === "registry:style"); } function validateFilesTarget(files$1, cwd) { for (const file of files$1) { if (!file?.target) continue; if (!isSafeTarget(file.target, cwd)) throw new Error(`We found an unsafe file path "${file.target} in the registry item. Installation aborted.`); } } //#endregion //#region src/utils/env-loader.ts async function loadEnvFiles(cwd = process.cwd()) { try { const { config } = await import("@dotenvx/dotenvx"); for (const envFile of [ ".env.local", ".env.development.local", ".env.development", ".env" ]) { const envPath = join(cwd, envFile); if (existsSync(envPath)) config({ path: envPath, overload: false, quiet: true }); } } catch (error) { logger.warn("Failed to load env files:", error); } } //#endregion //#region src/utils/file-helper.ts const FILE_BACKUP_SUFFIX = ".bak"; function createFileBackup(filePath) { if (!fsExtra.existsSync(filePath)) return null; const backupPath = `${filePath}${FILE_BACKUP_SUFFIX}`; try { fsExtra.renameSync(filePath, backupPath); return backupPath; } catch (error) { console.error(`Failed to create backup of ${filePath}: ${error}`); return null; } } function restoreFileBackup(filePath) { const backupPath = `${filePath}${FILE_BACKUP_SUFFIX}`; if (!fsExtra.existsSync(backupPath)) return false; try { fsExtra.renameSync(backupPath, filePath); return true; } catch (error) { console.error(`Warning: Could not restore backup file ${backupPath}: ${error}`); return false; } } function deleteFileBackup(filePath) { const backupPath = `${filePath}${FILE_BACKUP_SUFFIX}`; if (!fsExtra.existsSync(backupPath)) return false; try { fsExtra.unlinkSync(backupPath); return true; } catch (error) { return false; } } //#endregion //#region src/registry/namespaces.ts async function resolveRegistryNamespaces(components, config) { const discoveredNamespaces = /* @__PURE__ */ new Set(); const visitedItems = /* @__PURE__ */ new Set(); const itemsToProcess = [...components]; while (itemsToProcess.length > 0) { const currentItem = itemsToProcess.shift(); if (visitedItems.has(currentItem)) continue; visitedItems.add(currentItem); const { registry } = parseRegistryAndItemFromString(currentItem); if (registry && !BUILTIN_REGISTRIES[registry]) discoveredNamespaces.add(registry); try { const [item] = await fetchRegistryItems([currentItem], config, { useCache: true }); if (item?.registryDependencies) for (const dep of item.registryDependencies) { const { registry: depRegistry } = parseRegistryAndItemFromString(dep); if (depRegistry && !BUILTIN_REGISTRIES[depRegistry]) discoveredNamespaces.add(depRegistry); if (!visitedItems.has(dep)) itemsToProcess.push(dep); } } catch (error) { if (error instanceof RegistryNotConfiguredError) { const { registry: registry$1 } = parseRegistryAndItemFromString(currentItem); if (registry$1 && !BUILTIN_REGISTRIES[registry$1]) discoveredNamespaces.add(registry$1); continue; } continue; } } return Array.from(discoveredNamespaces); } //#endregion //#region src/utils/registries.ts async function ensureRegistriesInConfig(components, config, options = {}) { options = { silent: false, writeFile: true, ...options }; const missingRegistries = (await resolveRegistryNamespaces(components, config)).filter((registry) => !config.registries?.[registry] && !Object.keys(BUILTIN_REGISTRIES).includes(registry)); if (missingRegistries.length === 0) return { config, newRegistries: [] }; const registryIndex = await getRegistriesIndex({ useCache: process.env.NODE_ENV !== "development" }); if (!registryIndex) return { config, newRegistries: [] }; const foundRegistries = {}; for (const registry of missingRegistries) if (registryIndex[registry]) foundRegistries[registry] = registryIndex[registry]; if (Object.keys(foundRegistries).length === 0) return { config, newRegistries: [] }; const existingRegistries = Object.fromEntries(Object.entries(config.registries || {}).filter(([key]) => !Object.keys(BUILTIN_REGISTRIES).includes(key))); const newConfigWithRegistries = { ...config, registries: { ...existingRegistries, ...foundRegistries } }; if (options.writeFile) { const { resolvedPaths,...configWithoutResolvedPaths } = newConfigWithRegistries; const configSpinner = spinner("Updating components.json.", { silent: options.silent }).start(); const updatedConfig = rawConfigSchema.parse(configWithoutResolvedPaths); await fsExtra.writeFile(path.resolve(config.resolvedPaths.cwd, "components.json"), `${JSON.stringify(updatedConfig, null, 2)}\n`, "utf-8"); configSpinner.succeed(); } return { config: newConfigWithRegistries, newRegistries: Object.keys(foundRegistries) }; } //#endregion //#region src/utils/updaters/update-tailwind-content.ts async function updateTailwindContent(content, config, options) { if (!content) return; options = { silent: false, ...options }; const tailwindFileRelativePath = path.relative(config.resolvedPaths.cwd, config.resolvedPaths.tailwindConfig); const tailwindSpinner = spinner(`Updating ${highlighter.info(tailwindFileRelativePath)}`, { silent: options.silent }).start(); const output = await transformTailwindContent(await promises.readFile(config.resolvedPaths.tailwindConfig, "utf8"), content, config); await promises.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8"); tailwindSpinner?.succeed(); } async function transformTailwindContent(input, content, config) { const sourceFile = await _createSourceFile(input, config); const configObject = sourceFile.getDescendantsOfKi