mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 12:15:43 +00:00
Compare commits
3 Commits
shadcn@3.2
...
shadcn@3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
275e3a2d59 | ||
|
|
e5402f9a20 | ||
|
|
04668da018 |
@@ -87,7 +87,7 @@
|
|||||||
"recharts": "2.15.1",
|
"recharts": "2.15.1",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"shadcn": "3.2.0",
|
"shadcn": "3.2.1",
|
||||||
"shiki": "^1.10.1",
|
"shiki": "^1.10.1",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
|
|||||||
@@ -14,5 +14,6 @@
|
|||||||
"@heseui": "https://www.heseui.com/r/{name}.json",
|
"@heseui": "https://www.heseui.com/r/{name}.json",
|
||||||
"@paceui-ui": "https://ui.paceui.com/r/{name}.json",
|
"@paceui-ui": "https://ui.paceui.com/r/{name}.json",
|
||||||
"@basecn": "https://basecn.dev/r/{name}.json",
|
"@basecn": "https://basecn.dev/r/{name}.json",
|
||||||
"@ncdai": "https://chanhdai.com/r/{name}.json"
|
"@ncdai": "https://chanhdai.com/r/{name}.json",
|
||||||
|
"@8bitcn": "https://8bitcn.com/r/{name}.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"react-resizable-panels": "^2.0.22",
|
"react-resizable-panels": "^2.0.22",
|
||||||
"react-wrap-balancer": "^0.4.1",
|
"react-wrap-balancer": "^0.4.1",
|
||||||
"recharts": "2.12.7",
|
"recharts": "2.12.7",
|
||||||
"shadcn": "3.2.0",
|
"shadcn": "3.2.1",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"sonner": "^1.2.3",
|
"sonner": "^1.2.3",
|
||||||
"swr": "2.2.6-beta.3",
|
"swr": "2.2.6-beta.3",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @shadcn/ui
|
# @shadcn/ui
|
||||||
|
|
||||||
|
## 3.2.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8147](https://github.com/shadcn-ui/ui/pull/8147) [`e5402f9a20f070e92e7384c1ae08e6bfb79cd7a9`](https://github.com/shadcn-ui/ui/commit/e5402f9a20f070e92e7384c1ae08e6bfb79cd7a9) Thanks [@shadcn](https://github.com/shadcn)! - fix recursive namespacing
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "3.2.0",
|
"version": "3.2.1",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
469
packages/shadcn/src/registry/namespaces.test.ts
Normal file
469
packages/shadcn/src/registry/namespaces.test.ts
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { Config } from "../utils/get-config"
|
||||||
|
import { BUILTIN_REGISTRIES } from "./constants"
|
||||||
|
import { RegistryNotConfiguredError } from "./errors"
|
||||||
|
import { resolveRegistryNamespaces } from "./namespaces"
|
||||||
|
import * as resolver from "./resolver"
|
||||||
|
|
||||||
|
// Mock the resolver module.
|
||||||
|
vi.mock("./resolver", () => ({
|
||||||
|
fetchRegistryItems: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Test utility function to check namespace configuration.
|
||||||
|
function checkNamespaceConfiguration(
|
||||||
|
namespaces: string[],
|
||||||
|
config: Config
|
||||||
|
): { configured: string[]; missing: string[] } {
|
||||||
|
const configured: string[] = []
|
||||||
|
const missing: string[] = []
|
||||||
|
|
||||||
|
for (const namespace of namespaces) {
|
||||||
|
if (BUILTIN_REGISTRIES[namespace] || config.registries?.[namespace]) {
|
||||||
|
configured.push(namespace)
|
||||||
|
} else {
|
||||||
|
missing.push(namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { configured, missing }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveRegistryNamespaces", () => {
|
||||||
|
const mockConfig: Config = {
|
||||||
|
style: "default",
|
||||||
|
tailwind: {
|
||||||
|
config: "tailwind.config.js",
|
||||||
|
css: "app/globals.css",
|
||||||
|
baseColor: "slate",
|
||||||
|
cssVariables: true,
|
||||||
|
},
|
||||||
|
rsc: true,
|
||||||
|
tsx: true,
|
||||||
|
aliases: {
|
||||||
|
components: "@/components",
|
||||||
|
utils: "@/lib/utils",
|
||||||
|
ui: "@/components/ui",
|
||||||
|
lib: "@/lib",
|
||||||
|
hooks: "@/hooks",
|
||||||
|
},
|
||||||
|
resolvedPaths: {
|
||||||
|
cwd: "/test",
|
||||||
|
tailwindConfig: "/test/tailwind.config.js",
|
||||||
|
tailwindCss: "/test/app/globals.css",
|
||||||
|
utils: "/test/lib/utils",
|
||||||
|
components: "/test/components",
|
||||||
|
ui: "/test/components/ui",
|
||||||
|
lib: "/test/lib",
|
||||||
|
hooks: "/test/hooks",
|
||||||
|
},
|
||||||
|
registries: {
|
||||||
|
...BUILTIN_REGISTRIES,
|
||||||
|
"@foo": "https://foo.com/registry/{name}",
|
||||||
|
"@bar": "https://bar.com/registry/{name}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should discover namespaces from direct components", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems.mockResolvedValue([
|
||||||
|
{ name: "button", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@foo/button", "@bar/card"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@foo", "@bar"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip built-in registries", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems.mockResolvedValue([
|
||||||
|
{ name: "button", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@shadcn/button", "@foo/card"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@foo"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should discover namespaces from registry dependencies", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "dialog",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@bar/button", "@baz/modal"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "button", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "modal", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@foo/dialog"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toContain("@foo")
|
||||||
|
expect(namespaces).toContain("@bar")
|
||||||
|
expect(namespaces).toContain("@baz")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle circular dependencies", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "comp-a",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@bar/comp-b"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "comp-b",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@foo/comp-a"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@foo/comp-a"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@foo", "@bar"])
|
||||||
|
// Should only fetch each component once despite circular reference.
|
||||||
|
expect(mockFetchRegistryItems).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle RegistryNotConfiguredError gracefully", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems.mockRejectedValue(
|
||||||
|
new RegistryNotConfiguredError("@unknown")
|
||||||
|
)
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@unknown/button"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@unknown"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should continue processing on other errors", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockRejectedValueOnce(new Error("Network error"))
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "card", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@foo/button", "@bar/card"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should still discover both @foo and @bar.
|
||||||
|
// @foo from the initial parse, @bar from successful fetch.
|
||||||
|
expect(namespaces).toContain("@foo")
|
||||||
|
expect(namespaces).toContain("@bar")
|
||||||
|
expect(namespaces).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle deeply nested dependencies", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "level-1",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@level2/component"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@level3/deep"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "deep",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@level1/level-1"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@level1", "@level2", "@level3"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return unique namespaces", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "comp-a",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@foo/shared", "@bar/shared"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "shared", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "shared", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["@foo/comp-a", "@foo/comp-b", "@bar/comp-c"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should not have duplicate @foo.
|
||||||
|
expect(namespaces).toEqual(["@foo", "@bar"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle components without namespace", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems.mockResolvedValue([
|
||||||
|
{ name: "button", type: "registry:ui", files: [], dependencies: [] },
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["button", "@foo/card"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual(["@foo"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle empty input", async () => {
|
||||||
|
const namespaces = await resolveRegistryNamespaces([], mockConfig)
|
||||||
|
|
||||||
|
expect(namespaces).toEqual([])
|
||||||
|
expect(resolver.fetchRegistryItems).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should discover namespaces from components without namespaces but with registryDependencies", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "my-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@foo/dep1", "@bar/dep2"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "dep1",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "dep2",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["button"], // Component without namespace
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should discover namespaces from registryDependencies even though "button" has no namespace.
|
||||||
|
expect(namespaces).toEqual(["@foo", "@bar"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should discover namespaces from URL components with registryDependencies", async () => {
|
||||||
|
const mockFetchRegistryItems = vi.mocked(resolver.fetchRegistryItems)
|
||||||
|
mockFetchRegistryItems
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "to-8bitcn",
|
||||||
|
type: "registry:item",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
registryDependencies: ["@8bitcn/button"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "branch",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
name: "button",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const namespaces = await resolveRegistryNamespaces(
|
||||||
|
["https://api.npoint.io/2e006917dca7f7367495", "@ai-elements/branch"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(namespaces).toContain("@8bitcn")
|
||||||
|
expect(namespaces).toContain("@ai-elements")
|
||||||
|
|
||||||
|
// Verify fetchRegistryItems was called with the correct arguments.
|
||||||
|
expect(mockFetchRegistryItems).toHaveBeenCalledWith(
|
||||||
|
["https://api.npoint.io/2e006917dca7f7367495"],
|
||||||
|
mockConfig,
|
||||||
|
{ useCache: true }
|
||||||
|
)
|
||||||
|
expect(mockFetchRegistryItems).toHaveBeenCalledWith(
|
||||||
|
["@ai-elements/branch"],
|
||||||
|
mockConfig,
|
||||||
|
{ useCache: true }
|
||||||
|
)
|
||||||
|
expect(mockFetchRegistryItems).toHaveBeenCalledWith(
|
||||||
|
["@8bitcn/button"],
|
||||||
|
mockConfig,
|
||||||
|
{ useCache: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("checkNamespaceConfiguration", () => {
|
||||||
|
const mockConfig: Config = {
|
||||||
|
style: "default",
|
||||||
|
tailwind: {
|
||||||
|
config: "tailwind.config.js",
|
||||||
|
css: "app/globals.css",
|
||||||
|
baseColor: "slate",
|
||||||
|
cssVariables: true,
|
||||||
|
},
|
||||||
|
rsc: true,
|
||||||
|
tsx: true,
|
||||||
|
aliases: {
|
||||||
|
components: "@/components",
|
||||||
|
utils: "@/lib/utils",
|
||||||
|
ui: "@/components/ui",
|
||||||
|
lib: "@/lib",
|
||||||
|
hooks: "@/hooks",
|
||||||
|
},
|
||||||
|
resolvedPaths: {
|
||||||
|
cwd: "/test",
|
||||||
|
tailwindConfig: "/test/tailwind.config.js",
|
||||||
|
tailwindCss: "/test/app/globals.css",
|
||||||
|
utils: "/test/lib/utils",
|
||||||
|
components: "/test/components",
|
||||||
|
ui: "/test/components/ui",
|
||||||
|
lib: "/test/lib",
|
||||||
|
hooks: "/test/hooks",
|
||||||
|
},
|
||||||
|
registries: {
|
||||||
|
...BUILTIN_REGISTRIES,
|
||||||
|
"@foo": "https://foo.com/registry/{name}",
|
||||||
|
"@bar": "https://bar.com/registry/{name}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should identify configured namespaces", () => {
|
||||||
|
const result = checkNamespaceConfiguration(["@foo", "@bar"], mockConfig)
|
||||||
|
|
||||||
|
expect(result.configured).toEqual(["@foo", "@bar"])
|
||||||
|
expect(result.missing).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should identify missing namespaces", () => {
|
||||||
|
const result = checkNamespaceConfiguration(
|
||||||
|
["@foo", "@unknown", "@missing"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.configured).toEqual(["@foo"])
|
||||||
|
expect(result.missing).toEqual(["@unknown", "@missing"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle built-in registries as configured", () => {
|
||||||
|
const result = checkNamespaceConfiguration(["@shadcn", "@foo"], mockConfig)
|
||||||
|
|
||||||
|
expect(result.configured).toEqual(["@shadcn", "@foo"])
|
||||||
|
expect(result.missing).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle empty input", () => {
|
||||||
|
const result = checkNamespaceConfiguration([], mockConfig)
|
||||||
|
|
||||||
|
expect(result.configured).toEqual([])
|
||||||
|
expect(result.missing).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle config without registries", () => {
|
||||||
|
const configWithoutRegistries: Config = {
|
||||||
|
...mockConfig,
|
||||||
|
registries: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = checkNamespaceConfiguration(
|
||||||
|
["@foo", "@bar"],
|
||||||
|
configWithoutRegistries
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.configured).toEqual([])
|
||||||
|
expect(result.missing).toEqual(["@foo", "@bar"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle mixed configured and missing namespaces", () => {
|
||||||
|
const result = checkNamespaceConfiguration(
|
||||||
|
["@shadcn", "@foo", "@unknown", "@bar", "@missing"],
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.configured).toContain("@shadcn")
|
||||||
|
expect(result.configured).toContain("@foo")
|
||||||
|
expect(result.configured).toContain("@bar")
|
||||||
|
expect(result.missing).toContain("@unknown")
|
||||||
|
expect(result.missing).toContain("@missing")
|
||||||
|
})
|
||||||
|
})
|
||||||
63
packages/shadcn/src/registry/namespaces.ts
Normal file
63
packages/shadcn/src/registry/namespaces.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
|
||||||
|
import { RegistryNotConfiguredError } from "@/src/registry/errors"
|
||||||
|
import { parseRegistryAndItemFromString } from "@/src/registry/parser"
|
||||||
|
import { fetchRegistryItems } from "@/src/registry/resolver"
|
||||||
|
import { Config } from "@/src/utils/get-config"
|
||||||
|
|
||||||
|
// Recursively discovers all registry namespaces including nested ones.
|
||||||
|
export async function resolveRegistryNamespaces(
|
||||||
|
components: string[],
|
||||||
|
config: Config
|
||||||
|
) {
|
||||||
|
const discoveredNamespaces = new Set<string>()
|
||||||
|
const visitedItems = new Set<string>()
|
||||||
|
const itemsToProcess = [...components]
|
||||||
|
|
||||||
|
while (itemsToProcess.length > 0) {
|
||||||
|
const currentItem = itemsToProcess.shift()!
|
||||||
|
|
||||||
|
if (visitedItems.has(currentItem)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visitedItems.add(currentItem)
|
||||||
|
|
||||||
|
const { registry } = parseRegistryAndItemFromString(currentItem)
|
||||||
|
if (registry && !BUILTIN_REGISTRIES[registry]) {
|
||||||
|
discoveredNamespaces.add(registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [item] = await fetchRegistryItems([currentItem], config, {
|
||||||
|
useCache: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (item?.registryDependencies) {
|
||||||
|
for (const dep of item.registryDependencies) {
|
||||||
|
const { registry: depRegistry } = parseRegistryAndItemFromString(dep)
|
||||||
|
if (depRegistry && !BUILTIN_REGISTRIES[depRegistry]) {
|
||||||
|
discoveredNamespaces.add(depRegistry)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visitedItems.has(dep)) {
|
||||||
|
itemsToProcess.push(dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If a registry is not configured, we still track it.
|
||||||
|
if (error instanceof RegistryNotConfiguredError) {
|
||||||
|
const { registry } = parseRegistryAndItemFromString(currentItem)
|
||||||
|
if (registry && !BUILTIN_REGISTRIES[registry]) {
|
||||||
|
discoveredNamespaces.add(registry)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors (network, parsing, etc.), we skip this item
|
||||||
|
// but continue processing others to discover as many namespaces as possible.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(discoveredNamespaces)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { fetchRegistries } from "@/src/registry/api"
|
import { fetchRegistries } from "@/src/registry/api"
|
||||||
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
|
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
|
||||||
import { parseRegistryAndItemFromString } from "@/src/registry/parser"
|
import { resolveRegistryNamespaces } from "@/src/registry/namespaces"
|
||||||
import { rawConfigSchema } from "@/src/registry/schema"
|
import { rawConfigSchema } from "@/src/registry/schema"
|
||||||
import { Config } from "@/src/utils/get-config"
|
import { Config } from "@/src/utils/get-config"
|
||||||
import { spinner } from "@/src/utils/spinner"
|
import { spinner } from "@/src/utils/spinner"
|
||||||
@@ -21,16 +21,10 @@ export async function ensureRegistriesInConfig(
|
|||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryNames = new Set<string>()
|
// Use resolveRegistryNamespaces to discover all namespaces including dependencies.
|
||||||
|
const registryNames = await resolveRegistryNamespaces(components, config)
|
||||||
|
|
||||||
for (const component of components) {
|
const missingRegistries = registryNames.filter(
|
||||||
const { registry } = parseRegistryAndItemFromString(component)
|
|
||||||
if (registry) {
|
|
||||||
registryNames.add(registry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingRegistries = Array.from(registryNames).filter(
|
|
||||||
(registry) =>
|
(registry) =>
|
||||||
!config.registries?.[registry] &&
|
!config.registries?.[registry] &&
|
||||||
!Object.keys(BUILTIN_REGISTRIES).includes(registry)
|
!Object.keys(BUILTIN_REGISTRIES).includes(registry)
|
||||||
|
|||||||
@@ -18,12 +18,8 @@ export async function createRegistryServer(
|
|||||||
// Handle registries.json endpoint (don't strip .json for this one)
|
// Handle registries.json endpoint (don't strip .json for this one)
|
||||||
if (urlRaw?.endsWith("/registries.json")) {
|
if (urlRaw?.endsWith("/registries.json")) {
|
||||||
response.writeHead(200, { "Content-Type": "application/json" })
|
response.writeHead(200, { "Content-Type": "application/json" })
|
||||||
response.end(
|
// Return empty registry index for tests - we want to test manual configuration.
|
||||||
JSON.stringify({
|
response.end(JSON.stringify({}))
|
||||||
"@one": `http://localhost:${port}${path}/{name}`,
|
|
||||||
"@two": `http://localhost:5555/registry/{name}`,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -325,7 +325,7 @@ importers:
|
|||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.1
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
shiki:
|
shiki:
|
||||||
specifier: ^1.10.1
|
specifier: ^1.10.1
|
||||||
@@ -605,7 +605,7 @@ importers:
|
|||||||
specifier: 2.12.7
|
specifier: 2.12.7
|
||||||
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.1
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.32.6
|
specifier: ^0.32.6
|
||||||
|
|||||||
Reference in New Issue
Block a user