This commit is contained in:
shadcn
2026-02-15 20:28:01 +04:00
parent e9af9efaf3
commit 6f11e820b5
3 changed files with 165 additions and 130 deletions

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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",
}
}