snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
492
packages/shadcn/src/registry/builder.test.ts
Normal file
492
packages/shadcn/src/registry/builder.test.ts
Normal file
@@ -0,0 +1,492 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import { REGISTRY_URL } from "@/src/registry/constants"
|
||||
import { RegistryNotConfiguredError } from "@/src/registry/errors"
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
buildHeadersFromRegistryConfig,
|
||||
buildUrlAndHeadersForRegistryItem,
|
||||
buildUrlFromRegistryConfig,
|
||||
resolveRegistryUrl,
|
||||
} from "./builder"
|
||||
|
||||
describe("buildUrlFromRegistryConfig", () => {
|
||||
beforeEach(() => {
|
||||
process.env.TEST_TOKEN = "abc123"
|
||||
process.env.API_VERSION = "v2"
|
||||
process.env.API_KEY = "key456"
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_TOKEN
|
||||
delete process.env.API_VERSION
|
||||
delete process.env.API_KEY
|
||||
})
|
||||
|
||||
it("should build URL from string config", () => {
|
||||
const url = buildUrlFromRegistryConfig(
|
||||
"chat-component",
|
||||
"https://v0.dev/chat/b/{name}/json"
|
||||
)
|
||||
expect(url).toBe("https://v0.dev/chat/b/chat-component/json")
|
||||
})
|
||||
|
||||
it("should replace style placeholder in URL", () => {
|
||||
const url = buildUrlFromRegistryConfig(
|
||||
"button",
|
||||
"https://ui.shadcn.com/r/styles/{style}/{name}.json",
|
||||
{ style: "new-york" } as any
|
||||
)
|
||||
expect(url).toBe("https://ui.shadcn.com/r/styles/new-york/button.json")
|
||||
})
|
||||
|
||||
it("should handle both name and style placeholders", () => {
|
||||
const url = buildUrlFromRegistryConfig(
|
||||
"accordion",
|
||||
"https://example.com/{style}/components/{name}",
|
||||
{ style: "default" } as any
|
||||
)
|
||||
expect(url).toBe("https://example.com/default/components/accordion")
|
||||
})
|
||||
|
||||
it("should build URL with env vars", () => {
|
||||
const url = buildUrlFromRegistryConfig(
|
||||
"button",
|
||||
"https://api.com/{name}?token=${TEST_TOKEN}"
|
||||
)
|
||||
expect(url).toBe("https://api.com/button?token=abc123")
|
||||
})
|
||||
|
||||
it("should build URL with params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
version: "${API_VERSION}",
|
||||
format: "json",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table?version=v2&format=json")
|
||||
})
|
||||
|
||||
it("should skip empty param values", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
token: "${MISSING_VAR}",
|
||||
format: "json",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table?format=json")
|
||||
})
|
||||
|
||||
it("should handle existing query params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}?existing=true",
|
||||
params: {
|
||||
new: "param",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table?existing=true&new=param")
|
||||
})
|
||||
|
||||
it("should handle URL with no params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table")
|
||||
})
|
||||
|
||||
it("should handle multiple env vars in params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
token: "${TEST_TOKEN}",
|
||||
version: "${API_VERSION}",
|
||||
key: "${API_KEY}",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table?token=abc123&version=v2&key=key456")
|
||||
})
|
||||
|
||||
it("should handle all empty env vars in params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
token: "${MISSING_VAR1}",
|
||||
key: "${MISSING_VAR2}",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table")
|
||||
})
|
||||
|
||||
it("should handle mixed static and env var params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
static: "value",
|
||||
env: "${TEST_TOKEN}",
|
||||
empty: "${MISSING_VAR}",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe("https://api.com/table?static=value&env=abc123")
|
||||
})
|
||||
|
||||
it("should handle special characters in params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
params: {
|
||||
"user-id": "123",
|
||||
"api-key": "${TEST_TOKEN}",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe(
|
||||
"https://api.com/table?user-id=123&api-key=abc123&content-type=application%2Fjson"
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle URL with multiple existing query params", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}?param1=value1¶m2=value2",
|
||||
params: {
|
||||
newParam: "newValue",
|
||||
envParam: "${TEST_TOKEN}",
|
||||
},
|
||||
}
|
||||
|
||||
const url = buildUrlFromRegistryConfig("table", config)
|
||||
expect(url).toBe(
|
||||
"https://api.com/table?param1=value1¶m2=value2&newParam=newValue&envParam=abc123"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildHeadersFromRegistryConfig", () => {
|
||||
beforeEach(() => {
|
||||
process.env.AUTH_TOKEN = "secret123"
|
||||
process.env.CLIENT_ID = "client456"
|
||||
process.env.API_KEY = "key789"
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.AUTH_TOKEN
|
||||
delete process.env.CLIENT_ID
|
||||
delete process.env.API_KEY
|
||||
})
|
||||
|
||||
it("should return empty object for string config", () => {
|
||||
expect(buildHeadersFromRegistryConfig("https://api.com/{name}")).toEqual({})
|
||||
})
|
||||
|
||||
it("should return empty object for config without headers", () => {
|
||||
expect(
|
||||
buildHeadersFromRegistryConfig({ url: "https://api.com/{name}" })
|
||||
).toEqual({})
|
||||
})
|
||||
|
||||
it("should expand headers with env vars", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer ${AUTH_TOKEN}",
|
||||
"X-Client-Id": "${CLIENT_ID}",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
Authorization: "Bearer secret123",
|
||||
"X-Client-Id": "client456",
|
||||
})
|
||||
})
|
||||
|
||||
it("should skip headers with empty values", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer ${MISSING_VAR}",
|
||||
"X-Client-Id": "${CLIENT_ID}",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-Client-Id": "client456",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle headers with mixed static and env var content", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer ${AUTH_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
"X-Custom": "prefix-${CLIENT_ID}-suffix",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
Authorization: "Bearer secret123",
|
||||
"Content-Type": "application/json",
|
||||
"X-Custom": "prefix-client456-suffix",
|
||||
})
|
||||
})
|
||||
|
||||
it("should skip headers with only env vars that are empty", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"X-Missing": "${MISSING_VAR1}",
|
||||
"X-Also-Missing": "${MISSING_VAR2}",
|
||||
"X-Present": "${CLIENT_ID}",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-Present": "client456",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle headers with whitespace-only values", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"X-Empty": " ",
|
||||
"X-Whitespace": " ${MISSING_VAR} ",
|
||||
"X-Valid": " ${CLIENT_ID} ",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-Valid": " client456 ",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle complex env var patterns", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"X-Complex": "Bearer ${AUTH_TOKEN} with ${CLIENT_ID} and ${API_KEY}",
|
||||
"X-Simple": "${CLIENT_ID}",
|
||||
"X-Mixed": "static-${AUTH_TOKEN}-${MISSING_VAR}-${API_KEY}",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-Complex": "Bearer secret123 with client456 and key789",
|
||||
"X-Simple": "client456",
|
||||
"X-Mixed": "static-secret123--key789",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle headers with only static content", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "shadcn-ui/1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "shadcn-ui/1.0.0",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle headers with template-like content but no env vars", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"X-Template": "This is a template ${but not an env var}",
|
||||
"X-Regular": "Regular header value",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-Template": "This is a template ${but not an env var}",
|
||||
"X-Regular": "Regular header value",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle case where env var expansion doesn't change the value", () => {
|
||||
const config = {
|
||||
url: "https://api.com/{name}",
|
||||
headers: {
|
||||
"X-No-Change": "static value",
|
||||
"X-With-Env": "prefix-${CLIENT_ID}-suffix",
|
||||
},
|
||||
}
|
||||
|
||||
expect(buildHeadersFromRegistryConfig(config)).toEqual({
|
||||
"X-No-Change": "static value",
|
||||
"X-With-Env": "prefix-client456-suffix",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildUrlAndHeadersForRegistryItem", () => {
|
||||
it("should resolve non-registry items through @shadcn registry", () => {
|
||||
const input = "button"
|
||||
const config = {} as any
|
||||
// Non-prefixed items are resolved through the built-in @shadcn registry
|
||||
expect(buildUrlAndHeadersForRegistryItem(input, config)).toEqual({
|
||||
url: "https://ui.shadcn.com/r/styles/{style}/button.json",
|
||||
headers: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw error for unknown registry", () => {
|
||||
try {
|
||||
buildUrlAndHeadersForRegistryItem("@unknown/button", {} as any)
|
||||
expect.fail("Should have thrown RegistryNotConfiguredError")
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RegistryNotConfiguredError)
|
||||
if (error instanceof RegistryNotConfiguredError) {
|
||||
expect(error.message).toContain(
|
||||
'defined under "registries" in your components.json or package.json file'
|
||||
)
|
||||
expect(error.suggestion).toBe(
|
||||
'Add the registry configuration under "registries" in your components.json or package.json file.'
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should resolve registry items with string config", () => {
|
||||
const config = {
|
||||
registries: {
|
||||
"@v0": "https://v0.dev/chat/b/{name}/json",
|
||||
},
|
||||
} as any
|
||||
|
||||
const result1 = buildUrlAndHeadersForRegistryItem("@v0/button", config)
|
||||
expect(result1).toEqual({
|
||||
url: "https://v0.dev/chat/b/button/json",
|
||||
headers: {},
|
||||
})
|
||||
|
||||
const result2 = buildUrlAndHeadersForRegistryItem("@v0/data-table", config)
|
||||
expect(result2).toEqual({
|
||||
url: "https://v0.dev/chat/b/data-table/json",
|
||||
headers: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should resolve registry items with object config", () => {
|
||||
const config = {
|
||||
registries: {
|
||||
"@test": {
|
||||
url: "https://api.com/{name}.json",
|
||||
headers: {
|
||||
Authorization: "Bearer token123",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = buildUrlAndHeadersForRegistryItem("@test/button", config)
|
||||
expect(result).toEqual({
|
||||
url: "https://api.com/button.json",
|
||||
headers: {
|
||||
Authorization: "Bearer token123",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle environment variables in config", () => {
|
||||
process.env.TEST_TOKEN = "abc123"
|
||||
process.env.API_URL = "https://api.com"
|
||||
|
||||
const config = {
|
||||
registries: {
|
||||
"@env": {
|
||||
url: "${API_URL}/{name}.json",
|
||||
headers: {
|
||||
Authorization: "Bearer ${TEST_TOKEN}",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = buildUrlAndHeadersForRegistryItem("@env/button", config)
|
||||
expect(result).toEqual({
|
||||
url: "https://api.com/button.json",
|
||||
headers: {
|
||||
Authorization: "Bearer abc123",
|
||||
},
|
||||
})
|
||||
|
||||
delete process.env.TEST_TOKEN
|
||||
delete process.env.API_URL
|
||||
})
|
||||
|
||||
it("should handle complex item paths", () => {
|
||||
const config = {
|
||||
registries: {
|
||||
"@acme": "https://api.com/{name}.json",
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = buildUrlAndHeadersForRegistryItem("@acme/ui/button", config)
|
||||
expect(result).toEqual({
|
||||
url: "https://api.com/ui/button.json",
|
||||
headers: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle URLs and local files", () => {
|
||||
const config = { registries: {} } as any
|
||||
|
||||
// URLs should return null (not registry items)
|
||||
expect(
|
||||
buildUrlAndHeadersForRegistryItem("https://example.com/button", config)
|
||||
).toBeNull()
|
||||
|
||||
// Local files should return null (not registry items)
|
||||
expect(
|
||||
buildUrlAndHeadersForRegistryItem("./local/button", config)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveRegistryUrl", () => {
|
||||
it("should return the URL as-is for valid URLs", () => {
|
||||
const url = "https://example.com/component.json"
|
||||
expect(resolveRegistryUrl(url)).toBe(url)
|
||||
})
|
||||
|
||||
it("should append /json to v0 registry URLs", () => {
|
||||
const v0Url = "https://v0.dev/chat/b/abc123"
|
||||
expect(resolveRegistryUrl(v0Url)).toBe("https://v0.dev/chat/b/abc123/json")
|
||||
})
|
||||
|
||||
it("should not append /json if already present", () => {
|
||||
const v0Url = "https://v0.dev/chat/b/abc123/json"
|
||||
expect(resolveRegistryUrl(v0Url)).toBe(v0Url)
|
||||
})
|
||||
|
||||
it("should prepend REGISTRY_URL for non-URLs", () => {
|
||||
expect(resolveRegistryUrl("test.json")).toBe(`${REGISTRY_URL}/test.json`)
|
||||
expect(resolveRegistryUrl("styles/default/button.json")).toBe(
|
||||
`${REGISTRY_URL}/styles/default/button.json`
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user