493 lines
14 KiB
TypeScript
493 lines
14 KiB
TypeScript
/* 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`
|
|
)
|
|
})
|
|
})
|