Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/v4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"server-only": "^0.0.1",
"shadcn": "4.17.0",
"shadcn": "4.18.0",
"shiki": "^3.23.0",
"sonner": "^2.0.0",
"streamdown": "^2.5.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/shadcn/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# shadcn

## 4.18.0

### Minor Changes

- [#11501](https://github.com/shadcn-ui/ui/pull/11501) [`aef1cdca54e8da689351cdddf959342909e45e76`](https://github.com/shadcn-ui/ui/commit/aef1cdca54e8da689351cdddf959342909e45e76) Thanks [@shadcn](https://github.com/shadcn)! - merge registries from package.json and components.json, and support adding registries to package.json when components.json is not present

### Patch Changes

- [#11500](https://github.com/shadcn-ui/ui/pull/11500) [`e66b99b14dd9c54afc434dbf5a702f170b1153b0`](https://github.com/shadcn-ui/ui/commit/e66b99b14dd9c54afc434dbf5a702f170b1153b0) Thanks [@shadcn](https://github.com/shadcn)! - Skip unreadable directories during file scans instead of failing with `EACCES`.

- [#11504](https://github.com/shadcn-ui/ui/pull/11504) [`9f4e3ff26025d16a243ea03cc891c734c4cf0b59`](https://github.com/shadcn-ui/ui/commit/9f4e3ff26025d16a243ea03cc891c734c4cf0b59) Thanks [@shadcn](https://github.com/shadcn)! - Skip unreadable directories when resolving monorepo targets.

- [#11502](https://github.com/shadcn-ui/ui/pull/11502) [`87d71b3629c34f3c38a353a211ec8591c1ff1721`](https://github.com/shadcn-ui/ui/commit/87d71b3629c34f3c38a353a211ec8591c1ff1721) Thanks [@shadcn](https://github.com/shadcn)! - resolve registries declared in package.json when adding components. `shadcn add`, `search`, `view` and `init` now resolve registries from package.json in memory without persisting them to components.json

- [#9248](https://github.com/shadcn-ui/ui/pull/9248) [`03c45b822e60195796dfd3d2fcf7c223ff4ece86`](https://github.com/shadcn-ui/ui/commit/03c45b822e60195796dfd3d2fcf7c223ff4ece86) Thanks [@Grafikart](https://github.com/Grafikart)! - Fix shadcn for projects with unreadable permission files

## 4.17.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/shadcn/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "4.17.0",
"version": "4.18.0",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"
Expand Down
7 changes: 6 additions & 1 deletion packages/shadcn/src/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ describe("runInit", () => {
createProjectConfig(projectCwd)
)
vi.mocked(ensureRegistriesInConfig).mockImplementation(
async (_components, config) => ({ config, newRegistries: [] })
async (_components, config) => ({
config,
newRegistries: [],
discoveredRegistries: {},
packageJsonRegistries: {},
})
)
vi.mocked(addComponents).mockResolvedValue(undefined)
})
Expand Down
31 changes: 22 additions & 9 deletions packages/shadcn/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,17 +712,20 @@ export async function runInit(

// Ensure registries are configured for the components we're about to add.
const fullConfigForRegistry = await resolveConfigPaths(options.cwd, config)
const { config: configWithRegistries } = await ensureRegistriesInConfig(
components,
fullConfigForRegistry,
{
const { discoveredRegistries, packageJsonRegistries } =
await ensureRegistriesInConfig(components, fullConfigForRegistry, {
silent: true,
}
)
writeFile: false,
})

// Update config with any new registries found.
if (configWithRegistries.registries) {
config.registries = configWithRegistries.registries
// Update config with registries discovered from the registries index.
// Registries declared in package.json are resolved in memory below and
// never persisted to components.json.
if (Object.keys(discoveredRegistries).length > 0) {
config.registries = {
...config.registries,
...discoveredRegistries,
}
}

const componentSpinner = spinner(`Writing components.json.`).start()
Expand Down Expand Up @@ -775,6 +778,16 @@ export async function runInit(

// Propagate design settings to workspace components.json files.
const fullConfig = await resolveConfigPaths(options.cwd, config)

// Include package.json-declared registries for installation. These are
// resolved in memory and stay out of the components.json we just wrote.
if (Object.keys(packageJsonRegistries).length > 0) {
fullConfig.registries = {
...fullConfig.registries,
...packageJsonRegistries,
}
}

const workspaceConfig = await getWorkspaceConfig(fullConfig)
if (workspaceConfig) {
const designSettings: Record<string, unknown> = {}
Expand Down
158 changes: 157 additions & 1 deletion packages/shadcn/src/commands/registry/add.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { tmpdir } from "os"
import path from "path"
import fs from "fs-extra"
import { describe, expect, it } from "vitest"

import { parseRegistryArg } from "./add"
import { addRegistriesToConfig, parseRegistryArg } from "./add"

describe("parseRegistryArg", () => {
it("should parse namespace without URL", () => {
Expand Down Expand Up @@ -56,3 +59,156 @@ describe("parseRegistryArg", () => {
).toThrow("must start with @")
})
})

describe("addRegistriesToConfig", () => {
it("should write registries to components.json when it exists", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
const componentsJsonFile = path.join(tempDir, "components.json")

await fs.writeJson(componentsJsonFile, { style: "new-york" })

try {
await addRegistriesToConfig(
["@acme=https://acme.com/r/{name}.json"],
tempDir,
{ silent: true }
)

const config = await fs.readJson(componentsJsonFile)
expect(config).toEqual({
style: "new-york",
registries: {
"@acme": "https://acme.com/r/{name}.json",
},
})
} finally {
await fs.rm(tempDir, { recursive: true })
}
})

it("should prefer components.json over package.json when both exist", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
const componentsJsonFile = path.join(tempDir, "components.json")
const packageJsonFile = path.join(tempDir, "package.json")

await fs.writeJson(componentsJsonFile, { style: "new-york" })
await fs.writeJson(packageJsonFile, { name: "test-package" })

try {
await addRegistriesToConfig(
["@acme=https://acme.com/r/{name}.json"],
tempDir,
{ silent: true }
)

const componentsJson = await fs.readJson(componentsJsonFile)
const packageJson = await fs.readJson(packageJsonFile)
expect(componentsJson.registries).toEqual({
"@acme": "https://acme.com/r/{name}.json",
})
expect(packageJson.registries).toBeUndefined()
} finally {
await fs.rm(tempDir, { recursive: true })
}
})

it("should write registries to package.json when components.json does not exist", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
const packageJsonFile = path.join(tempDir, "package.json")

await fs.writeJson(packageJsonFile, {
name: "test-package",
version: "1.0.0",
})

try {
await addRegistriesToConfig(
["@acme=https://acme.com/r/{name}.json"],
tempDir,
{ silent: true }
)

const packageJson = await fs.readJson(packageJsonFile)
expect(packageJson).toEqual({
name: "test-package",
version: "1.0.0",
registries: {
"@acme": "https://acme.com/r/{name}.json",
},
})
} finally {
await fs.rm(tempDir, { recursive: true })
}
})

it("should preserve existing registries when adding to package.json", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
const packageJsonFile = path.join(tempDir, "package.json")

await fs.writeJson(packageJsonFile, {
name: "test-package",
registries: {
"@existing": "https://existing.com/r/{name}.json",
},
})

try {
await addRegistriesToConfig(
["@acme=https://acme.com/r/{name}.json"],
tempDir,
{ silent: true }
)

const packageJson = await fs.readJson(packageJsonFile)
expect(packageJson.registries).toEqual({
"@existing": "https://existing.com/r/{name}.json",
"@acme": "https://acme.com/r/{name}.json",
})
} finally {
await fs.rm(tempDir, { recursive: true })
}
})

it("should skip registries already configured in package.json", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
const packageJsonFile = path.join(tempDir, "package.json")

await fs.writeJson(packageJsonFile, {
name: "test-package",
registries: {
"@acme": "https://existing.com/r/{name}.json",
},
})

try {
await addRegistriesToConfig(
["@acme=https://new.com/r/{name}.json"],
tempDir,
{ silent: true }
)

const packageJson = await fs.readJson(packageJsonFile)
expect(packageJson.registries).toEqual({
"@acme": "https://existing.com/r/{name}.json",
})
} finally {
await fs.rm(tempDir, { recursive: true })
}
})

it("should throw when neither components.json nor package.json exists", async () => {
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))

try {
await expect(
addRegistriesToConfig(
["@acme=https://acme.com/r/{name}.json"],
tempDir,
{ silent: true }
)
).rejects.toThrow(/No .*components\.json.* or .*package\.json.* found/)
} finally {
await fs.rm(tempDir, { recursive: true })
}
})
})
21 changes: 14 additions & 7 deletions packages/shadcn/src/commands/registry/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,27 @@ function pluralize(count: number, singular: string, plural: string) {
return `${count} ${count === 1 ? singular : plural}`
}

async function addRegistriesToConfig(
export async function addRegistriesToConfig(
registryArgs: string[],
cwd: string,
options: { silent?: boolean }
) {
const configPath = path.resolve(cwd, "components.json")
if (!fs.existsSync(configPath)) {
// Write to components.json when it exists, otherwise fall back to
// package.json. This mirrors how registries are resolved.
const configPath = ["components.json", "package.json"]
.map((file) => path.resolve(cwd, file))
.find((file) => fs.existsSync(file))

if (!configPath) {
throw new Error(
`No ${highlighter.info("components.json")} found. Run ${highlighter.info(
"shadcn init"
)} first.`
`No ${highlighter.info("components.json")} or ${highlighter.info(
"package.json"
)} found. Run ${highlighter.info("shadcn init")} first.`
)
}

const configFileName = path.basename(configPath)

const parsed = registryArgs.map(parseRegistryArg)
const needsLookup = parsed.filter((p) => !p.url)
let registriesIndex: { name: string; url: string }[] = []
Expand Down Expand Up @@ -179,7 +186,7 @@ async function addRegistriesToConfig(
},
}

const writeSpinner = spinner("Updating components.json.", {
const writeSpinner = spinner(`Updating ${configFileName}.`, {
silent: options.silent,
}).start()
await fs.writeJson(configPath, updatedConfig, { spaces: 2 })
Expand Down
2 changes: 2 additions & 0 deletions packages/shadcn/src/commands/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ vi.mock("@/src/utils/registries", () => ({
ensureRegistriesInConfig: vi.fn(() => ({
config: baseConfig,
newRegistries: [],
discoveredRegistries: {},
packageJsonRegistries: {},
})),
}))

Expand Down
3 changes: 3 additions & 0 deletions packages/shadcn/src/migrations/migrate-radix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export async function migrateRadix(
cwd: basePath,
onlyFiles: true,
ignore: ["**/node_modules/**"],
suppressErrors: true,
})
} else {
const fullPath = path.resolve(basePath, options.path)
Expand All @@ -131,6 +132,7 @@ export async function migrateRadix(
cwd: basePath,
onlyFiles: true,
ignore: ["**/node_modules/**"],
suppressErrors: true,
})
} else if (stat.isFile()) {
files = [options.path]
Expand All @@ -154,6 +156,7 @@ export async function migrateRadix(
files = await fg("**/*.{js,ts,jsx,tsx}", {
cwd: basePath,
onlyFiles: true,
suppressErrors: true,
})
}

Expand Down
3 changes: 3 additions & 0 deletions packages/shadcn/src/migrations/migrate-rtl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function migrateRtl(
cwd: basePath,
onlyFiles: true,
ignore: ["**/node_modules/**"],
suppressErrors: true,
})
} else {
const fullPath = path.resolve(basePath, options.path)
Expand All @@ -51,6 +52,7 @@ export async function migrateRtl(
cwd: basePath,
onlyFiles: true,
ignore: ["**/node_modules/**"],
suppressErrors: true,
})
} else if (stat.isFile()) {
files = [options.path]
Expand All @@ -74,6 +76,7 @@ export async function migrateRtl(
files = await fg("**/*.{js,ts,jsx,tsx}", {
cwd: basePath,
onlyFiles: true,
suppressErrors: true,
})
}

Expand Down
Loading
Loading