From d3156c09ae1df2321c0505db6b2e637374e320f7 Mon Sep 17 00:00:00 2001 From: shadcn Date: Sun, 14 Dec 2025 02:16:26 +0400 Subject: [PATCH] fix(shadcn): resolver for url (#9054) --- .changeset/solid-rings-cover.md | 5 ++ .github/workflows/prerelease.yml | 18 ++++-- packages/shadcn/package.json | 2 +- packages/shadcn/src/commands/init.ts | 2 +- packages/shadcn/src/registry/builder.test.ts | 64 ++++--------------- packages/shadcn/src/registry/builder.ts | 15 ++++- packages/shadcn/src/registry/resolver.test.ts | 14 ++-- .../transformers/transform-icons.test.ts | 24 +++---- .../src/utils/transformers/transform-icons.ts | 13 ++-- .../test/utils/get-project-info.test.ts | 4 +- .../test/utils/updaters/update-files.test.ts | 2 +- packages/tests/src/tests/search.test.ts | 7 -- 12 files changed, 72 insertions(+), 98 deletions(-) create mode 100644 .changeset/solid-rings-cover.md diff --git a/.changeset/solid-rings-cover.md b/.changeset/solid-rings-cover.md new file mode 100644 index 000000000..1b04ddb8f --- /dev/null +++ b/.changeset/solid-rings-cover.md @@ -0,0 +1,5 @@ +--- +"shadcn": patch +--- + +fix resolver for url diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index e60d757d3..d77c97593 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -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 diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 7cd7e4055..f5d06baec 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -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": [ diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index ba906a258..09e38a92c 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -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" diff --git a/packages/shadcn/src/registry/builder.test.ts b/packages/shadcn/src/registry/builder.test.ts index 3e17bb994..922cc2bbb 100644 --- a/packages/shadcn/src/registry/builder.test.ts +++ b/packages/shadcn/src/registry/builder.test.ts @@ -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() }) }) diff --git a/packages/shadcn/src/registry/builder.ts b/packages/shadcn/src/registry/builder.ts index 076621332..4a7e1f083 100644 --- a/packages/shadcn/src/registry/builder.ts +++ b/packages/shadcn/src/registry/builder.ts @@ -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) diff --git a/packages/shadcn/src/registry/resolver.test.ts b/packages/shadcn/src/registry/resolver.test.ts index 57bb757ea..bcc7fdddb 100644 --- a/packages/shadcn/src/registry/resolver.test.ts +++ b/packages/shadcn/src/registry/resolver.test.ts @@ -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({ diff --git a/packages/shadcn/src/utils/transformers/transform-icons.test.ts b/packages/shadcn/src/utils/transformers/transform-icons.test.ts index 40cdbcedf..17746800b 100644 --- a/packages/shadcn/src/utils/transformers/transform-icons.test.ts +++ b/packages/shadcn/src/utils/transformers/transform-icons.test.ts @@ -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 + }" + `) }) test("does not forward library-specific props (lucide)", async () => { diff --git a/packages/shadcn/src/utils/transformers/transform-icons.ts b/packages/shadcn/src/utils/transformers/transform-icons.ts index e168eb451..8dcf4fda4 100644 --- a/packages/shadcn/src/utils/transformers/transform-icons.ts +++ b/packages/shadcn/src/utils/transformers/transform-icons.ts @@ -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 diff --git a/packages/shadcn/test/utils/get-project-info.test.ts b/packages/shadcn/test/utils/get-project-info.test.ts index e09177d14..54d08a106 100644 --- a/packages/shadcn/test/utils/get-project-info.test.ts +++ b/packages/shadcn/test/utils/get-project-info.test.ts @@ -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: "~", }, }, diff --git a/packages/shadcn/test/utils/updaters/update-files.test.ts b/packages/shadcn/test/utils/updaters/update-files.test.ts index ee3e19e61..b9ae54918 100644 --- a/packages/shadcn/test/utils/updaters/update-files.test.ts +++ b/packages/shadcn/test/utils/updaters/update-files.test.ts @@ -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, }, diff --git a/packages/tests/src/tests/search.test.ts b/packages/tests/src/tests/search.test.ts index fe0c6a2a9..f92f2934d 100644 --- a/packages/tests/src/tests/search.test.ts +++ b/packages/tests/src/tests/search.test.ts @@ -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"])