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