fix(shadcn): resolver for url (#9054)

This commit is contained in:
shadcn
2025-12-14 02:16:26 +04:00
committed by GitHub
parent 46bf4a0f06
commit d3156c09ae
12 changed files with 72 additions and 98 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
fix resolver for url

View File

@@ -7,6 +7,11 @@ on:
types: [labeled]
branches:
- main
permissions:
id-token: write
contents: read
jobs:
prerelease:
if: |
@@ -18,7 +23,7 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -28,22 +33,21 @@ jobs:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Authenticate to NPM
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> packages/shadcn/.npmrc
env:
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Publish Beta to NPM
run: pnpm pub:beta

View File

@@ -12,7 +12,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/shadcn/ui.git",
"url": "https://github.com/shadcn-ui/ui.git",
"directory": "packages/shadcn"
},
"files": [

View File

@@ -230,7 +230,7 @@ export const init = new Command()
item.extends === "none" ? false : options.baseStyle
}
if (item?.type === "registry:style" && !options.baseStyle) {
if (item?.type === "registry:style") {
// Set a default base color so we're not prompted.
// The style will extend or override it.
options.baseColor = "neutral"

View File

@@ -342,16 +342,12 @@ describe("buildHeadersFromRegistryConfig", () => {
})
describe("buildUrlAndHeadersForRegistryItem", () => {
it("should default to @shadcn registry for non-registry items", () => {
it("should resolve non-registry items through @shadcn registry", () => {
const input = "button"
const config = {
registries: {
"@shadcn": "https://ui.shadcn.com/r/{name}.json",
},
} as any
const result = buildUrlAndHeadersForRegistryItem(input, config)
expect(result).toEqual({
url: "https://ui.shadcn.com/r/button.json",
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: {},
})
})
@@ -362,14 +358,6 @@ describe("buildUrlAndHeadersForRegistryItem", () => {
}).toThrow('Unknown registry "@unknown"')
})
it("should throw error when @shadcn is not configured for non-registry items", () => {
const input = "button"
const config = {} as any
expect(() => {
buildUrlAndHeadersForRegistryItem(input, config)
}).toThrow('Unknown registry "@shadcn"')
})
it("should resolve registry items with string config", () => {
const config = {
registries: {
@@ -452,46 +440,18 @@ describe("buildUrlAndHeadersForRegistryItem", () => {
})
})
it("should default to @shadcn registry for URLs and local files", () => {
const config = {
registries: {
"@shadcn": "https://ui.shadcn.com/r/{name}.json",
},
} as any
// URLs default to @shadcn registry
const urlResult = buildUrlAndHeadersForRegistryItem(
"https://example.com/button",
config
)
expect(urlResult).toEqual({
url: "https://ui.shadcn.com/r/https://example.com/button.json",
headers: {},
})
// Local files default to @shadcn registry
const localResult = buildUrlAndHeadersForRegistryItem(
"./local/button",
config
)
expect(localResult).toEqual({
url: "https://ui.shadcn.com/r/./local/button.json",
headers: {},
})
})
it("should throw error when @shadcn is not configured for URLs and local files", () => {
it("should handle URLs and local files", () => {
const config = { registries: {} } as any
// URLs should throw error when @shadcn is not configured
expect(() => {
// URLs should return null (not registry items)
expect(
buildUrlAndHeadersForRegistryItem("https://example.com/button", config)
}).toThrow('Unknown registry "@shadcn"')
).toBeNull()
// Local files should throw error when @shadcn is not configured
expect(() => {
// Local files should return null (not registry items)
expect(
buildUrlAndHeadersForRegistryItem("./local/button", config)
}).toThrow('Unknown registry "@shadcn"')
).toBeNull()
})
})

View File

@@ -1,8 +1,8 @@
import { REGISTRY_URL } from "@/src/registry/constants"
import { BUILTIN_REGISTRIES, REGISTRY_URL } from "@/src/registry/constants"
import { expandEnvVars } from "@/src/registry/env"
import { RegistryNotConfiguredError } from "@/src/registry/errors"
import { parseRegistryAndItemFromString } from "@/src/registry/parser"
import { isUrl } from "@/src/registry/utils"
import { isLocalFile, isUrl } from "@/src/registry/utils"
import { validateRegistryConfig } from "@/src/registry/validator"
import { registryConfigItemSchema } from "@/src/schema"
import { Config } from "@/src/utils/get-config"
@@ -14,17 +14,26 @@ const ENV_VAR_PATTERN = /\${(\w+)}/g
const QUERY_PARAM_SEPARATOR = "?"
const QUERY_PARAM_DELIMITER = "&"
function isLocalPath(path: string) {
return path.startsWith("./") || path.startsWith("/")
}
export function buildUrlAndHeadersForRegistryItem(
name: string,
config?: Config
) {
let { registry, item } = parseRegistryAndItemFromString(name)
// If no registry prefix, check if it's a URL or local path.
// These should be handled directly, not through a registry.
if (!registry) {
if (isUrl(name) || isLocalFile(name) || isLocalPath(name)) {
return null
}
registry = "@shadcn"
}
const registries = config?.registries || {}
const registries = { ...BUILTIN_REGISTRIES, ...config?.registries }
const registryConfig = registries[registry]
if (!registryConfig) {
throw new RegistryNotConfiguredError(registry)

View File

@@ -65,13 +65,18 @@ describe("resolveRegistryItemsFromRegistries", () => {
expect(setRegistryHeaders).toHaveBeenCalledWith({})
})
it("should return non-registry items unchanged", () => {
it("should resolve non-registry items through @shadcn registry", () => {
const items = ["button", "card", "dialog"]
const config = { registries: {} } as any
const result = resolveRegistryItemsFromRegistries(items, config)
expect(result).toEqual(items)
// Non-prefixed items are resolved through the built-in @shadcn registry
expect(result).toEqual([
"https://ui.shadcn.com/r/styles/{style}/button.json",
"https://ui.shadcn.com/r/styles/{style}/card.json",
"https://ui.shadcn.com/r/styles/{style}/dialog.json",
])
expect(setRegistryHeaders).toHaveBeenCalledWith({})
})
@@ -137,10 +142,11 @@ describe("resolveRegistryItemsFromRegistries", () => {
const result = resolveRegistryItemsFromRegistries(items, config)
// Non-registry items (button, dialog) are resolved through the built-in @shadcn registry
expect(result).toEqual([
"button",
"https://ui.shadcn.com/r/styles/{style}/button.json",
"https://v0.dev/chat/b/card/json",
"dialog",
"https://ui.shadcn.com/r/styles/{style}/dialog.json",
"https://api.com/table.json",
])
expect(setRegistryHeaders).toHaveBeenCalledWith({

View File

@@ -11,7 +11,7 @@ const testConfig: Config = {
tailwind: {
baseColor: "neutral",
cssVariables: true,
config: "tailwind.config.ts",
config: "",
css: "tailwind.css",
},
aliases: {
@@ -25,7 +25,7 @@ const testConfig: Config = {
ui: "/ui",
lib: "/lib",
hooks: "/hooks",
tailwindConfig: "tailwind.config.ts",
tailwindConfig: "",
tailwindCss: "tailwind.css",
},
}
@@ -572,9 +572,9 @@ export function Component() {
`)
})
test("throws InvalidConfigIconLibraryError for invalid icon library", async () => {
await expect(
transform(
test("does not transform for invalid icon library", async () => {
expect(
await transform(
{
filename: "test.tsx",
raw: `import * as React from "react"
@@ -590,12 +590,14 @@ export function Component() {
},
[transformIcons]
)
).rejects.toMatchObject({
name: "InvalidConfigIconLibraryError",
message:
'Invalid icon library "invalid-library". Valid options are: lucide, tabler, hugeicons',
code: "INVALID_CONFIG",
})
).toMatchInlineSnapshot(`
"import * as React from "react"
import { IconPlaceholder } from "@/app/(create)/create/components/icon-placeholder"
export function Component() {
return <IconPlaceholder lucide="CheckIcon" />
}"
`)
})
test("does not forward library-specific props (lucide)", async () => {

View File

@@ -1,19 +1,14 @@
import { iconLibraries, type IconLibraryName } from "@/src/icons/libraries"
import { InvalidConfigIconLibraryError } from "@/src/registry/errors"
import { Transformer } from "@/src/utils/transformers"
import { SourceFile, SyntaxKind } from "ts-morph"
export const transformIcons: Transformer = async ({ sourceFile, config }) => {
const iconLibrary = config.iconLibrary
if (!iconLibrary) {
return sourceFile
}
if (!(iconLibrary in iconLibraries)) {
throw new InvalidConfigIconLibraryError(
iconLibrary,
Object.keys(iconLibraries)
)
// Fail silently if the icon library is not supported.
// This is for legacy icon libraries.
if (!iconLibrary || !(iconLibrary in iconLibraries)) {
return sourceFile
}
const targetLibrary = iconLibrary as IconLibraryName

View File

@@ -75,7 +75,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/styles/globals.css",
tailwindVersion: "v3",
frameworkVersion: "14.2.4",
frameworkVersion: "14.2.35",
aliasPrefix: "~",
},
},
@@ -89,7 +89,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/styles/globals.css",
tailwindVersion: "v3",
frameworkVersion: "13.4.2",
frameworkVersion: "14.2.35",
aliasPrefix: "~",
},
},

View File

@@ -114,7 +114,7 @@ describe("resolveFilePath", () => {
type: "registry:ui",
target: "design-system/ui/button.tsx",
},
resolvedPath: "/foo/bar/src/create-system/ui/button.tsx",
resolvedPath: "/foo/bar/src/design-system/ui/button.tsx",
projectInfo: {
isSrcDir: true,
},

View File

@@ -404,13 +404,6 @@ describe("shadcn search", () => {
expect(output.stdout).toContain('Unknown registry "@test-123"')
})
it("should handle empty registry name", async () => {
const fixturePath = await createFixtureTestDirectory("next-app-init")
const output = await npxShadcn(fixturePath, ["search", "@"])
expect(output.stdout).toContain("The item at @/registry was not found.")
})
it("should handle namespace without @ prefix", async () => {
const fixturePath = await createFixtureTestDirectory("next-app-init")
const output = await npxShadcn(fixturePath, ["search", "one"])