fix: bug in registries

This commit is contained in:
shadcn
2026-02-27 11:45:30 +04:00
parent 9f8a877e8f
commit 40ab22fded
3 changed files with 157 additions and 18 deletions

View File

@@ -70,6 +70,8 @@ export const add = new Command()
await loadEnvFiles(options.cwd)
const isDryRun = options.dryRun || options.diff || options.view
let initialConfig = await getConfig(options.cwd)
if (!initialConfig) {
initialConfig = createConfig({
@@ -103,8 +105,6 @@ export const add = new Command()
itemType !== "registry:style" &&
itemType !== "registry:base"
const isDryRun = options.dryRun || options.diff || options.view
if (isUniversalRegistryItem(registryItem) && !isDryRun) {
await addComponents(components, initialConfig, options)
return
@@ -263,13 +263,14 @@ export const add = new Command()
config,
{
silent: options.silent || hasNewRegistries,
writeFile: !isDryRun,
}
)
config = updatedConfig
// Dry-run mode: preview changes without writing files.
// --diff and --view imply --dry-run.
if (options.dryRun || options.diff || options.view) {
if (isDryRun) {
const dryRunSpinner = spinner("Resolving items.", {
silent: options.silent,
}).start()

View File

@@ -26,7 +26,9 @@ function colorAction(action: DryRunFile["action"] | "update") {
// Format the shared header line.
function formatHeader(componentNames: string[]) {
return `${bold("┌")} ${bold(`shadcn add ${componentNames.join(", ")}`)} ${dim("(dry run)")}`
return `${bold("┌")} ${bold(`shadcn add ${componentNames.join(", ")}`)} ${dim(
"(dry run)"
)}`
}
// Check if a CSS path matches a filter.
@@ -92,7 +94,9 @@ function formatSummaryOutput(result: DryRunResult, componentNames: string[]) {
if (overwriteCount > 0) {
lines.push(
yellow(
`${overwriteCount} ${overwriteCount === 1 ? "file" : "files"} will be overwritten.`
`${overwriteCount} ${
overwriteCount === 1 ? "file" : "files"
} will be overwritten.`
)
)
lines.push(dim("│"))
@@ -107,7 +111,9 @@ function formatSummaryOutput(result: DryRunResult, componentNames: string[]) {
}
if (result.dependencies.length > 0) {
summaryParts.push(
`${result.dependencies.length} ${result.dependencies.length === 1 ? "dep" : "deps"}`
`${result.dependencies.length} ${
result.dependencies.length === 1 ? "dep" : "deps"
}`
)
}
if (result.css?.cssVarsCount) {
@@ -154,7 +160,9 @@ function formatDiffOutput(
if (cssMatch && result.css) {
lines.push(
`${dim("├")} ${bold(result.css.path)} ${dim("(")}${colorAction(result.css.action)}${dim(")")}`
`${dim("├")} ${bold(result.css.path)} ${dim("(")}${colorAction(
result.css.action
)}${dim(")")}`
)
if (result.css.action === "create" || !result.css.existingContent) {
@@ -183,7 +191,9 @@ function formatDiffOutput(
// Format a single file's diff block.
function formatFileDiff(file: DryRunFile, lines: string[]) {
lines.push(
`${dim("├")} ${bold(file.path)} ${dim("(")}${colorAction(file.action)}${dim(")")}`
`${dim("├")} ${bold(file.path)} ${dim("(")}${colorAction(file.action)}${dim(
")"
)}`
)
if (file.action === "skip") {
@@ -225,7 +235,9 @@ function formatViewOutput(
for (const file of filesToView) {
const contentLines = file.content.split("\n")
lines.push(
`${dim("├")} ${bold(file.path)} ${dim("(")}${colorAction(file.action)}${dim(")")} ${dim(`${contentLines.length} lines`)}`
`${dim("├")} ${bold(file.path)} ${dim("(")}${colorAction(
file.action
)}${dim(")")} ${dim(`${contentLines.length} lines`)}`
)
pushContentBox(lines, contentLines)
lines.push(dim("│"))
@@ -234,7 +246,9 @@ function formatViewOutput(
if (cssMatch && result.css) {
const contentLines = result.css.content.split("\n")
lines.push(
`${dim("├")} ${bold(result.css.path)} ${dim("(")}${colorAction(result.css.action)}${dim(")")} ${dim(`${contentLines.length} lines`)}`
`${dim("├")} ${bold(result.css.path)} ${dim("(")}${colorAction(
result.css.action
)}${dim(")")} ${dim(`${contentLines.length} lines`)}`
)
pushContentBox(lines, contentLines)
lines.push(dim("│"))
@@ -285,8 +299,8 @@ function formatFilesSection(result: DryRunResult, lines: string[]) {
file.action === "create"
? green
: file.action === "overwrite"
? yellow
: dim
? yellow
: dim
const pathStr = file.action === "skip" ? dim(file.path) : file.path
@@ -319,7 +333,9 @@ function formatCssSection(result: DryRunResult, lines: string[]) {
if (result.css.cssVarsCount > 0) {
lines.push(
`${dim("│")} ${green("+")} ${result.css.cssVarsCount} CSS variables added to ${cyan(result.css.path)}`
`${dim("│")} ${green("+")} ${
result.css.cssVarsCount
} CSS variables added to ${cyan(result.css.path)}`
)
} else {
lines.push(`${dim("│")} ${green("+")} Updated ${cyan(result.css.path)}`)
@@ -416,10 +432,7 @@ function computeUnifiedDiff(
return [dim(" No changes.")]
}
const output: string[] = [
dim(`--- a/${filePath}`),
dim(`+++ b/${filePath}`),
]
const output: string[] = [dim(`--- a/${filePath}`), dim(`+++ b/${filePath}`)]
// Use the actual new file lines for display.
const newLines = newStr.split("\n")
@@ -439,7 +452,9 @@ function computeUnifiedDiff(
output.push(
cyan(
`@@ -${hunk.oldStart},${contextCount + removedCount} +${hunk.newStart},${contextCount + addedCount} @@`
`@@ -${hunk.oldStart},${contextCount + removedCount} +${
hunk.newStart
},${contextCount + addedCount} @@`
)
)

View File

@@ -0,0 +1,123 @@
import { afterEach, describe, expect, test, vi } from "vitest"
import type { Config } from "../../src/utils/get-config"
// Mock dependencies.
vi.mock("../../src/registry/namespaces", () => ({
resolveRegistryNamespaces: vi.fn().mockResolvedValue(["@foo"]),
}))
vi.mock("../../src/registry/api", () => ({
getRegistriesIndex: vi.fn().mockResolvedValue({
"@foo": "https://foo.com/r/{name}.json",
}),
}))
vi.mock("../../src/utils/spinner", () => ({
spinner: vi.fn().mockReturnValue({
start: vi.fn().mockReturnValue({
succeed: vi.fn(),
fail: vi.fn(),
stop: vi.fn(),
}),
}),
}))
vi.mock("fs-extra", () => ({
default: {
writeFile: vi.fn().mockResolvedValue(undefined),
},
}))
import { ensureRegistriesInConfig } from "../../src/utils/registries"
import fs from "fs-extra"
afterEach(() => {
vi.clearAllMocks()
})
const baseConfig: Config = {
$schema: "",
style: "new-york",
tailwind: {
config: "",
css: "",
baseColor: "",
cssVariables: true,
prefix: "",
},
rsc: false,
tsx: true,
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
registries: {},
resolvedPaths: {
cwd: "/tmp/test-project",
tailwindConfig: "",
tailwindCss: "",
utils: "",
components: "",
lib: "",
hooks: "",
ui: "",
},
}
describe("ensureRegistriesInConfig", () => {
test("does not write to disk when writeFile is false", async () => {
const { config, newRegistries } = await ensureRegistriesInConfig(
["@foo/bar"],
baseConfig,
{ writeFile: false }
)
// Should still return the updated config with new registries.
expect(newRegistries).toEqual(["@foo"])
expect(config.registries?.["@foo"]).toBe(
"https://foo.com/r/{name}.json"
)
// Should NOT have written to disk.
expect(fs.writeFile).not.toHaveBeenCalled()
})
test("writes to disk when writeFile is true", async () => {
await ensureRegistriesInConfig(["@foo/bar"], baseConfig, {
writeFile: true,
})
expect(fs.writeFile).toHaveBeenCalledTimes(1)
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("components.json"),
expect.any(String),
"utf-8"
)
})
test("writes to disk by default (writeFile not specified)", async () => {
await ensureRegistriesInConfig(["@foo/bar"], baseConfig)
expect(fs.writeFile).toHaveBeenCalledTimes(1)
})
test("does not write when no new registries are found", async () => {
const configWithRegistry: Config = {
...baseConfig,
registries: {
"@foo": "https://foo.com/r/{name}.json",
},
}
await ensureRegistriesInConfig(["@foo/bar"], configWithRegistry, {
writeFile: true,
})
// No new registries, so no write.
expect(fs.writeFile).not.toHaveBeenCalled()
})
})