UNPKG

shadcn-vue

Version:
1,334 lines 130 kB
import { a as BASES, d as ICON_LIBRARIES, f as REGISTRY_URL, l as FALLBACK_STYLE, m as STYLES, n as transformIcons, o as BASE_COLORS, p as SHADCN_VUE_URL, s as BUILTIN_REGISTRIES, t as transformMenu, u as FONTS } from "./transform-menu-CLNZ5vUh.js"; import { configSchema, iconsSchema, rawConfigSchema, registriesIndexSchema, registryBaseColorSchema, registryConfigSchema, registryIndexSchema, registryItemFileSchema, registryItemSchema, registryResolvedItemsTreeSchema, registrySchema, searchResultsSchema, stylesSchema, workspaceConfigSchema } from "./schema/index.js"; import path, { basename } from "pathe"; import prompts from "prompts"; import { z as z$1 } from "zod"; import fs, { existsSync, promises, statSync } from "fs"; import deepmerge from "deepmerge"; import fs$1 from "fs-extra"; import { createPathsMatcher, getTsconfig } from "get-tsconfig"; import { coerce } from "semver"; import { glob } from "tinyglobby"; import open from "open"; import { loadConfig } from "c12"; import { colors } from "consola/utils"; import { homedir, tmpdir } from "os"; import { Project, QuoteKind, ScriptKind, SyntaxKind } from "ts-morph"; import { transform } from "vue-metamorph"; import consola from "consola"; import ora from "ora"; import { transform as transform$1 } from "@unovue/detypes"; import { createHash } from "crypto"; import { ofetch } from "ofetch"; import { ProxyAgent } from "undici"; import objectToString from "stringify-object"; import path$1 from "path"; import { addDependency, addDevDependency, detectPackageManager } from "nypm"; import fuzzysort from "fuzzysort"; //#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$1.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$1.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$1.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; const paths = matcher(importPath); if (paths[0]) return paths[0]; const tsconfigPaths = config.config.compilerOptions?.paths ?? {}; const baseUrl = config.config.compilerOptions?.baseUrl ?? "."; const configDir = path.dirname(config.path); const resolvedBaseUrl = path.resolve(configDir, baseUrl); for (const [pattern, mappings] of Object.entries(tsconfigPaths)) if (pattern.replace(/\/\*$/, "") === importPath && Array.isArray(mappings) && mappings.length > 0) { const mapping = String(mappings[0]).replace(/\/\*$/, ""); return path.resolve(resolvedBaseUrl, mapping); } } //#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, await getFrameworkTsConfigPath(cwd, detectedFramework, isTypeScript)), 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, ".."), hooks: config.aliases.hooks ? await resolveImport(config.aliases.hooks, tsConfig) : path.resolve(await resolveImport(config.aliases.components, tsConfig) ?? cwd, "..", "hooks"), 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) => key !== "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) { if ((await getProjectInfo(cwd))?.tailwindVersion === "v4" && fallback === "new-york") return "new-york-v4"; return 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: "", hooks: "", composables: "" }, style: "", font: "inter", 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/registry/config.ts const VISUAL_STYLES = /* @__PURE__ */ new Set([ "vega", "nova", "maia", "lyra", "mira", "luma", "sera" ]); /** * Composes a base + visual style choice into the canonical style identifier * stored in `components.json`. For visual styles, returns `${base}-${style}` * (e.g. `"reka-luma"`). For non-visual styles like `"new-york-v4"`, returns * the style unchanged. */ function composeStyleId(base, style) { if (!style) return FALLBACK_STYLE; if (style.includes("-") || !VISUAL_STYLES.has(style)) return style; return `${base || "reka"}-${style}`; } /** * Resolves the registry style segment used in fetch URLs like * `styles/{registryStyle}/<comp>.json`. * * The `style` field in `components.json` is the **full** style identifier * (e.g. `"reka-luma"`, `"new-york-v4"`), matching shadcn-ui's model. We just * pass it through, falling back to `FALLBACK_STYLE` when unset. */ function resolveRegistryStyle(style) { return style || "new-york-v4"; } function resolveStyleFromConfig(config) { if (!config.style) return FALLBACK_STYLE; if (config.style === "new-york" && config.tailwind?.config === "") return FALLBACK_STYLE; return config.style; } function configWithDefaults(config) { const baseConfig = createConfig({ style: FALLBACK_STYLE, registries: BUILTIN_REGISTRIES }); if (!config) return baseConfig; return configSchema.parse(deepmerge(baseConfig, { ...config, style: resolveStyleFromConfig(config), registries: { ...BUILTIN_REGISTRIES, ...config.registries } })); } //#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/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/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) { if (path.parent.value.type !== "ImportDeclaration" && typeof path.node.value === "string") { const raw = path.node.value; const mapped = applyColorMapping(raw, baseColor.inlineColors).trim(); if (mapped !== raw) { path.node.value = mapped; transformCount++; } } return this.traverse(path); } }); 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) => value?.startsWith(prefix)); 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) { if (typeof path.node.value === "string") { const parent = path.parent.value; if (parent.type === "ImportDeclaration" || parent.type === "CallExpression" && parent.callee?.name === "import") { const sourcePath = path.node.value; const updatedImport = updateImportAliases(sourcePath, config, isRemote); if (updatedImport !== sourcePath) { path.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.node.value = utilsImport === updatedImport ? updatedImport.replace(utilsImport, config.aliases.utils) : config.aliases.utils; transformCount++; } } else if (parent.type === "CallExpression") { const grandParent = path.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.node.value = utilsImport === updatedImport ? updatedImport.replace(utilsImport, config.aliases.utils) : config.aliases.utils; transformCount++; } } } } } } return this.traverse(path); } }); 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.match(/^@\/styles\//)) moduleSpecifier = moduleSpecifier.replace(/^@\/styles\//, "@/registry/"); 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-rtl.ts const RTL_MAPPINGS = [ ["-ml-", "-ms-"], ["-mr-", "-me-"], ["ml-", "ms-"], ["mr-", "me-"], ["pl-", "ps-"], ["pr-", "pe-"], ["-left-", "-start-"], ["-right-", "-end-"], ["left-", "start-"], ["right-", "end-"], ["inset-l-", "inset-inline-start-"], ["inset-r-", "inset-inline-end-"], ["rounded-tl-", "rounded-ss-"], ["rounded-tr-", "rounded-se-"], ["rounded-bl-", "rounded-es-"], ["rounded-br-", "rounded-ee-"], ["rounded-l-", "rounded-s-"], ["rounded-r-", "rounded-e-"], ["border-l-", "border-s-"], ["border-r-", "border-e-"], ["border-l", "border-s"], ["border-r", "border-e"], ["text-left", "text-start"], ["text-right", "text-end"], ["scroll-ml-", "scroll-ms-"], ["scroll-mr-", "scroll-me-"], ["scroll-pl-", "scroll-ps-"], ["scroll-pr-", "scroll-pe-"], ["float-left", "float-start"], ["float-right", "float-end"], ["clear-left", "clear-start"], ["clear-right", "clear-end"], ["origin-top-left", "origin-top-start"], ["origin-top-right", "origin-top-end"], ["origin-bottom-left", "origin-bottom-start"], ["origin-bottom-right", "origin-bottom-end"], ["origin-left", "origin-start"], ["origin-right", "origin-end"] ]; const RTL_TRANSLATE_X_MAPPINGS = [["-translate-x-", "translate-x-"], ["translate-x-", "-translate-x-"]]; const RTL_REVERSE_MAPPINGS = [["space-x-", "space-x-reverse"], ["divide-x-", "divide-x-reverse"]]; const RTL_SWAP_MAPPINGS = [["cursor-w-resize", "cursor-e-resize"], ["cursor-e-resize", "cursor-w-resize"]]; const RTL_LOGICAL_SIDE_SLIDE_MAPPINGS = [ [ "data-[side=inline-start]", "slide-in-from-right", "slide-in-from-end" ], [ "data-[side=inline-start]", "slide-out-to-right", "slide-out-to-end" ], [ "data-[side=inline-end]", "slide-in-from-left", "slide-in-from-start" ], [ "data-[side=inline-end]", "slide-out-to-left", "slide-out-to-start" ] ]; const RTL_FLIP_MARKER = "cn-rtl-flip"; const POSITIONING_PREFIXES = [ "-left-", "-right-", "left-", "right-" ]; function applyRtlMapping(input) { return input.split(" ").flatMap((className) => { if (className.startsWith("rtl:") || className.startsWith("ltr:")) return [className]; if (className === RTL_FLIP_MARKER) return ["rtl:rotate-180"]; const [variant, value, modifier] = splitClassName(className); if (!value) return [className]; for (const [physical, rtlPhysical] of RTL_TRANSLATE_X_MAPPINGS) if (value.startsWith(physical)) { const rtlValue = value.replace(physical, rtlPhysical); return [className, variant ? `rtl:${variant}:${rtlValue}${modifier ? `/${modifier}` : ""}` : `rtl:${rtlValue}${modifier ? `/${modifier}` : ""}`]; } for (const [prefix, reverseClass] of RTL_REVERSE_MAPPINGS) if (value.startsWith(prefix)) return [className, variant ? `rtl:${variant}:${reverseClass}` : `rtl:${reverseClass}`]; for (const [physical, swapped] of RTL_SWAP_MAPPINGS) if (value === physical) return [className, variant ? `rtl:${variant}:${swapped}` : `rtl:${swapped}`]; for (const [variantPattern, physical, logical] of RTL_LOGICAL_SIDE_SLIDE_MAPPINGS) if (variant?.includes(variantPattern) && value.startsWith(physical)) { const mappedValue = value.replace(physical, logical); return [modifier ? `${variant}:${mappedValue}/${modifier}` : `${variant}:${mappedValue}`]; } const isPhysicalSideVariant = variant?.includes("data-[side=left]") || variant?.includes("data-[side=right]"); let mappedValue = value; for (const [physical, logical] of RTL_MAPPINGS) { if (isPhysicalSideVariant && POSITIONING_PREFIXES.some((p) => physical.startsWith(p))) continue; if (value.startsWith(physical)) { if (!physical.endsWith("-") && value !== physical) continue; mappedValue = value.replace(physical, logical); break; } } let result; if (variant) result = modifier ? `${variant}:${mappedValue}/${modifier}` : `${variant}:${mappedValue}`; else result = modifier ? `${mappedValue}/${modifier}` : mappedValue; return [result]; }).join(" "); } const CLASS_ATTR_NAMES = /* @__PURE__ */ new Set(["class", "className"]); function transformRtl(opts) { return { type: "codemod", name: "transform physical tailwind classes to logical rtl classes", transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) { let transformCount = 0; const { config } = opts; if (!config.rtl) return transformCount; for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitLiteral(path) { if (path.parent.value.type !== "ImportDeclaration" && typeof path.node.value === "string") { const raw = path.node.value; const mapped = applyRtlMapping(raw); if (mapped !== raw) { path.node.value = mapped; transformCount++; } } return this.traverse(path); } }); if (sfcAST) { const isInsideClassAttribute = (node) => { let current = node?.parent; while (current) { if (current.type === "VAttribute") { if (current.key?.type === "VIdentifier" && typeof current.key.name === "string" && CLASS_ATTR_NAMES.has(current.key.name)) return true; if (current.key?.type === "VDirectiveKey" && current.key.argument?.type === "VIdentifier" && CLASS_ATTR_NAMES.has(current.key.argument.name)) return true; return false; } current = current.parent; } return false; }; traverseTemplateAST(sfcAST, { enterNode(node) { if (node.type === "VLiteral" && typeof node.value === "string") { if (node.parent?.type === "VAttribute" && node.parent.key?.type === "VIdentifier" && CLASS_ATTR_NAMES.has(node.parent.key.name)) { const cleanValue = node.value.replace(/"/g, ""); const mapped = applyRtlMapping(cleanValue); if (mapped !== cleanValue) { node.value = mapped; transformCount++; } } } else if (node.type === "Literal" && typeof node.value === "string") { if (isInsideClassAttribute(node)) { const raw = node.value; const mapped = applyRtlMapping(raw); if (mapped !== raw) { node.value = mapped; transformCount++; } } } }, leaveNode() {} }); } return transformCount; } }; } //#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-style.ts const STYLE_CLASS_MAPPINGS = { nova: { "p-6": "p-4", "p-8": "p-6", "px-6": "px-4", "px-8": "px-6", "py-6": "py-4", "py-8": "py-6", "m-6": "m-4", "m-8": "m-6", "gap-6": "gap-4", "gap-8": "gap-6", "space-y-6": "space-y-4", "space-y-8": "space-y-6", "space-x-6": "space-x-4", "space-x-8": "space-x-6" }, maia: { "rounded-md": "rounded-xl", "rounded-lg": "rounded-2xl", "rounded-sm": "rounded-md", "p-4": "p-6", "p-3": "p-4", "px-4": "px-6", "py-4": "py-6", "gap-4": "gap-6", "gap-3": "gap-4" }, lyra: { "rounded-md": "rounded-none", "rounded-lg": "rounded-none", "rounded-xl": "rounded-none", "rounded-2xl": "rounded-none", "rounded-sm": "rounded-none", "rounded-full": "rounded-none" }, mira: { "p-4": "p-2", "p-6": "p-4", "p-8": "p-5", "px-4": "px-2", "px-6": "px-4", "py-4": "py-2", "py-6": "py-4", "m-4": "m-2", "m-6": "m-4", "gap-4": "gap-2", "gap-6": "gap-4", "space-y-4": "space-y-2", "space-y-6": "space-y-4", "space-x-4": "space-x-2", "space-x-6": "space-x-4", "text-base": "text-sm", "text-lg": "text-base", "text-xl": "text-lg", "h-10": "h-8", "h-12": "h-10", "w-10": "w-8", "w-12": "w-10" }, luma: { "rounded-sm": "rounded-xl", "rounded-md": "rounded-2xl", "rounded-lg": "rounded-3xl", "rounded-xl": "rounded-3xl" }, sera: { "rounded-sm": "rounded-none", "rounded-md": "rounded-none", "rounded-lg": "rounded-none", "rounded-xl": "rounded-none", "rounded-2xl": "rounded-none", "rounded-3xl": "rounded-none" } }; /** * Apply class mappings to a string of CSS classes. * Uses word boundary matching to avoid partial replacements. */ function applyClassMappings(value, classMapping) { let result = value; for (const [from, to] of Object.entries(classMapping)) { const regex = new RegExp(`\\b${from}\\b`, "g"); result = result.replace(regex, to); } return result; } /** * Transform component classes based on the selected visual style. * Handles: * - Static class attributes in templates (class="...") * - Dynamic class bindings in templates (:class="cn(...)") * - String literals in script (CVA variants, inline classes) * * Vega is the default style, so no transformations are applied. */ function transformStyle(opts) { return { type: "codemod", name: "transform-style", transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) { let transformCount = 0; const { config } = opts; const style = config.style?.split("-")[0] || "vega"; if (style === "vega" || style === "new") return transformCount; const classMapping = STYLE_CLASS_MAPPINGS[style]; if (!classMapping) return transformCount; for (const scriptAST of scriptASTs) traverseScriptAST(scriptAST, { visitLiteral(path) { if (path.parent.value.type === "ImportDeclaration") return this.traverse(path); if (typeof path.node.value === "string") { const originalValue = path.node.value; const newValue = applyClassMappings(originalValue, classMapping); if (newValue !== originalValue) { path.node.value = newValue; transformCount++; } } return this.traverse(path); }, visitTemplateLiteral(path) { for (const quasi of path.node.quasis) if (quasi.value.raw) { const originalValue = quasi.value.raw; const newValue = applyClassMappings(originalValue, classMapping); if (newValue !== originalValue) { quasi.value.raw = newValue; quasi.value.cooked = newValue; transformCount++; } } return this.traverse(path); } }); if (sfcAST) traverseTemplateAST(sfcAST, { enterNode(node) { if (node.type !== "VElement" || !node.startTag?.attributes) return; for (const attr of node.startTag.attributes) { if (attr.type === "VAttribute" && attr.key.type === "VIdentifier" && attr.key.name === "class" && attr.value && "value" in attr.value && typeof attr.value.value === "string") { const originalValue = attr.value.value; const newValue = applyClassMappings(originalValue, classMapping); if (newValue !== originalValue) { attr.value.value = newValue; transformCount++; } } if (attr.type === "VAttribute" && attr.key.type === "VDirectiveKey" && attr.key.argument?.type === "VIdentifier" && attr.key.argument.name === "class" && attr.value?.type === "VExpressionContainer" && attr.value.expression) transformExpression(attr.value.expression, classMapping, (count) => { transformCount += count; }); } } }); return transformCount; } }; } /** * Recursively traverse a Vue expression AST node to find and transform * string literals that contain CSS classes. */ function transformExpression(node, classMapping, onTransform) { if (!node) return; if (node.type === "Literal" && typeof node.value === "string") { const originalValue = node.value; const newValue = applyClassMappings(originalValue, classMapping); if (newValue !== originalValue) { node.value = newValue; if (node.raw) { const quote = node.raw[0]; node.raw = `${quote}${newValue}${quote}`; } onTransform(1); } return; } if (node.type === "TemplateLiteral" && node.quasis) { for (const quasi of node.quasis) if (quasi.value?.raw) { const originalValue = quasi.value.raw; const newValue = applyClassMappings(originalValue, classMapping); if (newValue !== originalValue) { quasi.value.raw = newValue; quasi.value.cooked = newValue; onTransform(1); } } if (node.expressions) for (const expr of node.expressions) transformExpression(expr, classMapping, onTransform); return; } if (node.type === "CallExpression" && node.arguments) { for (const arg of node.arguments) transformExpression(arg, classMapping, onTransform); return; } if (node.type === "ArrayExpression" && node.elements) { for (const element of node.elements) if (element) transformExpression(element, classMapping, onTransform); return; } if (node.type === "ConditionalExpression") { transformExpression(node.consequent, classMapping, onTransform); transformExpression(node.alternate, classMapping, onTransform); return; } if (node.type === "LogicalExpression") { transformExpression(node.left, classMapping, onTransform); transformExpression(node.right, classMapping, onTransform); return; } if (node.type === "ObjectExpression" && node.properties) { for (const prop of node.properties) if (prop.key) transformExpression(prop.key, classMapping, onTransform); } } //#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) { if (path.node.callee.type === "Identifier" && path.node.callee.name === "cva") { const args = path.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.node.callee.type === "Identifier" && path.node.callee.name === "cn") path.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); } }); if (sfcAST) traverseTemplateAST(sfcAST, { enterNode(node) { if (node.type === "VAttribute" && node.key.type === "VDirectiveKey") { if (node.key.argument?.type === "VIdentifier") { const argName = node.key.argument.name; if ([ "class", "className", "classes", "classNames" ].includes(argName)) { if (node.value?.type === "VExpressionContainer" && node.value.expression) traverseExpression(node.value.expression); } } } else if (node.type === "VLiteral" && typeof node.value === "string") { if (node.parent?.type === "VAttribute" && node.parent.key?.type === "VIdentifier" && [ "class", "className", "classes", "classNames" ].includes(node.parent.key.name)) { const cleanValue = node.value.replace(/"/g, ""); node.value = addPrefix(cleanValue); } } }, 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 :