UNPKG

shadcn-vue

Version:
1,326 lines (1,304 loc) 102 kB
import { _ as registrySchema, a as registryBaseColorSchema, b as stylesSchema, c as registryIndexSchema, f as registryItemFileSchema, g as registryResolvedItemsTreeSchema, i as registriesIndexSchema, n as iconsSchema, p as registryItemSchema, r as rawConfigSchema, s as registryConfigSchema, t as configSchema, x as workspaceConfigSchema, y as searchResultsSchema } from "./schema-Brc22MYG.js"; import path, { basename } from "pathe"; import prompts from "prompts"; import { z } from "zod"; import { existsSync, promises, statSync } from "fs"; import deepmerge from "deepmerge"; import fsExtra from "fs-extra"; import { createPathsMatcher, getTsconfig } from "get-tsconfig"; import { coerce } from "semver"; import { glob } from "tinyglobby"; import { loadConfig } from "c12"; import { colors } from "consola/utils"; import consola from "consola"; import ora from "ora"; import { homedir, tmpdir } from "os"; import { Project, QuoteKind, ScriptKind, SyntaxKind } from "ts-morph"; import { transform } from "vue-metamorph"; import { transform as transform$1 } from "@unovue/detypes"; import { ofetch } from "ofetch"; import { ProxyAgent } from "undici"; import { createHash } from "crypto"; import objectToString from "stringify-object"; import fuzzysort from "fuzzysort"; //#region src/utils/frameworks.ts const FRAMEWORKS = { vite: { name: "vite", label: "Vite", links: { installation: "https://shadcn-vue.com/docs/installation/vite", tailwind: "https://tailwindcss.com/docs/guides/vite" } }, nuxt3: { name: "nuxt3", label: "Nuxt 3", links: { installation: "https://shadcn-vue.com/docs/installation/nuxt", tailwind: "https://tailwindcss.com/docs/guides/nuxtjs" } }, nuxt4: { name: "nuxt4", label: "Nuxt 4", 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" } }, inertia: { name: "inertia", label: "Inertia", links: { installation: "https://shadcn-vue.com/docs/installation/manual", tailwind: "https://tailwindcss.com/docs/installation" } } }; //#endregion //#region src/registry/constants.ts const REGISTRY_URL = process.env.REGISTRY_URL ?? "https://shadcn-vue.com/r"; const FALLBACK_STYLE = "new-york-v4"; const BASE_COLORS = [ { name: "neutral", label: "Neutral" }, { name: "gray", label: "Gray" }, { name: "zinc", label: "Zinc" }, { name: "stone", label: "Stone" }, { name: "slate", label: "Slate" } ]; const BUILTIN_REGISTRIES = { "@shadcn": `${REGISTRY_URL}/styles/{style}/{name}.json` }; const DEPRECATED_COMPONENTS = [{ name: "toast", deprecatedBy: "sonner", message: "The toast component is deprecated. Use the sonner component instead." }, { name: "toaster", deprecatedBy: "sonner", message: "The toaster component is deprecated. Use the sonner component instead." }]; //#endregion //#region src/utils/highlighter.ts const highlighter = { error: colors.red, warn: colors.yellow, info: colors.cyan, success: colors.green }; //#endregion //#region src/registry/errors.ts const RegistryErrorCode = { NETWORK_ERROR: "NETWORK_ERROR", NOT_FOUND: "NOT_FOUND", UNAUTHORIZED: "UNAUTHORIZED", FORBIDDEN: "FORBIDDEN", FETCH_ERROR: "FETCH_ERROR", NOT_CONFIGURED: "NOT_CONFIGURED", INVALID_CONFIG: "INVALID_CONFIG", MISSING_ENV_VARS: "MISSING_ENV_VARS", LOCAL_FILE_ERROR: "LOCAL_FILE_ERROR", PARSE_ERROR: "PARSE_ERROR", VALIDATION_ERROR: "VALIDATION_ERROR", UNKNOWN_ERROR: "UNKNOWN_ERROR" }; var RegistryError = class extends Error { constructor(message, options = {}) { super(message); this.name = "RegistryError"; this.code = options.code || RegistryErrorCode.UNKNOWN_ERROR; this.statusCode = options.statusCode; this.cause = options.cause; this.context = options.context; this.suggestion = options.suggestion; this.timestamp = /* @__PURE__ */ new Date(); if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor); } toJSON() { return { name: this.name, message: this.message, code: this.code, statusCode: this.statusCode, context: this.context, suggestion: this.suggestion, timestamp: this.timestamp, stack: this.stack }; } }; var RegistryNotFoundError = class extends RegistryError { constructor(url, cause) { const message = `The item at ${url} was not found. It may not exist at the registry.`; super(message, { code: RegistryErrorCode.NOT_FOUND, statusCode: 404, cause, context: { url }, suggestion: "Check if the item name is correct and the registry URL is accessible." }); this.url = url; this.name = "RegistryNotFoundError"; } }; var RegistryUnauthorizedError = class extends RegistryError { constructor(url, cause) { const message = `You are not authorized to access the item at ${url}. If this is a remote registry, you may need to authenticate.`; super(message, { code: RegistryErrorCode.UNAUTHORIZED, statusCode: 401, cause, context: { url }, suggestion: "Check your authentication credentials and environment variables." }); this.url = url; this.name = "RegistryUnauthorizedError"; } }; var RegistryForbiddenError = class extends RegistryError { constructor(url, cause) { const message = `You are not authorized to access the item at ${url}. If this is a remote registry, you may need to authenticate.`; super(message, { code: RegistryErrorCode.FORBIDDEN, statusCode: 403, cause, context: { url }, suggestion: "Check your authentication credentials and environment variables." }); this.url = url; this.name = "RegistryForbiddenError"; } }; var RegistryFetchError = class extends RegistryError { constructor(url, statusCode, responseBody, cause) { const baseMessage = statusCode ? `Failed to fetch from registry (${statusCode}): ${url}` : `Failed to fetch from registry: ${url}`; const message = typeof cause === "string" && cause ? `${baseMessage} - ${cause}` : baseMessage; let suggestion = "Check your network connection and try again."; if (statusCode === 404) suggestion = "The requested resource was not found. Check the URL or item name."; else if (statusCode === 500) suggestion = "The registry server encountered an error. Try again later."; else if (statusCode && statusCode >= 400 && statusCode < 500) suggestion = "There was a client error. Check your request parameters."; super(message, { code: RegistryErrorCode.FETCH_ERROR, statusCode, cause, context: { url, responseBody }, suggestion }); this.url = url; this.responseBody = responseBody; this.name = "RegistryFetchError"; } }; var RegistryNotConfiguredError = class extends RegistryError { constructor(registryName) { const message = registryName ? `Unknown registry "${registryName}". Make sure it is defined in components.json as follows: { "registries": { "${registryName}": "[URL_TO_REGISTRY]" } }` : "Unknown registry. Make sure it is defined in components.json under \"registries\"."; super(message, { code: RegistryErrorCode.NOT_CONFIGURED, context: { registryName }, suggestion: "Add the registry configuration to your components.json file. Consult the registry documentation for the correct format." }); this.registryName = registryName; this.name = "RegistryNotConfiguredError"; } }; var RegistryLocalFileError = class extends RegistryError { constructor(filePath, cause) { super(`Failed to read local registry file: ${filePath}`, { code: RegistryErrorCode.LOCAL_FILE_ERROR, cause, context: { filePath }, suggestion: "Check if the file exists and you have read permissions." }); this.filePath = filePath; this.name = "RegistryLocalFileError"; } }; var RegistryParseError = class extends RegistryError { constructor(item, parseError) { let message = `Failed to parse registry item: ${item}`; if (parseError instanceof z.ZodError) message = `Failed to parse registry item: ${item}\n${parseError.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n")}`; super(message, { code: RegistryErrorCode.PARSE_ERROR, cause: parseError, context: { item }, suggestion: "The registry item may be corrupted or have an invalid format. Please make sure it returns a valid JSON object. See https://shadcn-vue.com/schema/registry-item.json." }); this.item = item; this.parseError = parseError; this.name = "RegistryParseError"; } }; var RegistryMissingEnvironmentVariablesError = class extends RegistryError { constructor(registryName, missingVars) { const message = `Registry "${registryName}" requires the following environment variables:\n\n${missingVars.map((v) => ` • ${v}`).join("\n")}`; super(message, { code: RegistryErrorCode.MISSING_ENV_VARS, context: { registryName, missingVars }, suggestion: "Set the required environment variables to your .env or .env.local file." }); this.registryName = registryName; this.missingVars = missingVars; this.name = "RegistryMissingEnvironmentVariablesError"; } }; var RegistryInvalidNamespaceError = class extends RegistryError { constructor(name) { const message = `Invalid registry namespace: "${name}". Registry names must start with @ (e.g., @shadcn, @v0).`; super(message, { code: RegistryErrorCode.VALIDATION_ERROR, context: { name }, suggestion: "Use a valid registry name starting with @ or provide a direct URL to the registry." }); this.name = name; this.name = "RegistryInvalidNamespaceError"; } }; var ConfigParseError = class extends RegistryError { constructor(cwd, parseError) { let message = `Invalid components.json configuration in ${cwd}.`; if (parseError instanceof Error && parseError.message.includes("built-in registry and cannot be overridden")) message = `Invalid components.json configuration in ${highlighter.info(`${cwd}/components.json`)}:\n - ${parseError.message}`; if (parseError instanceof SyntaxError) message = `Invalid components.json configuration in ${highlighter.info(`${cwd}/components.json`)}:\n - Syntax error: ${parseError.message.replace(`${cwd}/components.json`, "")}`; if (parseError instanceof z.ZodError) message = `Invalid components.json configuration in ${highlighter.info(`${cwd}/components.json`)}:\n${parseError.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n")}`; super(message, { code: RegistryErrorCode.INVALID_CONFIG, cause: parseError, context: { cwd }, suggestion: "Check your components.json file for syntax errors or invalid configuration. Run 'npx shadcn@latest init' to regenerate a valid configuration." }); this.cwd = cwd; this.name = "ConfigParseError"; } }; var RegistriesIndexParseError = class extends RegistryError { constructor(parseError) { let message = "Failed to parse registries index"; if (parseError instanceof z.ZodError) { const invalidNamespaces = parseError.errors.filter((e) => e.path.length > 0).map((e) => `"${e.path[0]}"`).filter((v, i, arr) => arr.indexOf(v) === i); if (invalidNamespaces.length > 0) message = `Failed to parse registries index. Invalid registry namespace(s): ${invalidNamespaces.join(", ")}\n${parseError.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n")}`; else message = `Failed to parse registries index:\n${parseError.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n")}`; } super(message, { code: RegistryErrorCode.PARSE_ERROR, cause: parseError, context: { parseError }, suggestion: "The registries index may be corrupted or have invalid registry namespace format. Registry names must start with @ (e.g., @shadcn, @example)." }); this.parseError = parseError; this.name = "RegistriesIndexParseError"; } }; //#endregion //#region src/utils/resolve-import.ts function resolveImport(importPath, config) { const matcher = createPathsMatcher(config); if (matcher === null) return; return matcher(importPath)[0]; } //#endregion //#region src/utils/get-config.ts const DEFAULT_COMPONENTS = "@/components"; const DEFAULT_UTILS = "@/lib/utils"; const DEFAULT_TAILWIND_CSS = "assets/css/tailwind.css"; const DEFAULT_TAILWIND_CONFIG = "tailwind.config.js"; 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); } async function resolveConfigPaths(cwd, config) { config.registries = { ...BUILTIN_REGISTRIES, ...config.registries || {} }; const detectedFramework = await detectFrameworkConfigFiles(cwd); const isTypeScript = await isTypeScriptProject(cwd); const tsConfig = await getTsconfig(path.resolve(cwd, detectedFramework?.name === "nuxt4" ? "./.nuxt/tsconfig.app.json" : detectedFramework?.name === "nuxt3" ? "./.nuxt/tsconfig.json" : detectedFramework?.name === "inertia" ? "./inertia/tsconfig.json" : isTypeScript ? "./tsconfig.json" : "./jsconfig.json"), isTypeScript ? void 0 : "jsconfig.json"); if (tsConfig === null) throw new Error(`Failed to load ${config.typescript ? "tsconfig" : "jsconfig"}.json.`.trim()); 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"), 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 loadConfig({ name: "components", configFile: "components", cwd, dotenv: false, packageJson: false, rcFile: false, jitiOptions: { rebuildFsCache: true, moduleCache: true } }); if (!configResult.config || Object.keys(configResult.config).length === 0) return null; const config = rawConfigSchema.parse(configResult.config); if (config.registries) { for (const registryName of Object.keys(config.registries)) if (registryName in BUILTIN_REGISTRIES) throw new Error(`"${registryName}" is a built-in registry and cannot be overridden.`); } return config; } catch (error) { throw new ConfigParseError(cwd, error); } } async function getWorkspaceConfig(config) { let resolvedAliases = {}; for (const key of Object.keys(config.aliases)) { if (!isAliasKey(key, config)) continue; const resolvedPath = config.resolvedPaths[key]; const packageRoot = await findPackageRoot(config.resolvedPaths.cwd, resolvedPath); if (!packageRoot) { resolvedAliases[key] = config; continue; } resolvedAliases[key] = await getConfig(packageRoot); } const result = workspaceConfigSchema.safeParse(resolvedAliases); if (!result.success) return null; return result.data; } async function findPackageRoot(cwd, resolvedPath) { const commonRoot = findCommonRoot$1(cwd, resolvedPath); const relativePath = path.relative(commonRoot, resolvedPath); const matchingPackageRoot = (await glob("**/package.json", { cwd: commonRoot, deep: 3, ignore: [ "**/node_modules/**", "**/dist/**", "**/build/**", "**/public/**" ] })).map((pkgPath) => path.dirname(pkgPath)).find((pkgDir) => relativePath.startsWith(pkgDir)); return matchingPackageRoot ? path.join(commonRoot, matchingPackageRoot) : null; } function isAliasKey(key, config) { return Object.keys(config.resolvedPaths).filter((key$1) => key$1 !== "utils").includes(key); } function findCommonRoot$1(cwd, resolvedPath) { const parts1 = cwd.split(path.sep); const parts2 = resolvedPath.split(path.sep); const commonParts = []; for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) { if (parts1[i] !== parts2[i]) break; commonParts.push(parts1[i]); } return commonParts.join(path.sep); } async function getTargetStyleFromConfig(cwd, fallback) { return (await getProjectInfo(cwd))?.tailwindVersion === "v4" ? "new-york-v4" : fallback; } /** * Creates a config object with sensible defaults. * Useful for universal registry items that bypass framework detection. * * @param partial - Partial config values to override defaults * @returns A complete Config object */ function createConfig(partial) { const defaultConfig = { typescript: true, resolvedPaths: { cwd: process.cwd(), tailwindConfig: "", tailwindCss: "", utils: "", components: "", ui: "", lib: "", composables: "" }, style: "", tailwind: { config: "", css: "", baseColor: "", cssVariables: false }, aliases: { components: "", utils: "" }, registries: { ...BUILTIN_REGISTRIES } }; if (partial) return { ...defaultConfig, ...partial, resolvedPaths: { ...defaultConfig.resolvedPaths, ...partial.resolvedPaths || {} }, tailwind: { ...defaultConfig.tailwind, ...partial.tailwind || {} }, aliases: { ...defaultConfig.aliases, ...partial.aliases || {} }, registries: { ...defaultConfig.registries, ...partial.registries || {} } }; return defaultConfig; } //#endregion //#region src/utils/get-package-info.ts function getPackageInfo(cwd = "", shouldThrow = true) { const packageJsonPath = path.join(cwd, "package.json"); return fsExtra.readJSONSync(packageJsonPath, { throws: shouldThrow }); } //#endregion //#region src/utils/get-project-info.ts const PROJECT_SHARED_IGNORE = [ "**/node_modules/**", ".nuxt", "public", "dist", "build" ]; const TS_CONFIG_SCHEMA = z.object({ compilerOptions: z.object({ paths: z.record(z.string().or(z.array(z.string()))) }) }); async function detectFrameworkConfigFiles(cwd) { const packageInfo = await getPackageInfo(cwd, false); const configFiles = await glob("**/{nuxt,vite,astro,wxt}.config.*|composer.json", { cwd, deep: 3, ignore: PROJECT_SHARED_IGNORE }); if (configFiles.find((file) => file.startsWith("nuxt.config."))) { const nuxtPkg = packageInfo?.dependencies?.nuxt || packageInfo?.devDependencies?.nuxt; const nuxtVersion = nuxtPkg && coerce(nuxtPkg)?.version || "4.0.0"; if (nuxtVersion.startsWith("4")) return FRAMEWORKS.nuxt4; else if (nuxtVersion.startsWith("3")) return FRAMEWORKS.nuxt3; return null; } if (configFiles.find((file) => file.startsWith("astro.config."))) return FRAMEWORKS.astro; if (configFiles.find((file) => file.startsWith("composer.json"))) return FRAMEWORKS.laravel; if (packageInfo?.dependencies?.["@inertiajs/vue3"] || packageInfo?.devDependencies?.["@inertiajs/vue3"] || await fsExtra.pathExists(path.join(cwd, "resources/js"))) return FRAMEWORKS.inertia; if (configFiles.find((file) => file.startsWith("wxt.config."))) return FRAMEWORKS.vite; if (configFiles.find((file) => file.startsWith("vite.config."))) return FRAMEWORKS.vite; return null; } async function isTypeScriptProject(cwd) { return (await glob("tsconfig.*", { cwd, deep: 1, ignore: PROJECT_SHARED_IGNORE })).length > 0; } async function getProjectInfo(cwd) { const [detectedFramework, typescript, isSrcDir, tailwindConfigFile, tailwindCssFile, tailwindVersion, aliasPrefix, packageJson] = await Promise.all([ detectFrameworkConfigFiles(cwd), isTypeScriptProject(cwd), fsExtra.pathExists(path.resolve(cwd, "src")), getTailwindConfigFile(cwd), getTailwindCssFile(cwd), getTailwindVersion(cwd), getTsConfigAliasPrefix(cwd), getPackageInfo(cwd, false) ]); return { framework: detectedFramework || FRAMEWORKS.manual, typescript, isSrcDir, tailwindConfigFile, tailwindCssFile, tailwindVersion, aliasPrefix }; } async function getTailwindVersion(cwd) { const [packageInfo, config] = await Promise.all([getPackageInfo(cwd, false), getConfig(cwd)]); if (config?.tailwind?.config === "") return "v4"; const hasNuxtTailwind = !!(packageInfo?.dependencies?.["@nuxtjs/tailwindcss"] || packageInfo?.devDependencies?.["@nuxtjs/tailwindcss"]); if (!!!(packageInfo?.dependencies?.tailwindcss || packageInfo?.devDependencies?.tailwindcss) && !hasNuxtTailwind) 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; for (const file of files) { const contents = await fsExtra.readFile(path.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 detectedFramework = await detectFrameworkConfigFiles(cwd); const isTypeScript = await isTypeScriptProject(cwd); const tsConfig = await getTsconfig(cwd, detectedFramework?.name === "nuxt4" ? "./.nuxt/tsconfig.app.json" : detectedFramework?.name === "nuxt3" ? "./.nuxt/tsconfig.json" : detectedFramework?.name === "inertia" ? "./inertia/tsconfig.json" : isTypeScript ? "./tsconfig.json" : "./jsconfig.json"); if (tsConfig === null || !Object.entries(tsConfig.config.compilerOptions?.paths ?? {}).length) return null; const aliasPaths = tsConfig.config.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 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; return await resolveConfigPaths(cwd, { $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` } }); } 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; } //#endregion //#region src/utils/logger.ts const 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(""); } }; //#endregion //#region src/utils/spinner.ts function spinner(text, options) { return ora({ text, isSilent: options?.silent }); } //#endregion //#region src/registry/env.ts function expandEnvVars(value) { return value.replace(/\$\{(\w+)\}/g, (_match, key) => process.env[key] || ""); } function extractEnvVars(value) { const vars = []; const regex = /\$\{(\w+)\}/g; let match; while ((match = regex.exec(value)) !== null) vars.push(match[1]); return vars; } //#endregion //#region src/registry/parser.ts const REGISTRY_PATTERN = /^(@[a-z0-9](?:[\w-]*[a-z0-9])?)\/(.+)$/i; function parseRegistryAndItemFromString(name) { if (!name.startsWith("@")) return { registry: null, item: name }; const match = name.match(REGISTRY_PATTERN); if (match) return { registry: match[1], item: match[2] }; return { registry: null, item: name }; } //#endregion //#region src/utils/compare.ts function isContentSame(existingContent, newContent, options = {}) { const { ignoreImports = false } = options; const normalizedExisting = existingContent.replace(/\r\n/g, "\n").trim(); const normalizedNew = newContent.replace(/\r\n/g, "\n").trim(); if (normalizedExisting === normalizedNew) return true; if (!ignoreImports) return false; const importRegex = /^(import\s+(?:type\s+)?(?:\*\s+as\s+\w+|\{[^}]*\}|\w+)?(?:\s*,\s*(?:\{[^}]*\}|\w+))?\s+from\s+["'])([^"']+)(["'])/gm; const normalizeImports = (content) => { return content.replace(importRegex, (_match, prefix, importPath, suffix) => { if (importPath.startsWith(".")) return `${prefix}${importPath}${suffix}`; const parts = importPath.split("/"); return `${prefix}@normalized/${parts[parts.length - 1]}${suffix}`; }); }; return normalizeImports(normalizedExisting) === normalizeImports(normalizedNew); } //#endregion //#region src/utils/env-helpers.ts function isEnvFile(filePath) { const fileName = path.basename(filePath); return /^\.env(?:\.|$)/.test(fileName); } /** * Finds a file variant in the project. * TODO: abstract this to a more generic function. */ function findExistingEnvFile(targetDir) { for (const variant of [ ".env.local", ".env", ".env.development.local", ".env.development" ]) { const filePath = path.join(targetDir, variant); if (existsSync(filePath)) return filePath; } return null; } /** * Parse .env content into key-value pairs. */ function parseEnvContent(content) { const lines = content.split("\n"); const env = {}; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const equalIndex = trimmed.indexOf("="); if (equalIndex === -1) continue; const key = trimmed.substring(0, equalIndex).trim(); const value = trimmed.substring(equalIndex + 1).trim(); if (key) env[key] = value.replace(/^["']|["']$/g, ""); } return env; } /** * Get the list of new keys that would be added when merging env content. */ function getNewEnvKeys(existingContent, newContent) { const existingEnv = parseEnvContent(existingContent); const newEnv = parseEnvContent(newContent); const newKeys = []; for (const key of Object.keys(newEnv)) if (!(key in existingEnv)) newKeys.push(key); return newKeys; } /** * Merge env content by appending ONLY new keys that don't exist in the existing content. * Existing keys are preserved with their original values. */ function mergeEnvContent(existingContent, newContent) { const existingEnv = parseEnvContent(existingContent); const newEnv = parseEnvContent(newContent); let result = existingContent.trimEnd(); if (result && !result.endsWith("\n")) result += "\n"; const newKeys = []; for (const [key, value] of Object.entries(newEnv)) if (!(key in existingEnv)) newKeys.push(`${key}=${value}`); if (newKeys.length > 0) { if (result) result += "\n"; result += newKeys.join("\n"); return `${result}\n`; } if (result && !result.endsWith("\n")) return `${result}\n`; return result; } //#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$1) { if (path$1.parent.value.type !== "ImportDeclaration" && typeof path$1.node.value === "string") { const raw = path$1.node.value; const mapped = applyColorMapping(raw, baseColor.inlineColors).trim(); if (mapped !== raw) { path$1.node.value = mapped; transformCount++; } } return this.traverse(path$1); } }); if (sfcAST) traverseTemplateAST(sfcAST, { enterNode(node) { if (node.type === "Literal" && typeof node.value === "string") { if (!["BinaryExpression", "Property"].includes(node.parent?.type ?? "")) { const raw = node.value; const mapped = applyColorMapping(raw, baseColor.inlineColors).trim(); if (mapped !== raw) { node.value = mapped; transformCount++; } } } else if (node.type === "VLiteral" && typeof node.value === "string") { if (node.parent.key.name === "class") { const raw = node.value; const mapped = applyColorMapping(raw, baseColor.inlineColors).trim(); if (mapped !== raw) { node.value = mapped; transformCount++; } } } }, leaveNode() {} }); return transformCount; } }; } function splitClassName(className) { if (!className.includes("/") && !className.includes(":")) return [ null, className, null ]; const parts = []; const [rest, alpha] = className.split("/"); if (!rest.includes(":")) return [ null, rest, alpha ]; const split = rest.split(":"); const name = split.pop(); const variant = split.join(":"); parts.push(variant ?? null, name ?? null, alpha ?? null); return parts; } 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 = /* @__PURE__ */ new Set(); const darkMode = /* @__PURE__ */ 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 } }) { let transformCount = 0; const { config, isRemote } = opts; const utilsAlias = config.aliases?.utils; const utilsImport = `${typeof utilsAlias === "string" && utilsAlias.includes("/") ? utilsAlias.split("/")[0] : "@"}/lib/utils`; for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitLiteral(path$1) { if (typeof path$1.node.value === "string") { const parent = path$1.parent.value; if (parent.type === "ImportDeclaration" || parent.type === "CallExpression" && parent.callee?.name === "import") { const sourcePath = path$1.node.value; const updatedImport = updateImportAliases(sourcePath, config, isRemote); if (updatedImport !== sourcePath) { path$1.node.value = updatedImport; transformCount++; } if (utilsImport === updatedImport || updatedImport === "@/lib/utils") { if (parent.type === "ImportDeclaration") { if ((parent.specifiers?.map((node) => node.local?.name ?? "") ?? []).find((i) => i === "cn") && config.aliases.utils) { path$1.node.value = utilsImport === updatedImport ? updatedImport.replace(utilsImport, config.aliases.utils) : config.aliases.utils; transformCount++; } } else if (parent.type === "CallExpression") { const grandParent = path$1.parent.parent?.value; if (grandParent?.type === "VariableDeclarator" && grandParent.id?.type === "ObjectPattern") { if (grandParent.id.properties?.some((prop) => prop.key?.name === "cn") && config.aliases.utils) { path$1.node.value = utilsImport === updatedImport ? updatedImport.replace(utilsImport, config.aliases.utils) : config.aliases.utils; transformCount++; } } } } } } return this.traverse(path$1); } }); 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; const addPrefix = (input) => { const result = applyPrefix(input, config.tailwind.prefix, tailwindVersion); transformCount++; return result; }; 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 = addPrefix(arg.value); else if (arg.type === "ConditionalExpression") { if (arg.consequent?.type === "Literal" && typeof arg.consequent.value === "string") arg.consequent.value = addPrefix(arg.consequent.value); if (arg.alternate?.type === "Literal" && typeof arg.alternate.value === "string") arg.alternate.value = addPrefix(arg.alternate.value); } else if (arg.type === "BinaryExpression") { if (arg.right?.type === "Literal" && typeof arg.right.value === "string") arg.right.value = addPrefix(arg.right.value); } 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 = addPrefix(prop.value.value); } }); else astHelpers.findAll(arg, { type: "Literal" }).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 = addPrefix(literal.value); } }); }); 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$1) { if (path$1.node.callee.type === "Identifier" && path$1.node.callee.name === "cva") { const args = path$1.node.arguments; if (args[0]?.type === "Literal" && typeof args[0].value === "string") args[0].value = addPrefix(args[0].value); 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") astHelpers.findAll(variantsProperty.value, { type: "Property" }).forEach((prop) => { if (prop.value?.type === "Literal" && typeof prop.value.value === "string") prop.value.value = addPrefix(prop.value.value); else if (prop.value?.type === "ArrayExpression") prop.value.elements.forEach((element) => { if (element?.type === "Literal" && typeof element.value === "string") element.value = addPrefix(element.value); }); }); } } if (path$1.node.callee.type === "Identifier" && path$1.node.callee.name === "cn") path$1.node.arguments.forEach((arg) => { if (arg.type === "Literal" && typeof arg.value === "string") arg.value = addPrefix(arg.value); else if (arg.type === "ConditionalExpression") { if (arg.consequent?.type === "Literal" && typeof arg.consequent.value === "string") arg.consequent.value = addPrefix(arg.consequent.value); if (arg.alternate?.type === "Literal" && typeof arg.alternate.value === "string") arg.alternate.value = addPrefix(arg.alternate.value); } else if (arg.type === "BinaryExpression") { if (arg.right?.type === "Literal" && typeof arg.right.value === "string") arg.right.value = addPrefix(arg.right.value); } 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 = addPrefix(prop.value.value); } }); else astHelpers.findAll(arg, { type: "Literal" }).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 = addPrefix(literal.value); } }); }); return this.traverse(path$1); } }); 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)) node.value = `"${addPrefix(node.value.replace(/"/g, ""))}"`; } }, 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" }, tabler: { name: "@tabler/icons-vue", package: "@tabler/icons-vue", import: "@tabler/icons-vue" }, phosphor: { name: "@phosphor-icons/vue", package: "@phosphor-icons/vue", import: "@phosphor-icons/vue" } }; //#endregion //#region src/utils/transformers/transform-icons.ts const SOURCE_LIBRARY = "lucide"; const ICON_LIBRARY_IMPORTS = new Set(Object.values(ICON_LIBRARIES).map((l) => l.import).filter(Boolean)); function transformIcons(opts, registryIcons) { return { type: "codemod", name: "modify import of icon library on user config", transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) { let transformCount = 0; const { config } = opts; if (!config.iconLibrary || !(config.iconLibrary in ICON_LIBRARIES)) return transformCount; const sourceLibrary = SOURCE_LIBRARY; const targetLibrary = config.iconLibrary; if (sourceLibrary === targetLibrary) return transformCount; const targetedIconsMap = /* @__PURE__ */ new Map(); for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitImportDeclaration(path$1) { const source = String(path$1.node.source.value); if (![...ICON_LIBRARY_IMPORTS].some((prefix) => source.startsWith(prefix))) return this.traverse(path$1); let hasChanges = false; for (const specifier of path$1.node.specifiers ?? []) if (specifier.type === "ImportSpecifier") { const iconName = specifier.imported.name; const targetedIcon = registryIcons[iconName]?.[targetLibrary]; if (!targetedIcon || targetedIconsMap.has(iconName)) continue; targetedIconsMap.set(iconName, targetedIcon); specifier.imported.name = targetedIcon; hasChanges = true; } if (hasChanges) { path$1.node.source.value = ICON_LIBRARIES[targetLibrary].import; transformCount++; } return this.traverse(path$1); } }); if (sfcAST && targetedIconsMap.size > 0) 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, config, options) { if (!files?.length) return { filesCreated: [], filesUpdated: [], filesSkipped: [] }; options = { overwrite: false, force: false, silent: false, isRemote: false, isWorkspace: false, ...options }; const filesCreatedSpinner = spinner(`Updating files.`, { silent: options.silent })?.start(); const [projectInfo, baseColor] = await Promise.all([getProjectInfo(config.resolvedPaths.cwd), config.tailwind.baseColor ? getRegistryBaseColor(config.tailwind.baseColor) : Promise.resolve(void 0)]); let filesCreated = []; let filesUpdated = []; let filesSkipped = []; let envVarsAdded = []; let envFile = null; for (let index = 0; index < files.length; index++) { const file = files[index]; if (!file.content) continue; let filePath = resolveFilePath(file, config, { framework: projectInfo?.framework.name, commonRoot: findCommonRoot(files.map((f) => f.path), file.path), path: options.path, fileIndex: index }); if (!filePath) continue; basename(file.path); const targetDir = path.dirname(filePath); if (!config.typescript) filePath = filePath.replace(/\.ts?$/, (match) => ".js"); if (isEnvFile(filePath) && !existsSync(filePath)) { const alternativeEnvFile = findExistingEnvFile(targetDir); if (alternativeEnvFile) filePath = alternativeEnvFile; } const existingFile = existsSync(filePath); if (existingFile && statSync(filePath).isDirectory()) throw new Error(`Cannot write to ${filePath}: path exists and is a directory. Please provide a file path instead.`); const content = isEnvFile(filePath) ? file.content : await transform$2({ filename: file.path, raw: file.content, config, baseColor, isRemote: options.isRemote }); if (existingFile && !isEnvFile(filePath)) { if (isContentSame(await promises.readFile(filePath, "utf-8"), content, { ignoreImports: options.isWorkspace })) { filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath)); continue; } } if (existingFile && !options.overwrite && !isEnvFile(filePath)) { filesCreatedSpinner.stop(); if (options.rootSpinner) options.rootSpinner.stop(); const { overwrite } = await prompts({ type: "confirm", name: "overwrite", message: `The file ${highlighter.info(path.relative(config.resolvedPaths.ui, filePath))} already exists. Would you like to overwrite?`, initial: false }); if (!overwrite) { filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath)); if (options.rootSpinner) options.rootSpinner.start(); continue; } filesCreatedSpinner?.start(); if (options.rootSpinner) options.rootSpinner.start(); } if (!existsSync(targetDir)) await promises.mkdir(targetDir, { recursive: true }); if (isEnvFile(filePath) && existingFile) { const existingFileContent = await promises.readFile(filePath, "utf-8"); const mergedContent = mergeEnvContent(existingFileContent, content); envVarsAdded = getNewEnvKeys(existingFileContent, content); envFile = path.relative(config.resolvedPaths.cwd, filePath); if (!envVarsAdded.length) { filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath)); continue; } await promises.writeFile(filePath, mergedContent, "utf-8"); filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath)); continue; } await promises.writeFile(filePath, content, "utf-8"); if (!existingFile) { filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath)); if (isEnvFile(filePath)) { envVarsAdded = Object.keys(parseEnvContent(content)); envFile = path.relative(config.resolvedPaths.cwd, filePath); } } else filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath)); } const updatedFiles = await resolveImports([ ...filesCreated, ...filesUpdated, ...filesSkipped ], config); filesUpdated.push(...updatedFiles); filesUpdated = filesUpdated.filter((file) => !filesCreated.includes(file)); if (!(filesCreated.length || filesUpdated.length) && !filesSkipped.length) filesCreatedSpinner?.info("No files updated."); filesCreated = Array.from(new Set(filesCreated)); filesUpdated = Array.from(new Set(filesUpdated)); filesSkipped = Array.from(new