From 6f11e820b57235f805d255bc951eee85d9891c38 Mon Sep 17 00:00:00 2001 From: shadcn Date: Sun, 15 Feb 2026 20:28:01 +0400 Subject: [PATCH] wip --- packages/shadcn/src/commands/add.ts | 41 ++++++-- packages/shadcn/src/commands/init.ts | 138 +++------------------------ packages/shadcn/src/utils/presets.ts | 116 ++++++++++++++++++++++ 3 files changed, 165 insertions(+), 130 deletions(-) diff --git a/packages/shadcn/src/commands/add.ts b/packages/shadcn/src/commands/add.ts index 4df5fe1ed6..4b41034651 100644 --- a/packages/shadcn/src/commands/add.ts +++ b/packages/shadcn/src/commands/add.ts @@ -1,5 +1,8 @@ import path from "path" -import { runInit } from "@/src/commands/init" +import { + getTemplateFromFrameworkName, + runInit, +} from "@/src/commands/init" import { preFlightAdd } from "@/src/preflights/preflight-add" import { getRegistryItems, getShadcnRegistryIndex } from "@/src/registry/api" import { DEPRECATED_COMPONENTS } from "@/src/registry/constants" @@ -13,6 +16,10 @@ import * as ERRORS from "@/src/utils/errors" import { createConfig, getConfig } from "@/src/utils/get-config" import { getProjectInfo } from "@/src/utils/get-project-info" import { handleError } from "@/src/utils/handle-error" +import { + promptForPreset, + resolveRegistryBaseConfig, +} from "@/src/utils/presets" import { highlighter } from "@/src/utils/highlighter" import { logger } from "@/src/utils/logger" import { ensureRegistriesInConfig } from "@/src/utils/registries" @@ -52,8 +59,8 @@ export const add = new Command() try { const options = addOptionsSchema.parse({ components, - cwd: path.resolve(opts.cwd), ...opts, + cwd: path.resolve(opts.cwd), }) await loadEnvFiles(options.cwd) @@ -159,6 +166,21 @@ export const add = new Command() process.exit(1) } + // Infer template from project framework. + const inferredTemplate = getTemplateFromFrameworkName( + projectInfo?.framework.name + ) + + // Prompt for preset. + const initUrl = await promptForPreset({ + rtl: false, + template: inferredTemplate, + }) + + // Resolve registry:base config. + const { registryBaseConfig, installStyleIndex } = + await resolveRegistryBaseConfig(initUrl, options.cwd) + config = await runInit({ cwd: options.cwd, yes: true, @@ -169,8 +191,9 @@ export const add = new Command() isNewProject: false, cssVariables: options.cssVariables, rtl: false, - installStyleIndex: shouldInstallStyleIndex, - components: options.components, + installStyleIndex, + components: [initUrl, ...(options.components ?? [])], + registryBaseConfig, }) initHasRun = true } @@ -193,6 +216,11 @@ export const add = new Command() options.cwd = path.resolve(options.cwd, "apps/web") config = await getConfig(options.cwd) } else { + // Prompt for preset. + const initUrl = await promptForPreset({ rtl: false, template }) + const { registryBaseConfig, installStyleIndex } = + await resolveRegistryBaseConfig(initUrl, options.cwd) + config = await runInit({ cwd: options.cwd, yes: true, @@ -203,8 +231,9 @@ export const add = new Command() isNewProject: true, cssVariables: options.cssVariables, rtl: false, - installStyleIndex: shouldInstallStyleIndex, - components: options.components, + installStyleIndex, + components: [initUrl, ...(options.components ?? [])], + registryBaseConfig, }) initHasRun = true diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index 7ca16e594b..17a111968f 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -3,11 +3,8 @@ import path from "path" import { preFlightInit } from "@/src/preflights/preflight-init" import { getRegistryBaseColors, - getRegistryItems, getRegistryStyles, } from "@/src/registry/api" -import { buildUrlAndHeadersForRegistryItem } from "@/src/registry/builder" -import { configWithDefaults } from "@/src/registry/config" import { BUILTIN_REGISTRIES } from "@/src/registry/constants" import { clearRegistryContext } from "@/src/registry/context" import { isUrl } from "@/src/registry/utils" @@ -43,15 +40,15 @@ import { highlighter } from "@/src/utils/highlighter" import { logger } from "@/src/utils/logger" import { DEFAULT_PRESETS, - resolveCreateUrl, + promptForPreset, resolveInitUrl, + resolveRegistryBaseConfig, } from "@/src/utils/presets" import { ensureRegistriesInConfig } from "@/src/utils/registries" import { spinner } from "@/src/utils/spinner" import { Command } from "commander" import deepmerge from "deepmerge" import fsExtra from "fs-extra" -import open from "open" import prompts from "prompts" import { z } from "zod" @@ -212,68 +209,9 @@ export const init = new Command() const presetArg = options.preset === true ? true : options.preset if (presetArg === true) { - const { selectedPreset } = await prompts({ - type: "select", - name: "selectedPreset", - message: `Which ${highlighter.info( - "preset" - )} would you like to use?`, - choices: [ - ...presets.map((preset) => ({ - title: preset.title, - description: preset.description, - value: preset.name, - })), - { - title: "Custom", - description: "Build your own on https://ui.shadcn.com", - value: "custom", - }, - ], - }) - - if (!selectedPreset) { - process.exit(0) - } - - if (selectedPreset === "custom") { - const createUrl = resolveCreateUrl({ - command: "init", - rtl: options.rtl, - ...(options.template && { template: options.template }), - }) - logger.break() - logger.log( - ` Build your custom preset on ${highlighter.info(createUrl)}` - ) - logger.log( - ` Then ${highlighter.info( - "copy and run the command" - )} from ui.shadcn.com.` - ) - logger.break() - - const { proceed } = await prompts({ - type: "confirm", - name: "proceed", - message: "Open in browser?", - initial: true, - }) - - if (proceed) { - await open(createUrl) - } - - process.exit(0) - } - - const preset = presets.find((p) => p.name === selectedPreset) - if (!preset) { - process.exit(0) - } - const initUrl = resolveInitUrl({ - ...preset, + const initUrl = await promptForPreset({ rtl: options.rtl, + template: options.template, }) components = [initUrl, ...components] } @@ -310,41 +248,14 @@ export const init = new Command() // We need to check if we're initializing with a new style. // This will allow us to determine if we need to install the base style. if (components.length > 0) { - // We don't know the full config at this point. - // So we'll use a shadow config to fetch the first item. - let shadowConfig = configWithDefaults( - createConfig({ - resolvedPaths: { - cwd: parsedOptions.cwd, - }, - }) - ) - - // Check if there's a components.json file. - // If so, we'll merge with our shadow config. + // Back up existing components.json if it exists. + // Since components.json might not be valid at this point, + // temporarily rename it to allow preflight to run. const componentsJsonPath = path.resolve( parsedOptions.cwd, "components.json" ) if (fsExtra.existsSync(componentsJsonPath)) { - const existingConfig = await fsExtra.readJson(componentsJsonPath) - const config = rawConfigSchema.partial().parse(existingConfig) - const baseConfig = createConfig({ - resolvedPaths: { - cwd: parsedOptions.cwd, - }, - }) - shadowConfig = configWithDefaults({ - ...config, - resolvedPaths: { - ...baseConfig.resolvedPaths, - cwd: parsedOptions.cwd, - }, - }) - - // Since components.json might not be valid at this point. - // Temporarily rename components.json to allow preflight to run. - // We'll rename it back after preflight. componentsJsonBackupPath = createFileBackup(componentsJsonPath) ?? undefined if (!componentsJsonBackupPath) { @@ -354,37 +265,16 @@ export const init = new Command() } } - // Ensure all registries used in components are configured. - const { config: updatedConfig } = await ensureRegistriesInConfig( - components, - shadowConfig, - { - silent: true, - writeFile: false, - } - ) - shadowConfig = updatedConfig + // Resolve registry:base config from the first component. + const { registryBaseConfig, installStyleIndex } = + await resolveRegistryBaseConfig(components[0], parsedOptions.cwd) - // This forces a shadowConfig validation early in the process. - buildUrlAndHeadersForRegistryItem(components[0], shadowConfig) - - const [item] = await getRegistryItems([components[0]], { - config: shadowConfig, - }) - - if (item?.extends === "none") { + if (!installStyleIndex) { parsedOptions.installStyleIndex = false } - if (item?.type === "registry:base") { - if (item.config) { - // Merge config values into shadowConfig. - shadowConfig = configWithDefaults( - deepmerge(shadowConfig, item.config) - ) - // Store config to be merged into components.json later. - parsedOptions.registryBaseConfig = item.config - } + if (registryBaseConfig) { + parsedOptions.registryBaseConfig = registryBaseConfig } } @@ -730,7 +620,7 @@ async function promptForMinimalConfig( }) } -function getTemplateFromFrameworkName(frameworkName?: string) { +export function getTemplateFromFrameworkName(frameworkName?: string) { if (frameworkName === "next-app" || frameworkName === "next-pages") { return "next" } diff --git a/packages/shadcn/src/utils/presets.ts b/packages/shadcn/src/utils/presets.ts index 66303627e4..5986f7fa2a 100644 --- a/packages/shadcn/src/utils/presets.ts +++ b/packages/shadcn/src/utils/presets.ts @@ -1,5 +1,14 @@ +import { getRegistryItems } from "@/src/registry/api" +import { buildUrlAndHeadersForRegistryItem } from "@/src/registry/builder" +import { configWithDefaults } from "@/src/registry/config" import { REGISTRY_URL } from "@/src/registry/constants" import { type Preset } from "@/src/schema" +import { createConfig } from "@/src/utils/get-config" +import { highlighter } from "@/src/utils/highlighter" +import { logger } from "@/src/utils/logger" +import { ensureRegistriesInConfig } from "@/src/utils/registries" +import open from "open" +import prompts from "prompts" const SHADCN_URL = REGISTRY_URL.replace(/\/r\/?$/, "") @@ -79,3 +88,110 @@ export function resolveInitUrl( return `${SHADCN_URL}/init?${params.toString()}` } + +export async function promptForPreset(options: { + rtl: boolean + template?: string +}) { + const presets = Object.values(DEFAULT_PRESETS) + + const { selectedPreset } = await prompts({ + type: "select", + name: "selectedPreset", + message: `Which ${highlighter.info("preset")} would you like to use?`, + choices: [ + ...presets.map((preset) => ({ + title: preset.title, + description: preset.description, + value: preset.name, + })), + { + title: "Custom", + description: "Build your own on https://ui.shadcn.com", + value: "custom", + }, + ], + }) + + if (!selectedPreset) { + process.exit(0) + } + + if (selectedPreset === "custom") { + const createUrl = resolveCreateUrl({ + command: "init", + rtl: options.rtl, + ...(options.template && { template: options.template }), + }) + logger.break() + logger.log( + ` Build your custom preset on ${highlighter.info(createUrl)}` + ) + logger.log( + ` Then ${highlighter.info( + "copy and run the command" + )} from ui.shadcn.com.` + ) + logger.break() + + const { proceed } = await prompts({ + type: "confirm", + name: "proceed", + message: "Open in browser?", + initial: true, + }) + + if (proceed) { + await open(createUrl) + } + + process.exit(0) + } + + const preset = presets.find((p) => p.name === selectedPreset) + if (!preset) { + process.exit(0) + } + + return resolveInitUrl({ ...preset, rtl: options.rtl }) +} + +export async function resolveRegistryBaseConfig( + initUrl: string, + cwd: string +) { + // Use a shadow config to fetch the registry:base item. + let shadowConfig = configWithDefaults( + createConfig({ + resolvedPaths: { + cwd, + }, + }) + ) + + // Ensure all registries used in the init URL are configured. + const { config: updatedConfig } = await ensureRegistriesInConfig( + [initUrl], + shadowConfig, + { + silent: true, + writeFile: false, + } + ) + shadowConfig = updatedConfig + + // This forces a shadowConfig validation early in the process. + buildUrlAndHeadersForRegistryItem(initUrl, shadowConfig) + + const [item] = await getRegistryItems([initUrl], { + config: shadowConfig, + }) + + return { + registryBaseConfig: + item?.type === "registry:base" && item.config + ? item.config + : undefined, + installStyleIndex: item?.extends !== "none", + } +}