This commit is contained in:
shadcn
2026-03-16 14:58:14 +04:00
parent 36d0b07a0c
commit 04cbfb73ad
2 changed files with 258 additions and 7 deletions

View File

@@ -9,7 +9,7 @@ import {
resolveInitUrl,
resolveRegistryBaseConfig,
} from "@/src/preset/presets"
import { getRegistryStyles } from "@/src/registry/api"
import { getRegistryBaseColors, getRegistryStyles } from "@/src/registry/api"
import { BUILTIN_REGISTRIES, SHADCN_URL } from "@/src/registry/constants"
import { clearRegistryContext } from "@/src/registry/context"
import { registryConfigSchema } from "@/src/registry/schema"
@@ -31,7 +31,12 @@ import {
restoreFileBackup,
} from "@/src/utils/file-helper"
import {
DEFAULT_COMPONENTS,
DEFAULT_TAILWIND_CONFIG,
DEFAULT_TAILWIND_CSS,
DEFAULT_UTILS,
explorer,
getConfig,
getWorkspaceConfig,
resolveConfigPaths,
type Config,
@@ -639,11 +644,9 @@ export async function runInit(
}
// Standard init path for existing projects.
if (!projectConfig) {
throw new Error("Project config is required for standard init.")
}
let config = await promptForMinimalConfig(projectConfig, options)
let config = projectConfig
? await promptForMinimalConfig(projectConfig, options)
: await promptForConfig(await getConfig(options.cwd))
if (!options.yes) {
const { proceed } = await prompts({
@@ -778,6 +781,205 @@ export async function runInit(
return fullConfig
}
async function promptForConfig(defaultConfig: Config | null = null) {
const [styles, baseColors] = await Promise.all([
getRegistryStyles(),
getRegistryBaseColors(),
])
logger.info("")
const options = await prompts([
{
type: "toggle",
name: "typescript",
message: `Would you like to use ${highlighter.info(
"TypeScript"
)} (recommended)?`,
initial: defaultConfig?.tsx ?? true,
active: "yes",
inactive: "no",
},
{
type: "select",
name: "style",
message: `Which ${highlighter.info("style")} would you like to use?`,
choices: styles.map((style) => ({
title: style.label,
value: style.name,
})),
},
{
type: "select",
name: "tailwindBaseColor",
message: `Which color would you like to use as the ${highlighter.info(
"base color"
)}?`,
choices: baseColors.map((color) => ({
title: color.label,
value: color.name,
})),
},
{
type: "text",
name: "tailwindCss",
message: `Where is your ${highlighter.info("global CSS")} file?`,
initial: defaultConfig?.tailwind.css ?? DEFAULT_TAILWIND_CSS,
},
{
type: "toggle",
name: "tailwindCssVariables",
message: `Would you like to use ${highlighter.info(
"CSS variables"
)} for theming?`,
initial: defaultConfig?.tailwind.cssVariables ?? true,
active: "yes",
inactive: "no",
},
{
type: "text",
name: "tailwindPrefix",
message: `Are you using a custom ${highlighter.info(
"tailwind prefix eg. tw-"
)}? (Leave blank if not)`,
initial: "",
},
{
type: "text",
name: "tailwindConfig",
message: `Where is your ${highlighter.info(
"tailwind.config.js"
)} located?`,
initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG,
},
{
type: "text",
name: "components",
message: `Configure the import alias for ${highlighter.info(
"components"
)}:`,
initial: defaultConfig?.aliases["components"] ?? DEFAULT_COMPONENTS,
},
])
if (!options.style) {
process.exit(1)
}
const existingAliases =
defaultConfig && defaultConfig.aliases.components === options.components
? defaultConfig.aliases
: undefined
const aliasDefaults = getInitAliasDefaults(
options.components,
existingAliases
)
const aliasOptions = await prompts([
{
type: "text",
name: "ui",
message: `Configure the import alias for ${highlighter.info("ui")}:`,
initial: aliasDefaults.ui,
},
{
type: "text",
name: "lib",
message: `Configure the import alias for ${highlighter.info("lib")}:`,
initial: aliasDefaults.lib,
},
{
type: "text",
name: "hooks",
message: `Configure the import alias for ${highlighter.info("hooks")}:`,
initial: aliasDefaults.hooks,
},
{
type: "text",
name: "utils",
message: `Configure the import alias for ${highlighter.info("utils")}:`,
initial: aliasDefaults.utils,
},
{
type: "toggle",
name: "rsc",
message: `Are you using ${highlighter.info("React Server Components")}?`,
initial: defaultConfig?.rsc ?? true,
active: "yes",
inactive: "no",
},
])
return rawConfigSchema.parse({
$schema: "https://ui.shadcn.com/schema.json",
style: options.style,
tailwind: {
config: options.tailwindConfig,
css: options.tailwindCss,
baseColor: options.tailwindBaseColor,
cssVariables: options.tailwindCssVariables,
prefix: options.tailwindPrefix,
},
rsc: aliasOptions.rsc,
tsx: options.typescript,
aliases: {
components: options.components,
ui: aliasOptions.ui,
lib: aliasOptions.lib,
hooks: aliasOptions.hooks,
utils: aliasOptions.utils,
},
})
}
export function getInitAliasDefaults(
componentsAlias: string,
existingAliases?: Config["aliases"]
) {
const derivedLib =
existingAliases?.lib ?? deriveSiblingAlias(componentsAlias, "lib")
return {
ui: existingAliases?.ui ?? deriveUiAlias(componentsAlias),
lib: derivedLib,
hooks:
existingAliases?.hooks ?? deriveSiblingAlias(componentsAlias, "hooks"),
utils:
existingAliases?.utils ?? deriveUtilsAlias(componentsAlias, derivedLib),
}
}
function deriveUiAlias(componentsAlias: string) {
return componentsAlias ? `${componentsAlias}/ui` : `${DEFAULT_COMPONENTS}/ui`
}
function deriveUtilsAlias(componentsAlias: string, libAlias: string) {
if (componentsAlias === "#components") {
return "#utils"
}
return libAlias ? `${libAlias}/utils` : DEFAULT_UTILS
}
function deriveSiblingAlias(componentsAlias: string, segment: "lib" | "hooks") {
if (componentsAlias === "components") {
return segment
}
if (componentsAlias.endsWith("/components")) {
return `${componentsAlias.slice(0, -"/components".length)}/${segment}`
}
if (
componentsAlias.endsWith("components") &&
!componentsAlias.includes("/")
) {
return `${componentsAlias.slice(0, -"components".length)}${segment}`
}
return ""
}
export function shouldRunTemplatePostInit(
template:
| { postInit?: (options: { projectPath: string }) => Promise<void> }

View File

@@ -1,6 +1,55 @@
import { describe, expect, test } from "vitest"
import { shouldRunTemplatePostInit } from "../../src/commands/init"
import {
getInitAliasDefaults,
shouldRunTemplatePostInit,
} from "../../src/commands/init"
describe("getInitAliasDefaults", () => {
test("derives standard aliases from components", () => {
expect(getInitAliasDefaults("@/components")).toEqual({
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
utils: "@/lib/utils",
})
})
test("derives package import aliases from #components", () => {
expect(getInitAliasDefaults("#components")).toEqual({
ui: "#components/ui",
lib: "#lib",
hooks: "#hooks",
utils: "#utils",
})
})
test("derives sibling aliases for nested custom aliases", () => {
expect(getInitAliasDefaults("#custom/components")).toEqual({
ui: "#custom/components/ui",
lib: "#custom/lib",
hooks: "#custom/hooks",
utils: "#custom/lib/utils",
})
})
test("preserves existing aliases when components alias is unchanged", () => {
expect(
getInitAliasDefaults("#components", {
components: "#components",
ui: "#components/ui",
lib: "#lib",
hooks: "#hooks",
utils: "#utils",
})
).toEqual({
ui: "#components/ui",
lib: "#lib",
hooks: "#hooks",
utils: "#utils",
})
})
})
describe("shouldRunTemplatePostInit", () => {
test("does not run post-init for existing projects with an explicit template", () => {