feat: add preset commands (#10530)

* feat(cli): add preset commands

* docs(skill): update preset command guidance

* docs(cli): document preset commands

* chore: changeset

* fix(cli): refine preset command output

* fix(cli): align preset decode output

* fix(cli): update preset output fields

* docs(changelog): add preset commands entry

* docs(changelog): show preset command output

* docs(changelog): clarify preset resolve examples

* docs(changelog): refine preset examples

* docs(changelog): add preset command sections

* docs(changelog): show preset resolve output

* docs(changelog): clarify preset open example

* docs(changelog): update preset resolve example

* docs: update preset announcement

* docs: link preset announcement to changelog

* test: increase next init timeout
This commit is contained in:
shadcn
2026-04-28 20:43:16 +04:00
committed by GitHub
parent 68a69d81f7
commit ea6086cbcc
10 changed files with 1003 additions and 67 deletions

View File

@@ -191,58 +191,7 @@ export function printInfo(data: Awaited<ReturnType<typeof collectInfo>>) {
})
logger.break()
logger.log(highlighter.info("Preset"))
if (!data.preset?.code) {
printEntries({
"--preset": "-",
})
} else {
const fallbacks = data.preset.fallbacks ?? []
const formatPresetValue = (key: string, value: string | undefined) => {
const suffix = fallbacks.includes(key) ? "*" : ""
return `${value ?? "-"}${suffix}`
}
printEntries({
"--preset": data.preset.code,
url: `${SHADCN_URL}/create?preset=${data.preset.code}`,
style: data.preset.values?.style ?? "-",
baseColor: formatPresetValue(
"baseColor",
data.preset.values?.baseColor
),
theme: formatPresetValue("theme", data.preset.values?.theme),
chartColor: formatPresetValue(
"chartColor",
data.preset.values?.chartColor
),
iconLibrary: formatPresetValue(
"iconLibrary",
data.preset.values?.iconLibrary
),
font: formatPresetValue("font", data.preset.values?.font),
fontHeading: formatPresetValue(
"fontHeading",
data.preset.values?.fontHeading
),
radius: formatPresetValue("radius", data.preset.values?.radius),
menuAccent: formatPresetValue(
"menuAccent",
data.preset.values?.menuAccent
),
menuColor: formatPresetValue(
"menuColor",
data.preset.values?.menuColor
),
})
if (fallbacks.length > 0) {
logger.log("")
logger.log(
" * Uses preset defaults for values not available as options on shadcn/create."
)
}
}
printPresetInfo(data.preset)
// Aliases.
logger.break()
@@ -296,8 +245,54 @@ export function printInfo(data: Awaited<ReturnType<typeof collectInfo>>) {
logger.break()
}
export function printPresetInfo(
preset: Awaited<ReturnType<typeof collectInfo>>["preset"],
options: {
fallbackNote?: string
} = {}
) {
logger.log(highlighter.info("Preset"))
if (!preset?.code) {
printEntries({
code: "-",
})
} else {
const fallbacks = preset.fallbacks ?? []
const formatPresetValue = (key: string, value: string | undefined) => {
const suffix = fallbacks.includes(key) ? "*" : ""
return `${value ?? "-"}${suffix}`
}
printEntries({
code: preset.code,
version: preset.code[0],
style: preset.values?.style ?? "-",
baseColor: formatPresetValue("baseColor", preset.values?.baseColor),
theme: formatPresetValue("theme", preset.values?.theme),
chartColor: formatPresetValue("chartColor", preset.values?.chartColor),
iconLibrary: formatPresetValue("iconLibrary", preset.values?.iconLibrary),
font: formatPresetValue("font", preset.values?.font),
fontHeading: formatPresetValue("fontHeading", preset.values?.fontHeading),
radius: formatPresetValue("radius", preset.values?.radius),
menuAccent: formatPresetValue("menuAccent", preset.values?.menuAccent),
menuColor: formatPresetValue("menuColor", preset.values?.menuColor),
url: `${SHADCN_URL}/create?preset=${preset.code}`,
})
if (fallbacks.length > 0) {
logger.log("")
logger.log(
options.fallbackNote ??
" * Uses preset defaults for values not available as options on shadcn/create."
)
}
}
}
function printEntries(entries: Record<string, string>) {
const maxKeyLength = Math.max(...Object.keys(entries).map((k) => k.length))
const maxKeyLength = Math.max(
...Object.keys(entries).map((key) => key.length)
)
for (const [key, value] of Object.entries(entries)) {
logger.log(` ${key.padEnd(maxKeyLength + 2)}${value}`)
}

View File

@@ -0,0 +1,492 @@
import { existsSync } from "fs"
import path from "path"
import { resolveProjectPreset } from "@/src/preset/resolve"
import { getConfig } from "@/src/utils/get-config"
import {
formatMonorepoMessage,
getMonorepoTargets,
isMonorepoRoot,
} from "@/src/utils/get-monorepo-info"
import { getProjectInfo } from "@/src/utils/get-project-info"
import { logger } from "@/src/utils/logger"
import open from "open"
import { beforeEach, describe, expect, it, vi } from "vitest"
import {
decodePresetCode,
getPresetUrl,
openCommand,
preset as presetCommand,
printPresetDecode,
resolve as resolveCommand,
url,
} from "./preset"
vi.mock("fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("fs")>()
return {
...actual,
existsSync: vi.fn(),
}
})
vi.mock("open", () => ({
default: vi.fn(),
}))
vi.mock("@/src/preset/resolve", () => ({
resolveProjectPreset: vi.fn(),
}))
vi.mock("@/src/utils/get-config", () => ({
getBase: vi.fn(() => "radix"),
getConfig: vi.fn(),
}))
vi.mock("@/src/utils/get-monorepo-info", () => ({
formatMonorepoMessage: vi.fn(),
getMonorepoTargets: vi.fn(),
isMonorepoRoot: vi.fn(),
}))
vi.mock("@/src/utils/get-project-info", () => ({
getProjectComponents: vi.fn(),
getProjectInfo: vi.fn(),
}))
vi.mock("@/src/utils/handle-error", () => ({
handleError: vi.fn((error) => {
throw error
}),
}))
vi.mock("@/src/utils/highlighter", () => ({
highlighter: {
info: (value: string) => value,
},
}))
vi.mock("@/src/utils/logger", () => ({
logger: {
break: vi.fn(),
error: vi.fn(),
log: vi.fn(),
},
}))
describe("preset commands", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(existsSync).mockReturnValue(true)
vi.mocked(isMonorepoRoot).mockResolvedValue(false)
vi.mocked(getMonorepoTargets).mockResolvedValue([])
vi.mocked(getConfig).mockResolvedValue(
{} as Awaited<ReturnType<typeof getConfig>>
)
vi.mocked(getProjectInfo).mockResolvedValue(
{} as Awaited<ReturnType<typeof getProjectInfo>>
)
vi.mocked(resolveProjectPreset).mockResolvedValue({
code: "b123",
fallbacks: [],
values: {
style: "luma",
baseColor: "mist",
theme: "blue",
chartColor: "emerald",
iconLibrary: "phosphor",
font: "inter",
fontHeading: "lora",
radius: "large",
menuAccent: "bold",
menuColor: "inverted",
},
})
})
it("decodes current preset codes", () => {
const result = decodePresetCode("b0")
expect(result).toEqual({
code: "b0",
version: "b",
values: {
style: "nova",
baseColor: "neutral",
theme: "neutral",
chartColor: "neutral",
iconLibrary: "lucide",
font: "inter",
fontHeading: "inherit",
radius: "default",
menuAccent: "subtle",
menuColor: "default",
},
derived: [],
url: getPresetUrl("b0"),
})
})
it("applies compatibility values for older preset codes", () => {
const result = decodePresetCode("a0")
expect(result.version).toBe("a")
expect(result.values.chartColor).toBe("blue")
expect(result.derived).toEqual(["chartColor"])
})
it("prints compatibility markers in human output", () => {
printPresetDecode(decodePresetCode("a0"))
expect(logger.log).toHaveBeenCalledWith("Preset")
expect(logger.log).toHaveBeenCalledWith(" code a0")
expect(logger.log).toHaveBeenCalledWith(" version a")
expect(logger.log).toHaveBeenCalledWith(" chartColor blue*")
expect(logger.log).toHaveBeenCalledWith(
` url ${getPresetUrl("a0")}`
)
expect(logger.log).toHaveBeenCalledWith(
" * Compatibility value for older preset versions."
)
})
it("matches decoded preset output ordering to resolved preset output", () => {
printPresetDecode(decodePresetCode("b0"))
expect(logger.log).toHaveBeenCalledWith("Preset")
expect(vi.mocked(logger.log).mock.calls.map((call) => call[0])).toEqual([
"Preset",
" code b0",
" version b",
" style nova",
" baseColor neutral",
" theme neutral",
" chartColor neutral",
" iconLibrary lucide",
" font inter",
" fontHeading inherit",
" radius default",
" menuAccent subtle",
" menuColor default",
` url ${getPresetUrl("b0")}`,
])
})
it("prints decoded preset JSON with derived values", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
await presetCommand.parseAsync(["decode", "a0", "--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toEqual({
code: "a0",
version: "a",
values: {
style: "nova",
baseColor: "neutral",
theme: "neutral",
chartColor: "blue",
iconLibrary: "lucide",
font: "inter",
fontHeading: "inherit",
radius: "default",
menuAccent: "subtle",
menuColor: "default",
},
derived: ["chartColor"],
url: getPresetUrl("a0"),
})
log.mockRestore()
})
it("rejects invalid codes", () => {
expect(() => decodePresetCode("c0")).toThrow("Invalid preset code: c0")
})
it("rejects URLs", () => {
expect(() =>
decodePresetCode("https://ui.shadcn.com/create?preset=a0")
).toThrow("Invalid preset code: https://ui.shadcn.com/create?preset=a0")
})
it("prints the create URL from preset url", async () => {
await url.parseAsync(["a0"], { from: "user" })
expect(logger.break).not.toHaveBeenCalled()
expect(logger.log).toHaveBeenCalledWith(getPresetUrl("a0"))
})
it("prints the create URL before opening it", async () => {
vi.mocked(open).mockImplementation(async () => {
expect(logger.log).toHaveBeenCalledWith(
` Opening ${getPresetUrl("a0")} in your browser.`
)
return {} as Awaited<ReturnType<typeof open>>
})
await openCommand.parseAsync(["a0"], { from: "user" })
expect(logger.break).toHaveBeenCalledTimes(2)
expect(logger.log).toHaveBeenCalledWith(
` Opening ${getPresetUrl("a0")} in your browser.`
)
expect(open).toHaveBeenCalledWith(getPresetUrl("a0"))
})
it("does not open invalid preset codes", async () => {
const exit = mockProcessExit()
await expect(
openCommand.parseAsync(["https://ui.shadcn.com/create?preset=a0"], {
from: "user",
})
).rejects.toThrow("process.exit:1")
expect(logger.error).toHaveBeenCalledWith(
"Invalid preset code: https://ui.shadcn.com/create?preset=a0"
)
expect(open).not.toHaveBeenCalled()
exit.mockRestore()
})
it("fails when opening the browser fails", async () => {
const exit = mockProcessExit()
vi.mocked(open).mockRejectedValue(new Error("open failed"))
await expect(
openCommand.parseAsync(["a0"], { from: "user" })
).rejects.toThrow("process.exit:1")
expect(logger.log).toHaveBeenCalledWith(
` Opening ${getPresetUrl("a0")} in your browser.`
)
expect(logger.error).toHaveBeenCalledWith(
"Failed to open preset URL: open failed"
)
exit.mockRestore()
})
it("resolves a project preset using the existing resolver", async () => {
await resolveCommand.parseAsync([], { from: "user" })
expect(logger.log).toHaveBeenCalledWith("Preset")
expect(logger.log).toHaveBeenCalledWith(" code b123")
expect(logger.log).toHaveBeenCalledWith(" version b")
expect(logger.log).toHaveBeenCalledWith(expect.stringContaining("b123"))
expect(logger.log).toHaveBeenCalledWith(
expect.stringContaining("https://ui.shadcn.com/create?preset=b123")
)
expect(resolveProjectPreset).toHaveBeenCalledWith({}, {})
})
it("prints fallback markers when resolving a project preset", async () => {
vi.mocked(resolveProjectPreset).mockResolvedValueOnce({
code: "b123",
fallbacks: ["theme"],
values: {
style: "luma",
baseColor: "mist",
theme: "neutral",
chartColor: "emerald",
iconLibrary: "phosphor",
font: "inter",
fontHeading: "lora",
radius: "large",
menuAccent: "bold",
menuColor: "inverted",
},
})
await resolveCommand.parseAsync([], { from: "user" })
expect(logger.log).toHaveBeenCalledWith(expect.stringContaining("neutral*"))
expect(logger.log).toHaveBeenCalledWith(
" * Uses preset defaults for values not available as options on shadcn/create."
)
})
it("prints the resolved preset as JSON", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
await resolveCommand.parseAsync(["--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toEqual({
code: "b123",
fallbacks: [],
values: {
style: "luma",
baseColor: "mist",
theme: "blue",
chartColor: "emerald",
iconLibrary: "phosphor",
font: "inter",
fontHeading: "lora",
radius: "large",
menuAccent: "bold",
menuColor: "inverted",
},
})
log.mockRestore()
})
it("prints fallback metadata in resolved preset JSON", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
vi.mocked(resolveProjectPreset).mockResolvedValueOnce({
code: "b123",
fallbacks: ["theme"],
values: {
style: "luma",
baseColor: "mist",
theme: "neutral",
chartColor: "emerald",
iconLibrary: "phosphor",
font: "inter",
fontHeading: "lora",
radius: "large",
menuAccent: "bold",
menuColor: "inverted",
},
})
await resolveCommand.parseAsync(["--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toMatchObject({
fallbacks: ["theme"],
values: {
theme: "neutral",
},
})
log.mockRestore()
})
it("supports preset info as an alias for preset resolve", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
await presetCommand.parseAsync(["info", "--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toMatchObject({
code: "b123",
})
log.mockRestore()
})
it("resolves presets from the provided cwd", async () => {
await resolveCommand.parseAsync(["--cwd", "apps/web"], { from: "user" })
expect(getConfig).toHaveBeenCalledWith(path.resolve("apps/web"))
expect(getProjectInfo).toHaveBeenCalledWith(path.resolve("apps/web"))
})
it("prints no-config output when no components.json exists", async () => {
vi.mocked(existsSync).mockReturnValue(false)
vi.mocked(getConfig).mockResolvedValueOnce(null)
await resolveCommand.parseAsync([], { from: "user" })
expect(logger.log).toHaveBeenCalledWith("No components.json found.")
})
it("prints null JSON when no components.json exists", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
vi.mocked(existsSync).mockReturnValue(false)
vi.mocked(getConfig).mockResolvedValueOnce(null)
await resolveCommand.parseAsync(["--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toBeNull()
log.mockRestore()
})
it("prints null JSON when no preset can be resolved", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {})
vi.mocked(resolveProjectPreset).mockResolvedValueOnce({
code: null,
fallbacks: [],
values: null,
})
await resolveCommand.parseAsync(["--json"], { from: "user" })
expect(JSON.parse(log.mock.calls[0][0] as string)).toBeNull()
log.mockRestore()
})
it("matches info monorepo-root behavior", async () => {
const exit = mockProcessExit()
const targets = [{ name: "apps/web" }]
vi.mocked(existsSync).mockReturnValue(false)
vi.mocked(isMonorepoRoot).mockResolvedValue(true)
vi.mocked(getMonorepoTargets).mockResolvedValue(targets as never)
await expect(
resolveCommand.parseAsync([], { from: "user" })
).rejects.toThrow("process.exit:1")
expect(formatMonorepoMessage).toHaveBeenCalledWith(
"preset resolve",
targets
)
expect(getConfig).not.toHaveBeenCalled()
exit.mockRestore()
})
it("prints monorepo-root JSON for preset resolve --json", async () => {
const exit = mockProcessExit()
const log = vi.spyOn(console, "log").mockImplementation(() => {})
const targets = [{ name: "apps/web" }]
vi.mocked(existsSync).mockReturnValue(false)
vi.mocked(isMonorepoRoot).mockResolvedValue(true)
vi.mocked(getMonorepoTargets).mockResolvedValue(targets as never)
await expect(
resolveCommand.parseAsync(["--json"], { from: "user" })
).rejects.toThrow("process.exit:1")
expect(JSON.parse(log.mock.calls[0][0] as string)).toEqual({
error: "monorepo_root",
message:
"You are running preset resolve from a monorepo root. Use the -c flag to specify a workspace.",
targets: ["apps/web"],
})
expect(formatMonorepoMessage).not.toHaveBeenCalled()
expect(getConfig).not.toHaveBeenCalled()
log.mockRestore()
exit.mockRestore()
})
it("prints help when no preset subcommand is provided", async () => {
const outputHelp = vi
.spyOn(presetCommand, "outputHelp")
.mockImplementation(() => {})
await presetCommand.parseAsync([], { from: "user" })
expect(outputHelp).toHaveBeenCalledOnce()
outputHelp.mockRestore()
})
it("errors on unknown preset subcommands", async () => {
const exit = mockProcessExit()
const outputHelp = vi
.spyOn(presetCommand, "outputHelp")
.mockImplementation(() => {})
const writeErr = vi
.spyOn(process.stderr, "write")
.mockImplementation(() => true)
await expect(
presetCommand.parseAsync(["bogus"], { from: "user" })
).rejects.toThrow("process.exit:1")
expect(outputHelp).not.toHaveBeenCalled()
expect(writeErr.mock.calls.join("")).toContain("too many arguments")
writeErr.mockRestore()
outputHelp.mockRestore()
exit.mockRestore()
})
})
function mockProcessExit() {
return vi.spyOn(process, "exit").mockImplementation((code) => {
throw new Error(`process.exit:${code}`)
})
}

View File

@@ -0,0 +1,231 @@
import { existsSync } from "fs"
import path from "path"
import { printPresetInfo } from "@/src/commands/info"
import {
decodePreset,
V1_CHART_COLOR_MAP,
type PresetConfig,
} from "@/src/preset/preset"
import { resolveProjectPreset } from "@/src/preset/resolve"
import { SHADCN_URL } from "@/src/registry/constants"
import { getConfig } from "@/src/utils/get-config"
import {
formatMonorepoMessage,
getMonorepoTargets,
isMonorepoRoot,
} from "@/src/utils/get-monorepo-info"
import { getProjectInfo } from "@/src/utils/get-project-info"
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
import { Command } from "commander"
import open from "open"
type PresetValues = Omit<PresetConfig, "chartColor"> & {
chartColor: NonNullable<PresetConfig["chartColor"]>
}
type PresetDecodeResult = {
code: string
version: string
values: PresetValues
derived: string[]
url: string
}
export function getPresetUrl(code: string) {
return `${SHADCN_URL}/create?preset=${code}`
}
export function decodePresetCode(code: string): PresetDecodeResult {
const decoded = decodePreset(code)
if (!decoded) {
throw new Error(`Invalid preset code: ${code}`)
}
const derived: string[] = []
const chartColor = (decoded.chartColor ??
V1_CHART_COLOR_MAP[decoded.theme] ??
decoded.theme) as PresetValues["chartColor"]
if (!decoded.chartColor) {
derived.push("chartColor")
}
return {
code,
version: code[0],
values: {
...decoded,
chartColor,
},
derived,
url: getPresetUrl(code),
}
}
export function printPresetDecode(result: PresetDecodeResult) {
printPresetInfo(
{
code: result.code,
fallbacks: result.derived,
values: result.values,
},
{
fallbackNote: " * Compatibility value for older preset versions.",
}
)
}
export const decode = new Command()
.name("decode")
.description("decode a preset code")
.argument("<code>", "the preset code to decode")
.option("--json", "output as JSON.", false)
.action((code, opts) => {
try {
const result = decodePresetCode(code)
if (opts.json) {
console.log(
JSON.stringify(
{
code: result.code,
version: result.version,
values: result.values,
derived: result.derived,
url: result.url,
},
null,
2
)
)
return
}
printPresetDecode(result)
} catch (error) {
handlePresetError(error)
}
})
export const url = new Command()
.name("url")
.description("get the create URL for a preset code")
.argument("<code>", "the preset code")
.action((code) => {
try {
logger.log(decodePresetCode(code).url)
} catch (error) {
handlePresetError(error)
}
})
export const openCommand = new Command()
.name("open")
.description("open a preset code in the browser")
.argument("<code>", "the preset code")
.action(async (code) => {
let presetUrl: string
try {
presetUrl = decodePresetCode(code).url
} catch (error) {
handlePresetError(error)
return
}
logger.break()
logger.log(` Opening ${presetUrl} in your browser.`)
logger.break()
try {
await open(presetUrl)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
handlePresetError(new Error(`Failed to open preset URL: ${message}`))
}
})
export const resolve = new Command()
.name("resolve")
.alias("info")
.description("resolve a preset from your project")
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
)
.option("--json", "output as JSON.", false)
.action(async (opts) => {
try {
const cwd = path.resolve(opts.cwd)
if (
!existsSync(path.resolve(cwd, "components.json")) &&
(await isMonorepoRoot(cwd))
) {
const targets = await getMonorepoTargets(cwd)
if (targets.length > 0) {
if (opts.json) {
console.log(
JSON.stringify(
{
error: "monorepo_root",
message:
"You are running preset resolve from a monorepo root. Use the -c flag to specify a workspace.",
targets: targets.map((target) => target.name),
},
null,
2
)
)
} else {
formatMonorepoMessage("preset resolve", targets)
}
process.exit(1)
}
}
const config = await getConfig(cwd)
if (!config) {
if (opts.json) {
console.log(JSON.stringify(null, null, 2))
return
}
logger.log("No components.json found.")
return
}
const projectInfo = await getProjectInfo(cwd)
const preset = await resolveProjectPreset(config, projectInfo)
if (opts.json) {
console.log(JSON.stringify(preset.code ? preset : null, null, 2))
return
}
printPresetInfo(preset)
} catch (error) {
handleError(error)
}
})
export const preset = new Command()
.name("preset")
.description("manage presets")
.addCommand(decode)
.addCommand(resolve)
.addCommand(url)
.addCommand(openCommand)
.action(() => {
preset.outputHelp()
})
function handlePresetError(error: unknown) {
if (error instanceof Error) {
logger.error(error.message)
}
process.exit(1)
}

View File

@@ -8,6 +8,7 @@ import { info } from "@/src/commands/info"
import { init } from "@/src/commands/init"
import { mcp } from "@/src/commands/mcp"
import { migrate } from "@/src/commands/migrate"
import { preset } from "@/src/commands/preset"
import { registry } from "@/src/commands/registry"
import { search } from "@/src/commands/search"
import { view } from "@/src/commands/view"
@@ -40,6 +41,7 @@ async function main() {
.addCommand(info)
.addCommand(build)
.addCommand(mcp)
.addCommand(preset)
.addCommand(registry)
program.parse()

View File

@@ -13,7 +13,7 @@ import { createRegistryServer } from "../utils/registry"
describe("shadcn init - next-app", () => {
it("should init with default configuration", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults"])
await npxShadcn(fixturePath, ["init", "--defaults"], { timeout: 120000 })
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
@@ -54,7 +54,9 @@ describe("shadcn init - next-app", () => {
it("should init without CSS variables", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"])
await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"], {
timeout: 120000,
})
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
@@ -64,7 +66,9 @@ describe("shadcn init - next-app", () => {
it("should init with components", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "button"])
await npxShadcn(fixturePath, ["init", "--defaults", "button"], {
timeout: 120000,
})
expect(
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))