UNPKG

shadcn-vue

Version:
1,617 lines (1,599 loc) 127 kB
#!/usr/bin/env node // src/commands/init.ts import { promises as fs9 } from "node:fs"; // src/utils/errors.ts var MISSING_DIR_OR_EMPTY_PROJECT = "1"; var MISSING_CONFIG = "3"; var TAILWIND_NOT_CONFIGURED = "5"; var IMPORT_ALIAS_MISSING = "6"; var UNSUPPORTED_FRAMEWORK = "7"; var BUILD_MISSING_REGISTRY_FILE = "13"; // src/utils/frameworks.ts var FRAMEWORKS = { vite: { name: "vite", label: "Vite", links: { installation: "https://shadcn-vue.com/docs/installation/vite", tailwind: "https://tailwindcss.com/docs/guides/vite" } }, nuxt: { name: "nuxt", label: "Nuxt", links: { installation: "https://shadcn-vue.com/docs/installation/nuxt", tailwind: "https://tailwindcss.com/docs/guides/nuxtjs" } }, astro: { name: "astro", label: "Astro", links: { installation: "https://shadcn-vue.com/docs/installation/astro", tailwind: "https://tailwindcss.com/docs/guides/astro" } }, laravel: { name: "laravel", label: "Laravel", links: { installation: "https://shadcn-vue.com/docs/installation/laravel", tailwind: "https://tailwindcss.com/docs/guides/laravel" } }, manual: { name: "manual", label: "Manual", links: { installation: "https://shadcn-vue.com/docs/installation/manual", tailwind: "https://tailwindcss.com/docs/installation" } } }; // src/utils/resolve-import.ts import { createPathsMatcher } from "get-tsconfig"; function resolveImport(importPath, config) { const matcher = createPathsMatcher(config); if (matcher === null) { return; } const paths = matcher(importPath); return paths[0]; } // src/utils/get-config.ts import { cosmiconfig } from "cosmiconfig"; import { getTsconfig } from "get-tsconfig"; import path from "pathe"; import { z } from "zod"; // src/utils/highlighter.ts import { colors } from "consola/utils"; var highlighter = { error: colors.red, warn: colors.yellow, info: colors.cyan, success: colors.green }; // src/utils/get-config.ts var TAILWIND_CSS_PATH = { nuxt: "assets/css/tailwind.css", vite: "src/assets/index.css", laravel: "resources/css/app.css", astro: "src/styles/globals.css" }; var DEFAULT_COMPONENTS = "@/components"; var DEFAULT_UTILS = "@/lib/utils"; var DEFAULT_TAILWIND_CSS = TAILWIND_CSS_PATH.nuxt; var DEFAULT_TAILWIND_CONFIG = "tailwind.config.js"; var explorer = cosmiconfig("components", { searchPlaces: ["components.json"] }); var rawConfigSchema = z.object({ $schema: z.string().optional(), style: z.string(), typescript: z.boolean().default(true), tailwind: z.object({ config: z.string().optional(), css: z.string(), baseColor: z.string(), cssVariables: z.boolean().default(true), prefix: z.string().default("").optional() }), aliases: z.object({ components: z.string(), composables: z.string().optional(), utils: z.string(), ui: z.string().optional(), lib: z.string().optional() }), iconLibrary: z.string().optional() }).strict(); var configSchema = rawConfigSchema.extend({ resolvedPaths: z.object({ cwd: z.string(), tailwindConfig: z.string(), tailwindCss: z.string(), utils: z.string(), components: z.string(), composables: z.string(), lib: z.string(), ui: z.string() }) }); async function getConfig(cwd) { const config = await getRawConfig(cwd); if (!config) { return null; } if (!config.iconLibrary) { config.iconLibrary = config.style === "new-york" ? "radix" : "lucide"; } return await resolveConfigPaths(cwd, config); } function getTSConfig(cwd, tsconfigName) { const parsedConfig = getTsconfig(path.resolve(cwd, "package.json"), tsconfigName); if (parsedConfig === null) { throw new Error( `Failed to find ${highlighter.info(tsconfigName)}` ); } return parsedConfig; } async function resolveConfigPaths(cwd, config) { const tsconfigType = config.typescript ? "tsconfig.json" : "jsconfig.json"; const tsConfig = getTSConfig(cwd, tsconfigType); return configSchema.parse({ ...config, resolvedPaths: { cwd, tailwindConfig: config.tailwind.config ? path.resolve(cwd, config.tailwind.config) : "", tailwindCss: path.resolve(cwd, config.tailwind.css), utils: await resolveImport(config.aliases.utils, tsConfig), components: await resolveImport(config.aliases.components, tsConfig), ui: config.aliases.ui ? await resolveImport(config.aliases.ui, tsConfig) : path.resolve( await resolveImport(config.aliases.components, tsConfig) ?? cwd, "ui" ), // TODO: Make this configurable. // For now, we assume the lib and hooks directories are one level up from the components directory. lib: config.aliases.lib ? await resolveImport(config.aliases.lib, tsConfig) : path.resolve( await resolveImport(config.aliases.utils, tsConfig) ?? cwd, ".." ), composables: config.aliases.composables ? await resolveImport(config.aliases.composables, tsConfig) : path.resolve( await resolveImport(config.aliases.components, tsConfig) ?? cwd, "..", "composables" ) } }); } async function getRawConfig(cwd) { try { const configResult = await explorer.search(cwd); if (!configResult) { return null; } return rawConfigSchema.parse(configResult.config); } catch (error) { throw new Error(`Invalid configuration found in ${cwd}/components.json.`); } } async function getTargetStyleFromConfig(cwd, fallback) { const projectInfo = await getProjectInfo(cwd); return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback; } // src/utils/get-package-info.ts import fs from "fs-extra"; import path2 from "pathe"; function getPackageInfo(cwd = "", shouldThrow = true) { const packageJsonPath = path2.join(cwd, "package.json"); return fs.readJSONSync(packageJsonPath, { throws: shouldThrow }); } // src/utils/get-project-info.ts import fs2 from "fs-extra"; import { parseTsconfig } from "get-tsconfig"; import path3 from "pathe"; import { glob } from "tinyglobby"; import { z as z2 } from "zod"; var PROJECT_SHARED_IGNORE = [ "**/node_modules/**", ".nuxt", "public", "dist", "build" ]; var TS_CONFIG_SCHEMA = z2.object({ compilerOptions: z2.object({ paths: z2.record(z2.string().or(z2.array(z2.string()))) }) }); async function getProjectInfo(cwd) { const [ configFiles, typescript, tailwindConfigFile, tailwindCssFile, tailwindVersion, aliasPrefix, packageJson ] = await Promise.all([ glob("**/{nuxt,vite,astro}.config.*|composer.json", { cwd, deep: 3, ignore: PROJECT_SHARED_IGNORE }), isTypeScriptProject(cwd), getTailwindConfigFile(cwd), getTailwindCssFile(cwd), getTailwindVersion(cwd), getTsConfigAliasPrefix(cwd), getPackageInfo(cwd, false) ]); const type = { framework: FRAMEWORKS.manual, typescript, tailwindConfigFile, tailwindCssFile, tailwindVersion, aliasPrefix }; if (configFiles.find((file) => file.startsWith("nuxt.config."))?.length) { type.framework = FRAMEWORKS.nuxt; return type; } if (configFiles.find((file) => file.startsWith("astro.config."))?.length) { type.framework = FRAMEWORKS.astro; return type; } if (configFiles.find((file) => file.startsWith("composer.json"))?.length) { type.framework = FRAMEWORKS.laravel; return type; } if (configFiles.find((file) => file.startsWith("vite.config."))?.length) { type.framework = FRAMEWORKS.vite; return type; } return type; } async function getTailwindVersion(cwd) { const [packageInfo, config] = await Promise.all([ getPackageInfo(cwd), getConfig(cwd) ]); if (config?.tailwind?.config === "") { return "v4"; } if (!packageInfo?.dependencies?.tailwindcss && !packageInfo?.devDependencies?.tailwindcss) { return null; } if (/^(?:\^|~)?3(?:\.\d+)*(?:-.*)?$/.test( packageInfo?.dependencies?.tailwindcss || packageInfo?.devDependencies?.tailwindcss || "" )) { return "v3"; } return "v4"; } async function getTailwindCssFile(cwd) { const [files, tailwindVersion] = await Promise.all([ glob(["**/*.css", "**/*.scss"], { cwd, deep: 5, ignore: PROJECT_SHARED_IGNORE }), getTailwindVersion(cwd) ]); if (!files.length) { return null; } const needle = tailwindVersion === "v4" ? `@import "tailwindcss"` : "@tailwind base"; for (const file of files) { const contents = await fs2.readFile(path3.resolve(cwd, file), "utf8"); if (contents.includes(`@import "tailwindcss"`) || contents.includes(`@import 'tailwindcss'`) || contents.includes(`@tailwind base`)) { return file; } } return null; } async function getTailwindConfigFile(cwd) { const files = await glob("tailwind.config.*", { cwd, deep: 3, ignore: PROJECT_SHARED_IGNORE }); if (!files.length) { return null; } return files[0]; } async function getTsConfigAliasPrefix(cwd) { const isTypescript = await isTypeScriptProject(cwd); const tsconfigType = isTypescript ? "tsconfig.json" : "jsconfig.json"; const tsConfig = getTSConfig(cwd, tsconfigType); const parsedTsConfig = parseTsconfig(tsConfig.path); const aliasPaths = parsedTsConfig.compilerOptions?.paths ?? {}; for (const [alias, paths] of Object.entries(aliasPaths)) { if (paths.includes("./*") || paths.includes("./src/*") || paths.includes("./app/*") || paths.includes("./resources/js/*")) { const cleanAlias = alias.replace(/\/\*$/, "") ?? null; return cleanAlias === "#build" ? "@" : cleanAlias; } } return Object.keys(aliasPaths)?.[0]?.replace(/\/\*$/, "") ?? null; } async function isTypeScriptProject(cwd) { const files = await glob("tsconfig.*", { cwd, deep: 1, ignore: PROJECT_SHARED_IGNORE }); return files.length > 0; } async function getProjectConfig(cwd, defaultProjectInfo = null) { const [existingConfig, projectInfo] = await Promise.all([ getConfig(cwd), !defaultProjectInfo ? getProjectInfo(cwd) : Promise.resolve(defaultProjectInfo) ]); if (existingConfig) { return existingConfig; } if (!projectInfo || !projectInfo.tailwindCssFile || projectInfo.tailwindVersion === "v3" && !projectInfo.tailwindConfigFile) { return null; } const config = { $schema: "https://shadcn-vue.com/schema.json", typescript: projectInfo.typescript, style: "new-york", tailwind: { config: projectInfo.tailwindConfigFile ?? "", baseColor: "zinc", css: projectInfo.tailwindCssFile, cssVariables: true, prefix: "" }, iconLibrary: "lucide", aliases: { components: `${projectInfo.aliasPrefix}/components`, ui: `${projectInfo.aliasPrefix}/components/ui`, composables: `${projectInfo.aliasPrefix}/composables`, lib: `${projectInfo.aliasPrefix}/lib`, utils: `${projectInfo.aliasPrefix}/lib/utils` } }; return await resolveConfigPaths(cwd, config); } async function getProjectTailwindVersionFromConfig(config) { if (!config.resolvedPaths?.cwd) { return "v3"; } const projectInfo = await getProjectInfo(config.resolvedPaths.cwd); if (!projectInfo?.tailwindVersion) { return null; } return projectInfo.tailwindVersion; } // src/utils/logger.ts import consola from "consola"; var logger = { error(...args) { consola.log(highlighter.error(args.join(" "))); }, warn(...args) { consola.log(highlighter.warn(args.join(" "))); }, info(...args) { consola.log(highlighter.info(args.join(" "))); }, success(...args) { consola.log(highlighter.success(args.join(" "))); }, log(...args) { consola.log(args.join(" ")); }, break() { consola.log(""); } }; // src/utils/spinner.ts import ora from "ora"; function spinner(text, options) { return ora({ text, isSilent: options?.silent }); } // src/preflights/preflight-init.ts import fs3 from "fs-extra"; import path4 from "pathe"; async function preFlightInit(options) { const errors = {}; if (!fs3.existsSync(options.cwd) || !fs3.existsSync(path4.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 (fs3.existsSync(path4.resolve(options.cwd, "components.json")) && !options.force) { projectSpinner?.fail(); logger.break(); logger.error( `A ${highlighter.info( "components.json" )} file already exists at ${highlighter.info( options.cwd )}. To start over, remove the ${highlighter.info( "components.json" )} file and run ${highlighter.info("init")} again.` ); logger.break(); process.exit(1); } projectSpinner?.succeed(); const frameworkSpinner = spinner(`Verifying framework.`, { silent: options.silent }).start(); const projectInfo = await getProjectInfo(options.cwd); if (!projectInfo || projectInfo?.framework.name === "manual") { errors[UNSUPPORTED_FRAMEWORK] = true; frameworkSpinner?.fail(); logger.break(); if (projectInfo?.framework.links.installation) { logger.error( `We could not detect a supported framework at ${highlighter.info( options.cwd )}. Visit ${highlighter.info( projectInfo?.framework.links.installation )} to manually configure your project. Once configured, you can use the cli to add components.` ); } logger.break(); process.exit(1); } frameworkSpinner?.succeed( `Verifying framework. Found ${highlighter.info( projectInfo.framework.label )}.` ); let tailwindSpinnerMessage = "Validating Tailwind CSS."; if (projectInfo.tailwindVersion === "v4") { tailwindSpinnerMessage = `Validating Tailwind CSS config. Found ${highlighter.info( "v4" )}.`; } const tailwindSpinner = spinner(tailwindSpinnerMessage, { silent: options.silent }).start(); if (projectInfo.tailwindVersion === "v3" && (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile)) { errors[TAILWIND_NOT_CONFIGURED] = true; tailwindSpinner?.fail(); } else if (projectInfo.tailwindVersion === "v4" && !projectInfo?.tailwindCssFile) { errors[TAILWIND_NOT_CONFIGURED] = true; tailwindSpinner?.fail(); } else if (!projectInfo.tailwindVersion) { errors[TAILWIND_NOT_CONFIGURED] = true; tailwindSpinner?.fail(); } else { tailwindSpinner?.succeed(); } const tsConfigSpinner = spinner(`Validating import alias.`, { silent: options.silent }).start(); if (!projectInfo?.aliasPrefix) { errors[IMPORT_ALIAS_MISSING] = true; tsConfigSpinner?.fail(); } else { tsConfigSpinner?.succeed(); } if (Object.keys(errors).length > 0) { if (errors[TAILWIND_NOT_CONFIGURED]) { logger.break(); logger.error( `No Tailwind CSS configuration found at ${highlighter.info( options.cwd )}.` ); logger.error( `It is likely you do not have Tailwind CSS installed or have an invalid configuration.` ); logger.error(`Install Tailwind CSS then try again.`); if (projectInfo?.framework.links.tailwind) { logger.error( `Visit ${highlighter.info( projectInfo?.framework.links.tailwind )} to get started.` ); } } if (errors[IMPORT_ALIAS_MISSING]) { logger.break(); logger.error(`No import alias found in your tsconfig.json file.`); if (projectInfo?.framework.links.installation) { logger.error( `Visit ${highlighter.info( projectInfo?.framework.links.installation )} to learn how to set an import alias.` ); } } logger.break(); process.exit(1); } return { errors, projectInfo }; } // src/registry/schema.ts import { z as z3 } from "zod"; var registryItemTypeSchema = z3.enum([ "registry:lib", "registry:block", "registry:component", "registry:ui", "registry:hook", "registry:page", "registry:file", "registry:theme", "registry:style", // Internal use only "registry:example", "registry:internal" ]); var registryItemFileSchema = z3.discriminatedUnion("type", [ // Target is required for registry:file and registry:page z3.object({ path: z3.string(), content: z3.string().optional(), type: z3.enum(["registry:file", "registry:page"]), target: z3.string() }), z3.object({ path: z3.string(), content: z3.string().optional(), type: registryItemTypeSchema.exclude(["registry:file", "registry:page"]), target: z3.string().optional() }) ]); var registryItemTailwindSchema = z3.object({ config: z3.object({ content: z3.array(z3.string()).optional(), theme: z3.record(z3.string(), z3.any()).optional(), plugins: z3.array(z3.string()).optional() }).optional() }); var registryItemCssVarsSchema = z3.object({ theme: z3.record(z3.string(), z3.string()).optional(), light: z3.record(z3.string(), z3.string()).optional(), dark: z3.record(z3.string(), z3.string()).optional() }); var registryItemCssSchema = z3.record( z3.string(), z3.lazy( () => z3.union([ z3.string(), z3.record( z3.string(), z3.union([z3.string(), z3.record(z3.string(), z3.string())]) ) ]) ) ); var registryItemSchema = z3.object({ $schema: z3.string().optional(), extends: z3.string().optional(), name: z3.string(), type: registryItemTypeSchema, title: z3.string().optional(), author: z3.string().min(2).optional(), description: z3.string().optional(), dependencies: z3.array(z3.string()).optional(), devDependencies: z3.array(z3.string()).optional(), registryDependencies: z3.array(z3.string()).optional(), files: z3.array(registryItemFileSchema).optional(), tailwind: registryItemTailwindSchema.optional(), cssVars: registryItemCssVarsSchema.optional(), css: registryItemCssSchema.optional(), meta: z3.record(z3.string(), z3.any()).optional(), docs: z3.string().optional(), categories: z3.array(z3.string()).optional() }); var registrySchema = z3.object({ name: z3.string(), homepage: z3.string(), items: z3.array(registryItemSchema) }); var registryIndexSchema = z3.array(registryItemSchema); var stylesSchema = z3.array( z3.object({ name: z3.string(), label: z3.string() }) ); var iconsSchema = z3.record( z3.string(), z3.record(z3.string(), z3.string()) ); var registryBaseColorSchema = z3.object({ inlineColors: z3.object({ light: z3.record(z3.string(), z3.string()), dark: z3.record(z3.string(), z3.string()) }), cssVars: registryItemCssVarsSchema, cssVarsV4: registryItemCssVarsSchema.optional(), inlineColorsTemplate: z3.string(), cssVarsTemplate: z3.string() }); var registryResolvedItemsTreeSchema = registryItemSchema.pick({ dependencies: true, devDependencies: true, files: true, tailwind: true, cssVars: true, css: true, docs: true }); // src/utils/handle-error.ts import { consola as consola2 } from "consola"; function handleError(error) { consola2.log("this is error: ", error); if (typeof error === "string") { consola2.error(error); process.exit(1); } if (error instanceof Error) { consola2.error(error.message); process.exit(1); } consola2.error("Something went wrong. Please try again."); process.exit(1); } // src/utils/updaters/update-tailwind-config.ts import { promises as fs4 } from "node:fs"; import { tmpdir } from "node:os"; import deepmerge from "deepmerge"; import path5 from "pathe"; import objectToString from "stringify-object"; import { Project, QuoteKind, ScriptKind, SyntaxKind } from "ts-morph"; async function updateTailwindConfig(tailwindConfig, config, options) { if (!tailwindConfig) { return; } options = { silent: false, tailwindVersion: "v3", ...options }; if (options.tailwindVersion === "v4") { return; } const tailwindFileRelativePath = path5.relative( config.resolvedPaths.cwd, config.resolvedPaths.tailwindConfig ); const tailwindSpinner = spinner( `Updating ${highlighter.info(tailwindFileRelativePath)}`, { silent: options.silent } ).start(); const raw = await fs4.readFile(config.resolvedPaths.tailwindConfig, "utf8"); const output = await transformTailwindConfig(raw, tailwindConfig, config); await fs4.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8"); tailwindSpinner?.succeed(); } async function transformTailwindConfig(input, tailwindConfig, config) { const sourceFile = await _createSourceFile(input, config); const configObject = sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression).find( (node) => node.getProperties().some( (property) => property.isKind(SyntaxKind.PropertyAssignment) && property.getName() === "content" ) ); if (!configObject) { return input; } const quoteChar = _getQuoteChar(configObject); addTailwindConfigProperty( configObject, { name: "darkMode", value: "class" }, { quoteChar } ); tailwindConfig.plugins?.forEach((plugin) => { addTailwindConfigPlugin(configObject, plugin); }); if (tailwindConfig.theme) { await addTailwindConfigTheme(configObject, tailwindConfig.theme); } return sourceFile.getFullText(); } function addTailwindConfigProperty(configObject, property, { quoteChar }) { const existingProperty = configObject.getProperty("darkMode"); if (!existingProperty) { const newProperty = { name: property.name, initializer: `[${quoteChar}${property.value}${quoteChar}]` }; if (property.name === "darkMode") { configObject.insertPropertyAssignment(0, newProperty); return configObject; } configObject.addPropertyAssignment(newProperty); return configObject; } if (existingProperty.isKind(SyntaxKind.PropertyAssignment)) { const initializer = existingProperty.getInitializer(); const newValue = `${quoteChar}${property.value}${quoteChar}`; if (initializer?.isKind(SyntaxKind.StringLiteral)) { const initializerText = initializer.getText(); initializer.replaceWithText(`[${initializerText}, ${newValue}]`); return configObject; } if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) { if (initializer.getElements().map((element) => element.getText()).includes(newValue)) { return configObject; } initializer.addElement(newValue); } return configObject; } return configObject; } async function addTailwindConfigTheme(configObject, theme) { if (!configObject.getProperty("theme")) { configObject.addPropertyAssignment({ name: "theme", initializer: "{}" }); } nestSpreadProperties(configObject); const themeProperty = configObject.getPropertyOrThrow("theme")?.asKindOrThrow(SyntaxKind.PropertyAssignment); const themeInitializer = themeProperty.getInitializer(); if (themeInitializer?.isKind(SyntaxKind.ObjectLiteralExpression)) { const themeObjectString = themeInitializer.getText(); const themeObject = await parseObjectLiteral(themeObjectString); const result = deepmerge(themeObject, theme, { arrayMerge: (dst, src) => src }); const resultString = objectToString(result).replace(/'\.\.\.(.*)'/g, "...$1").replace(/'"/g, "'").replace(/"'/g, "'").replace(/'\[/g, "[").replace(/\]'/g, "]").replace(/'\\'/g, "'").replace(/\\'/g, "'").replace(/\\''/g, "'").replace(/''/g, "'"); themeInitializer.replaceWithText(resultString); } unnestSpreadProperties(configObject); } function addTailwindConfigPlugin(configObject, plugin) { const existingPlugins = configObject.getProperty("plugins"); if (!existingPlugins) { configObject.addPropertyAssignment({ name: "plugins", initializer: `[${plugin}]` }); return configObject; } if (existingPlugins.isKind(SyntaxKind.PropertyAssignment)) { const initializer = existingPlugins.getInitializer(); if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) { if (initializer.getElements().map((element) => { return element.getText().replace(/["']/g, ""); }).includes(plugin.replace(/["']/g, ""))) { return configObject; } initializer.addElement(plugin); } return configObject; } return configObject; } async function _createSourceFile(input, config) { const dir = await fs4.mkdtemp(path5.join(tmpdir(), "shadcn-")); const resolvedPath = config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts"; const tempFile = path5.join(dir, `shadcn-${path5.basename(resolvedPath)}`); const project = new Project({ compilerOptions: {} }); const sourceFile = project.createSourceFile(tempFile, input, { // Note: .js and .mjs can still be valid for TS projects. // We can't infer TypeScript from config.tsx. scriptKind: path5.extname(resolvedPath) === ".ts" ? ScriptKind.TS : ScriptKind.JS }); return sourceFile; } function _getQuoteChar(configObject) { return configObject.getFirstDescendantByKind(SyntaxKind.StringLiteral)?.getQuoteKind() === QuoteKind.Single ? "'" : '"'; } function nestSpreadProperties(obj) { const properties = obj.getProperties(); for (let i = 0; i < properties.length; i++) { const prop = properties[i]; if (prop.isKind(SyntaxKind.SpreadAssignment)) { const spreadAssignment = prop.asKindOrThrow(SyntaxKind.SpreadAssignment); const spreadText = spreadAssignment.getExpression().getText(); obj.insertPropertyAssignment(i, { // Need to escape the name with " so that deepmerge doesn't mishandle the key name: `"___${spreadText.replace(/^\.\.\./, "")}"`, initializer: `"...${spreadText.replace(/^\.\.\./, "")}"` }); spreadAssignment.remove(); } else if (prop.isKind(SyntaxKind.PropertyAssignment)) { const propAssignment = prop.asKindOrThrow(SyntaxKind.PropertyAssignment); const initializer = propAssignment.getInitializer(); if (initializer && initializer.isKind(SyntaxKind.ObjectLiteralExpression)) { nestSpreadProperties( initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) ); } else if (initializer && initializer.isKind(SyntaxKind.ArrayLiteralExpression)) { nestSpreadElements( initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) ); } } } } function nestSpreadElements(arr) { const elements = arr.getElements(); for (let j = 0; j < elements.length; j++) { const element = elements[j]; if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { nestSpreadProperties( element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) ); } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { nestSpreadElements( element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) ); } else if (element.isKind(SyntaxKind.SpreadElement)) { const spreadText = element.getText(); arr.removeElement(j); arr.insertElement(j, `"${spreadText}"`); } } } function unnestSpreadProperties(obj) { const properties = obj.getProperties(); for (let i = 0; i < properties.length; i++) { const prop = properties[i]; if (prop.isKind(SyntaxKind.PropertyAssignment)) { const propAssignment = prop; const initializer = propAssignment.getInitializer(); if (initializer && initializer.isKind(SyntaxKind.StringLiteral)) { const value = initializer.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue(); if (value.startsWith("...")) { obj.insertSpreadAssignment(i, { expression: value.slice(3) }); propAssignment.remove(); } } else if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) { unnestSpreadProperties(initializer); } else if (initializer && initializer.isKind(SyntaxKind.ArrayLiteralExpression)) { unnsetSpreadElements( initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) ); } } } } function unnsetSpreadElements(arr) { const elements = arr.getElements(); for (let j = 0; j < elements.length; j++) { const element = elements[j]; if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { unnestSpreadProperties( element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) ); } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { unnsetSpreadElements( element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) ); } else if (element.isKind(SyntaxKind.StringLiteral)) { const spreadText = element.getText(); const spreadTest = /^['"](\.\.\..*)['"]$/g; if (spreadTest.test(spreadText)) { arr.removeElement(j); arr.insertElement(j, spreadText.replace(spreadTest, "$1")); } } } } async function parseObjectLiteral(objectLiteralString) { const sourceFile = await _createSourceFile( `const theme = ${objectLiteralString}`, null ); const statement = sourceFile.getStatements()[0]; if (statement?.getKind() === SyntaxKind.VariableStatement) { const declaration = statement.getDeclarationList()?.getDeclarations()[0]; const initializer = declaration.getInitializer(); if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) { return await parseObjectLiteralExpression(initializer); } } throw new Error("Invalid input: not an object literal"); } function parseObjectLiteralExpression(node) { const result = {}; for (const property of node.getProperties()) { if (property.isKind(SyntaxKind.PropertyAssignment)) { const name = property.getName().replace(/'/g, ""); if (property.getInitializer()?.isKind(SyntaxKind.ObjectLiteralExpression)) { result[name] = parseObjectLiteralExpression( property.getInitializer() ); } else if (property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression)) { result[name] = parseArrayLiteralExpression( property.getInitializer() ); } else { result[name] = parseValue(property.getInitializer()); } } } return result; } function parseArrayLiteralExpression(node) { const result = []; for (const element of node.getElements()) { if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { result.push( parseObjectLiteralExpression( element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) ) ); } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { result.push( parseArrayLiteralExpression( element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) ) ); } else { result.push(parseValue(element)); } } return result; } function parseValue(node) { switch (node.getKind()) { case SyntaxKind.StringLiteral: return node.getText(); case SyntaxKind.NumericLiteral: return Number(node.getText()); case SyntaxKind.TrueKeyword: return true; case SyntaxKind.FalseKeyword: return false; case SyntaxKind.NullKeyword: return null; case SyntaxKind.ArrayLiteralExpression: return node.getElements().map(parseValue); case SyntaxKind.ObjectLiteralExpression: return parseObjectLiteralExpression(node); default: return node.getText(); } } function buildTailwindThemeColorsFromCssVars(cssVars) { const result = {}; for (const key of Object.keys(cssVars)) { const parts = key.split("-"); const colorName = parts[0]; const subType = parts.slice(1).join("-"); if (subType === "") { if (typeof result[colorName] === "object") { result[colorName].DEFAULT = `hsl(var(--${key}))`; } else { result[colorName] = `hsl(var(--${key}))`; } } else { if (typeof result[colorName] !== "object") { result[colorName] = { DEFAULT: `hsl(var(--${colorName}))` }; } result[colorName][subType] = `hsl(var(--${key}))`; } } for (const [colorName, value] of Object.entries(result)) { if (typeof value === "object" && value.DEFAULT === `hsl(var(--${colorName}))` && !(colorName in cssVars)) { delete value.DEFAULT; } } return result; } // src/registry/api.ts import deepmerge2 from "deepmerge"; import { ofetch } from "ofetch"; import path6 from "pathe"; import { ProxyAgent } from "undici"; import { z as z4 } from "zod"; var REGISTRY_URL = process.env.REGISTRY_URL ?? "https://shadcn-vue.com/r"; var agent = process.env.https_proxy ? new ProxyAgent(process.env.https_proxy) : void 0; var registryCache = /* @__PURE__ */ new Map(); async function getRegistryIndex() { try { const [result] = await fetchRegistry(["index.json"]); return registryIndexSchema.parse(result); } catch (error) { logger.error("\n"); handleError(error); } } async function getRegistryStyles() { try { const [result] = await fetchRegistry(["styles/index.json"]); return stylesSchema.parse(result); } catch (error) { logger.error("\n"); handleError(error); return []; } } async function getRegistryIcons() { try { const [result] = await fetchRegistry(["icons/index.json"]); return iconsSchema.parse(result); } catch (error) { handleError(error); return {}; } } async function getRegistryItem(name, style) { try { const [result] = await fetchRegistry([ isUrl(name) ? name : `styles/${style}/${name}.json` ]); return registryItemSchema.parse(result); } catch (error) { logger.break(); handleError(error); return null; } } var BASE_COLORS = [ { name: "neutral", label: "Neutral" }, { name: "gray", label: "Gray" }, { name: "zinc", label: "Zinc" }, { name: "stone", label: "Stone" }, { name: "slate", label: "Slate" } ]; async function getRegistryBaseColors() { return BASE_COLORS; } async function getRegistryBaseColor(baseColor) { try { const [result] = await fetchRegistry([`colors/${baseColor}.json`]); return registryBaseColorSchema.parse(result); } catch (error) { handleError(error); } } async function fetchTree(style, tree) { try { const paths = tree.map((item) => `styles/${style}/${item.name}.json`); const result = await fetchRegistry(paths); return registryIndexSchema.parse(result); } catch (error) { handleError(error); } } async function getItemTargetPath(config, item, override) { if (override) { return override; } if (item.type === "registry:ui") { return config.resolvedPaths.ui ?? config.resolvedPaths.components; } const [parent, type] = item.type?.split(":") ?? []; if (!(parent in config.resolvedPaths)) { return null; } return path6.join( config.resolvedPaths[parent], type ); } async function fetchRegistry(paths) { try { const results = await Promise.all( paths.map(async (path20) => { const url = getRegistryUrl(path20); if (registryCache.has(url)) { return registryCache.get(url); } const response = await ofetch(url, { dispatcher: agent, parseResponse: JSON.parse }).catch((error) => { throw new Error(error.data); }); registryCache.set(url, response); return response; }) ); return results; } catch (error) { logger.error("\n"); handleError(error); return []; } } function getRegistryItemFileTargetPath(file, config, override) { if (override) { return override; } if (file.type === "registry:ui") { const folder = file.path.split("/")[1]; return path6.join(config.resolvedPaths.ui, folder); } if (file.type === "registry:lib") { return config.resolvedPaths.lib; } if (file.type === "registry:block" || file.type === "registry:component") { return config.resolvedPaths.components; } if (file.type === "registry:hook") { return config.resolvedPaths.composables; } if (file.type === "registry:page") { return config.resolvedPaths.components; } return config.resolvedPaths.components; } async function registryResolveItemsTree(names, config) { try { const index = await getRegistryIndex(); if (!index) { return null; } if (names.includes("index")) { names.unshift("index"); } const registryDependencies = /* @__PURE__ */ new Set(); for (const name of names) { const itemRegistryDependencies = await resolveRegistryDependencies( name, config ); itemRegistryDependencies.forEach((dep) => registryDependencies.add(dep)); } const uniqueRegistryDependencies = Array.from(registryDependencies); const result = await fetchRegistry(uniqueRegistryDependencies); const payload = z4.array(registryItemSchema).parse(result); if (!payload) { return null; } if (names.includes("index")) { if (config.tailwind.baseColor) { const theme = await registryGetTheme(config.tailwind.baseColor, config); if (theme) { payload.unshift(theme); } } } let tailwind = {}; payload.forEach((item) => { tailwind = deepmerge2(tailwind, item.tailwind ?? {}); }); let cssVars = {}; payload.forEach((item) => { cssVars = deepmerge2(cssVars, item.cssVars ?? {}); }); let docs = ""; payload.forEach((item) => { if (item.docs) { docs += `${item.docs} `; } }); return registryResolvedItemsTreeSchema.parse({ dependencies: Array.from(new Set(payload.flatMap((item) => item.dependencies ?? []))), devDependencies: Array.from(new Set(payload.flatMap((item) => item.devDependencies ?? []))), files: deepmerge2.all(payload.map((item) => item.files ?? [])), tailwind, cssVars, docs }); } catch (error) { handleError(error); return null; } } async function resolveRegistryDependencies(url, config) { const visited = /* @__PURE__ */ new Set(); const payload = []; const style = config.resolvedPaths?.cwd ? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style) : config.style; async function resolveDependencies(itemUrl) { const url2 = getRegistryUrl( isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json` ); if (visited.has(url2)) { return; } visited.add(url2); try { const [result] = await fetchRegistry([url2]); const item = registryItemSchema.parse(result); payload.push(url2); if (item.registryDependencies) { for (const dependency of item.registryDependencies) { await resolveDependencies(dependency); } } } catch (error) { console.error( `Error fetching or parsing registry item at ${itemUrl}:`, error ); } } await resolveDependencies(url); return Array.from(new Set(payload)); } async function registryGetTheme(name, config) { const [baseColor, tailwindVersion] = await Promise.all([ getRegistryBaseColor(name), getProjectTailwindVersionFromConfig(config) ]); if (!baseColor) { return null; } const theme = { name, type: "registry:theme", tailwind: { config: { theme: { extend: { borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" }, colors: {} } } } }, cssVars: { theme: {}, light: { radius: "0.5rem" }, dark: {} } }; if (config.tailwind.cssVariables) { theme.tailwind.config.theme.extend.colors = { ...theme.tailwind.config.theme.extend.colors, ...buildTailwindThemeColorsFromCssVars(baseColor.cssVars.dark ?? {}) }; theme.cssVars = { theme: { ...baseColor.cssVars.theme, ...theme.cssVars.theme }, light: { ...baseColor.cssVars.light, ...theme.cssVars.light }, dark: { ...baseColor.cssVars.dark, ...theme.cssVars.dark } }; if (tailwindVersion === "v4" && baseColor.cssVarsV4) { theme.cssVars = { theme: { ...baseColor.cssVarsV4.theme, ...theme.cssVars.theme }, light: { ...theme.cssVars.light, ...baseColor.cssVarsV4.light }, dark: { ...theme.cssVars.dark, ...baseColor.cssVarsV4.dark } }; } } return theme; } function getRegistryUrl(path20) { if (isUrl(path20)) { const url = new URL(path20); if (url.pathname.match(/\/chat\/b\//) && !url.pathname.endsWith("/json")) { url.pathname = `${url.pathname}/json`; } return url.toString(); } return `${REGISTRY_URL}/${path20}`; } function isUrl(path20) { try { new URL(path20); return true; } catch (error) { return false; } } async function resolveRegistryItems(names, config) { const registryDependencies = []; for (const name of names) { const itemRegistryDependencies = await resolveRegistryDependencies( name, config ); registryDependencies.push(...itemRegistryDependencies); } return Array.from(new Set(registryDependencies)); } // src/utils/updaters/update-css.ts import { promises as fs5 } from "node:fs"; import path7 from "pathe"; import postcss from "postcss"; async function updateCss(css, config, options) { if (!config.resolvedPaths.tailwindCss || !css || Object.keys(css).length === 0) { return; } options = { silent: false, ...options }; const cssFilepath = config.resolvedPaths.tailwindCss; const cssFilepathRelative = path7.relative( config.resolvedPaths.cwd, cssFilepath ); const cssSpinner = spinner( `Updating ${highlighter.info(cssFilepathRelative)}`, { silent: options.silent } ).start(); const raw = await fs5.readFile(cssFilepath, "utf8"); const output = await transformCss(raw, css); await fs5.writeFile(cssFilepath, output, "utf8"); cssSpinner.succeed(); } async function transformCss(input, css) { const plugins = [updateCssPlugin(css)]; const result = await postcss(plugins).process(input, { from: void 0 }); let output = result.css; output = output.replace(/\/\* ---break--- \*\//g, ""); output = output.replace(/(\n\s*\n)+/g, "\n\n"); output = output.trimEnd(); return output; } function updateCssPlugin(css) { return { postcssPlugin: "update-css", Once(root) { for (const [selector, properties] of Object.entries(css)) { if (selector.startsWith("@")) { const atRuleMatch = selector.match(/@([a-z-]+)\s*(.*)/i); if (!atRuleMatch) continue; const [, name, params] = atRuleMatch; if (name === "keyframes") { let themeInline = root.nodes?.find( (node) => node.type === "atrule" && node.name === "theme" && node.params === "inline" ); if (!themeInline) { themeInline = postcss.atRule({ name: "theme", params: "inline", raws: { semicolon: true, between: " ", before: "\n" } }); root.append(themeInline); root.insertBefore( themeInline, postcss.comment({ text: "---break---" }) ); } const keyframesRule = postcss.atRule({ name: "keyframes", params, raws: { semicolon: true, between: " ", before: "\n " } }); themeInline.append(keyframesRule); if (typeof properties === "object") { for (const [step, stepProps] of Object.entries(properties)) { processRule(keyframesRule, step, stepProps); } } } else if (name === "utility") { const utilityAtRule = root.nodes?.find( (node) => node.type === "atrule" && node.name === name && node.params === params ); if (!utilityAtRule) { const atRule = postcss.atRule({ name, params, raws: { semicolon: true, between: " ", before: "\n" } }); root.append(atRule); root.insertBefore( atRule, postcss.comment({ text: "---break---" }) ); if (typeof properties === "object") { for (const [prop, value] of Object.entries(properties)) { if (typeof value === "string") { const decl = postcss.decl({ prop, value, raws: { semicolon: true, before: "\n " } }); atRule.append(decl); } else if (typeof value === "object") { processRule(atRule, prop, value); } } } } else { if (typeof properties === "object") { for (const [prop, value] of Object.entries(properties)) { if (typeof value === "string") { const existingDecl = utilityAtRule.nodes?.find( (node) => node.type === "decl" && node.prop === prop ); const decl = postcss.decl({ prop, value, raws: { semicolon: true, before: "\n " } }); existingDecl ? existingDecl.replaceWith(decl) : utilityAtRule.append(decl); } else if (typeof value === "object") { processRule(utilityAtRule, prop, value); } } } } } else { processAtRule(root, name, params, properties); } } else { processRule(root, selector, properties); } } } }; } function processAtRule(root, name, params, properties) { let atRule = root.nodes?.find( (node) => node.type === "atrule" && node.name === name && node.params === params ); if (!atRule) { atRule = postcss.atRule({ name, params, raws: { semicolon: true, between: " ", before: "\n" } }); root.append(atRule); root.insertBefore(atRule, postcss.comment({ text: "---break---" })); } if (typeof properties === "object") { for (const [childSelector, childProps] of Object.entries(properties)) { if (childSelector.startsWith("@")) { const nestedMatch = childSelector.match(/@([a-z-]+)\s*(.*)/i); if (nestedMatch) { const [, nestedName, nestedParams] = nestedMatch; processAtRule(atRule, nestedName, nestedParams, childProps); } } else { processRule(atRule, childSelector, childProps); } } } else if (typeof properties === "string") { try { const parsed = postcss.parse(`.temp{${properties}}`); const tempRule = parsed.first; if (tempRule && tempRule.nodes) { const rule = postcss.rule({ selector: "temp", raws: { semicolon: true, between: " ", before: "\n " } }); tempRule.nodes.forEach((node) => { if (node.type === "decl") { const clone = node.clone(); clone.raws.before = "\n "; rule.append(clone); } }); if (rule.nodes?.length) { atRule.append(rule); } } } catch (error) { console.error("Error parsing at-rule content:", properties, error); throw error; } } } function processRule(parent, selector, properties) { let rule = parent.nodes?.find( (node) => node.type === "rule" && node.selector === selector ); if (!rule) { rule = postcss.rule({ selector, raws: { semicolon: true, between: " ", before: "\n " } }); parent.append(rule); } if (typeof properties === "object") { for (const [prop, value] of Object.entries(properties)) { if (typeof value === "string") { const decl = postcss.decl({ prop, value, raws: { semicolon: true, before: "\n " } }); const existingDecl = rule.nodes?.find( (node) => node.type === "decl" && node.prop === prop ); existingDecl ? existingDecl.replaceWith(decl) : rule.append(decl); } else if (typeof value === "object") { const nestedSelector = prop.startsWith("&") ? selector.replace(/^([^:]+)/, `$1${prop.substring(1)}`) : prop; processRule(parent, nestedSelector, value); } } } else if (typeof properties === "string") { try { const parsed = postcss.parse(`.temp{${properties}}`); const tempRule = parsed.first; if (tempRule && tempRule.nodes) { tempRule.nodes.forEach((node) => { if (node.type === "decl") { const clone = node.clone(); clone.raws.before = "\n "; rule?.append(clone); } }); } } catch (error) { console.error("Error parsing rule content:", selector, properties, error); throw error; } } } // src/utils/updaters/update-css-vars.ts import { promises as fs6 } from "node:fs"; import path8 from "node:path"; import postcss2 from "postcss"; import AtRule from "postcss/lib/at-rule"; import { z as z5 } from "zod"; async function updateCssVars(cssVars, config, options) { if (!config.resolvedPaths.tailwindCss || !Object.keys(cssVars ?? {}).length) { return; } options = { cleanupDefaultNextStyles: false, silent: false, tailwind