shadcn-vue
Version:
Add components to your apps.
1,334 lines • 130 kB
JavaScript
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 :