Skip to content

Commit fc6d909

Browse files
authored
add getRegistriesIndex (#8216)
* feat: add getRegistriesIndex * chore: changeset * fix: formatting
1 parent 590b9be commit fc6d909

File tree

6 files changed

+158
-12
lines changed

6 files changed

+158
-12
lines changed

.changeset/tangy-spiders-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": minor
3+
---
4+
5+
add getRegistriesIndex

packages/shadcn/src/registry/api.test.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ import {
2727
} from "vitest"
2828
import { z } from "zod"
2929

30-
import { getRegistriesConfig, getRegistry, getRegistryItems } from "./api"
30+
import {
31+
getRegistriesConfig,
32+
getRegistriesIndex,
33+
getRegistry,
34+
getRegistryItems,
35+
} from "./api"
36+
import { RegistriesIndexParseError } from "./errors"
3137

3238
vi.mock("@/src/utils/handle-error", () => ({
3339
handleError: vi.fn(),
@@ -96,6 +102,13 @@ const server = setupServer(
96102
},
97103
],
98104
})
105+
}),
106+
http.get(`${REGISTRY_URL}/registries.json`, () => {
107+
return HttpResponse.json({
108+
"@shadcn": "https://ui.shadcn.com/r/styles/{style}/{name}.json",
109+
"@example": "https://example.com/registry/styles/{style}/{name}.json",
110+
"@test": "https://test.com/registry/{name}.json",
111+
})
99112
})
100113
)
101114

@@ -1650,4 +1663,75 @@ describe("getRegistriesConfig", () => {
16501663
}
16511664
})
16521665
})
1666+
1667+
describe("getRegistriesIndex", () => {
1668+
it("should fetch and parse the registries index successfully", async () => {
1669+
const result = await getRegistriesIndex()
1670+
1671+
expect(result).toEqual({
1672+
"@shadcn": "https://ui.shadcn.com/r/styles/{style}/{name}.json",
1673+
"@example": "https://example.com/registry/styles/{style}/{name}.json",
1674+
"@test": "https://test.com/registry/{name}.json",
1675+
})
1676+
})
1677+
1678+
it("should respect cache options", async () => {
1679+
// Test with cache disabled
1680+
const result1 = await getRegistriesIndex({ useCache: false })
1681+
expect(result1).toBeDefined()
1682+
1683+
// Test with cache enabled
1684+
const result2 = await getRegistriesIndex({ useCache: true })
1685+
expect(result2).toBeDefined()
1686+
1687+
// Results should be the same
1688+
expect(result1).toEqual(result2)
1689+
})
1690+
1691+
it("should use default cache behavior when no options provided", async () => {
1692+
const result = await getRegistriesIndex()
1693+
expect(result).toBeDefined()
1694+
expect(typeof result).toBe("object")
1695+
})
1696+
1697+
it("should handle network errors properly", async () => {
1698+
server.use(
1699+
http.get(`${REGISTRY_URL}/registries.json`, () => {
1700+
return new HttpResponse(null, { status: 500 })
1701+
})
1702+
)
1703+
1704+
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow()
1705+
1706+
try {
1707+
await getRegistriesIndex({ useCache: false })
1708+
} catch (error) {
1709+
expect(error).not.toBeInstanceOf(RegistriesIndexParseError)
1710+
}
1711+
})
1712+
1713+
it("should handle invalid JSON response", async () => {
1714+
server.use(
1715+
http.get(`${REGISTRY_URL}/registries.json`, () => {
1716+
return HttpResponse.json({
1717+
"invalid-namespace": "some-url",
1718+
})
1719+
})
1720+
)
1721+
1722+
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow(
1723+
RegistriesIndexParseError
1724+
)
1725+
})
1726+
1727+
it("should handle network timeout", async () => {
1728+
server.use(
1729+
http.get(`${REGISTRY_URL}/registries.json`, () => {
1730+
return HttpResponse.error()
1731+
})
1732+
)
1733+
1734+
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow()
1735+
})
1736+
})
16531737
})

packages/shadcn/src/registry/api.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "@/src/registry/context"
1313
import {
1414
ConfigParseError,
15+
RegistriesIndexParseError,
1516
RegistryInvalidNamespaceError,
1617
RegistryNotFoundError,
1718
RegistryParseError,
@@ -277,15 +278,24 @@ export async function getItemTargetPath(
277278
)
278279
}
279280

280-
export async function fetchRegistries() {
281+
export async function getRegistriesIndex(options?: { useCache?: boolean }) {
282+
options = {
283+
useCache: true,
284+
...options,
285+
}
286+
287+
const url = `${REGISTRY_URL}/registries.json`
288+
const [data] = await fetchRegistry([url], {
289+
useCache: options.useCache,
290+
})
291+
281292
try {
282-
// TODO: Do we want this inside /r?
283-
const url = `${REGISTRY_URL}/registries.json`
284-
const [data] = await fetchRegistry([url], {
285-
useCache: process.env.NODE_ENV !== "development",
286-
})
287293
return registriesIndexSchema.parse(data)
288-
} catch {
289-
return null
294+
} catch (error) {
295+
if (error instanceof z.ZodError) {
296+
throw new RegistriesIndexParseError(error)
297+
}
298+
299+
throw error
290300
}
291301
}

packages/shadcn/src/registry/errors.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,41 @@ export class ConfigParseError extends RegistryError {
285285
this.name = "ConfigParseError"
286286
}
287287
}
288+
289+
export class RegistriesIndexParseError extends RegistryError {
290+
public readonly parseError: unknown
291+
292+
constructor(parseError: unknown) {
293+
let message = "Failed to parse registries index"
294+
295+
if (parseError instanceof z.ZodError) {
296+
const invalidNamespaces = parseError.errors
297+
.filter((e) => e.path.length > 0)
298+
.map((e) => `"${e.path[0]}"`)
299+
.filter((v, i, arr) => arr.indexOf(v) === i) // remove duplicates
300+
301+
if (invalidNamespaces.length > 0) {
302+
message = `Failed to parse registries index. Invalid registry namespace(s): ${invalidNamespaces.join(
303+
", "
304+
)}\n${parseError.errors
305+
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
306+
.join("\n")}`
307+
} else {
308+
message = `Failed to parse registries index:\n${parseError.errors
309+
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
310+
.join("\n")}`
311+
}
312+
}
313+
314+
super(message, {
315+
code: RegistryErrorCode.PARSE_ERROR,
316+
cause: parseError,
317+
context: { parseError },
318+
suggestion:
319+
"The registries index may be corrupted or have invalid registry namespace format. Registry names must start with @ (e.g., @shadcn, @example).",
320+
})
321+
322+
this.parseError = parseError
323+
this.name = "RegistriesIndexParseError"
324+
}
325+
}

packages/shadcn/src/registry/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export { getRegistryItems, resolveRegistryItems, getRegistry } from "./api"
1+
export {
2+
getRegistryItems,
3+
resolveRegistryItems,
4+
getRegistry,
5+
getRegistriesIndex,
6+
} from "./api"
27

38
export { searchRegistries } from "./search"
49

@@ -11,6 +16,7 @@ export {
1116
RegistryNotConfiguredError,
1217
RegistryLocalFileError,
1318
RegistryParseError,
19+
RegistriesIndexParseError,
1420
RegistryMissingEnvironmentVariablesError,
1521
RegistryInvalidNamespaceError,
1622
} from "./errors"

packages/shadcn/src/utils/registries.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path"
2-
import { fetchRegistries } from "@/src/registry/api"
2+
import { getRegistriesIndex } from "@/src/registry/api"
33
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
44
import { resolveRegistryNamespaces } from "@/src/registry/namespaces"
55
import { rawConfigSchema } from "@/src/registry/schema"
@@ -39,7 +39,10 @@ export async function ensureRegistriesInConfig(
3939

4040
// We'll fail silently if we can't fetch the registry index.
4141
// The error handling by caller will guide user to add the missing registries.
42-
const registryIndex = await fetchRegistries()
42+
const registryIndex = await getRegistriesIndex({
43+
useCache: process.env.NODE_ENV !== "development",
44+
})
45+
4346
if (!registryIndex) {
4447
return {
4548
config,

0 commit comments

Comments
 (0)