From 29195a17a748a6cb5b2df5fc7d52238cb7be0c2b Mon Sep 17 00:00:00 2001 From: shadcn Date: Tue, 17 Feb 2026 13:39:34 +0400 Subject: [PATCH] fix --- .github/workflows/test.yml | 3 + packages/tests/src/tests/init.test.ts | 123 ++++++------------- packages/tests/src/utils/helpers.ts | 3 - packages/tests/src/utils/registry.ts | 163 +------------------------- 4 files changed, 40 insertions(+), 252 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11bc27bb0d..1e212999f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,4 +45,7 @@ jobs: - name: Build packages run: pnpm build --filter=shadcn + - name: Build v4 registry + run: pnpm --filter=v4 registry:build + - run: pnpm test diff --git a/packages/tests/src/tests/init.test.ts b/packages/tests/src/tests/init.test.ts index dc819b4b10..6a0a0d60c4 100644 --- a/packages/tests/src/tests/init.test.ts +++ b/packages/tests/src/tests/init.test.ts @@ -1,6 +1,5 @@ import os from "os" import path from "path" -import { fileURLToPath } from "url" import fs from "fs-extra" import { afterAll, beforeAll, describe, expect, it } from "vitest" @@ -11,29 +10,13 @@ import { } from "../utils/helpers" import { createRegistryServer } from "../utils/registry" -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const PUBLIC_R_DIR = path.join(__dirname, "../../../../apps/v4/public/r") -const MOCK_PORT = 4001 -const MOCK_ENV = { REGISTRY_URL: `http://localhost:${MOCK_PORT}/r` } - -// Mock registry server that handles /init requests and serves static files. -const mockRegistry = await createRegistryServer([], { - port: MOCK_PORT, - publicDir: PUBLIC_R_DIR, -}) - -beforeAll(async () => { - await mockRegistry.start() -}) - -afterAll(async () => { - await mockRegistry.stop() -}) - describe("shadcn init - next-app", () => { it("should init with default configuration", async () => { + // Sleep for 1 second to avoid race condition with the registry server. + await new Promise((resolve) => setTimeout(resolve, 2000)) + const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) + await npxShadcn(fixturePath, ["init", "--defaults"]) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -74,9 +57,7 @@ describe("shadcn init - next-app", () => { it("should init without CSS variables", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"]) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -86,9 +67,7 @@ describe("shadcn init - next-app", () => { it("should init with components", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults", "button"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--defaults", "button"]) expect( await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx")) @@ -99,9 +78,7 @@ describe("shadcn init - next-app", () => { describe("shadcn init - vite-app", () => { it("should init with custom alias and src", async () => { const fixturePath = await createFixtureTestDirectory("vite-app") - await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"]) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -235,13 +212,7 @@ describe("shadcn init - custom style", async () => { it("should init with style that extends shadcn", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn( - fixturePath, - ["init", "http://localhost:4445/r/style.json"], - { - env: MOCK_ENV, - } - ) + await npxShadcn(fixturePath, ["init", "http://localhost:4445/r/style.json"]) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -302,11 +273,10 @@ describe("shadcn init - custom style", async () => { it("should init with style that extends another style", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn( - fixturePath, - ["init", "http://localhost:4445/r/style-extended.json"], - { env: MOCK_ENV } - ) + await npxShadcn(fixturePath, [ + "init", + "http://localhost:4445/r/style-extended.json", + ]) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -371,11 +341,10 @@ describe("shadcn init - custom style", async () => { it("should init with custom style extended none", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn( - fixturePath, - ["init", "http://localhost:4445/r/style-extend-none.json"], - { env: MOCK_ENV } - ) + await npxShadcn(fixturePath, [ + "init", + "http://localhost:4445/r/style-extend-none.json", + ]) // We still expect components.json to be created. // With some defaults. @@ -429,7 +398,7 @@ describe("shadcn init - custom style", async () => { describe("shadcn init - unsupported framework", () => { it("should init with --defaults on unsupported framework", async () => { const fixturePath = await createFixtureTestDirectory("remix-app") - await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) + await npxShadcn(fixturePath, ["init", "--defaults"]) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -450,9 +419,7 @@ describe("shadcn init - unsupported framework", () => { it("should init with --defaults and components on unsupported framework", async () => { const fixturePath = await createFixtureTestDirectory("remix-app") - await npxShadcn(fixturePath, ["init", "--defaults", "button"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--defaults", "button"]) expect( await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx")) @@ -477,9 +444,7 @@ describe("shadcn init - template flag", () => { it("should accept valid template with --defaults", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"]) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -511,7 +476,6 @@ describe("shadcn init - --name flag", () => { await npxShadcn(emptyDir, ["init", "--defaults", "--name", projectName], { timeout: 120000, - env: MOCK_ENV, }) const projectPath = path.join(emptyDir, projectName) @@ -542,7 +506,7 @@ describe("shadcn init - --name flag", () => { await npxShadcn( emptyDir, ["init", "--defaults", "--name", projectName, "-t", "vite"], - { timeout: 120000, env: MOCK_ENV } + { timeout: 120000 } ) const projectPath = path.join(emptyDir, projectName) @@ -587,14 +551,7 @@ describe("shadcn init - next-monorepo", () => { "--preset", "radix-nova", ], - { - timeout: 300000, - env: { - ...MOCK_ENV, - // Force pnpm detection since the monorepo template uses workspace:* deps. - npm_config_user_agent: "pnpm/9.0.0 node/v20.0.0", - }, - } + { timeout: 300000 } ) expect(result.exitCode).toBe(0) @@ -642,9 +599,10 @@ describe("shadcn init - next-monorepo", () => { it("should create a monorepo with custom preset url", async () => { const projectName = `test-monorepo-url-${process.pid}` - // Build a custom init URL with specific options using the mock server. - const mockBaseUrl = `http://localhost:${MOCK_PORT}` - const initUrl = `${mockBaseUrl}/init?base=radix&style=nova&baseColor=zinc&theme=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next` + // Build a custom init URL with specific options. + const registryUrl = process.env.REGISTRY_URL || "http://localhost:4000/r" + const baseUrl = registryUrl.replace(/\/r\/?$/, "") + const initUrl = `${baseUrl}/init?base=radix&style=nova&baseColor=zinc&theme=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next` const result = await npxShadcn( testBaseDir, @@ -657,14 +615,7 @@ describe("shadcn init - next-monorepo", () => { "--preset", initUrl, ], - { - timeout: 300000, - env: { - ...MOCK_ENV, - // Force pnpm detection since the monorepo template uses workspace:* deps. - npm_config_user_agent: "pnpm/9.0.0 node/v20.0.0", - }, - } + { timeout: 300000 } ) expect(result.exitCode).toBe(0) @@ -708,11 +659,11 @@ describe("shadcn init - next-monorepo", () => { describe("shadcn init - deprecated --src-dir", () => { it("should reject --src-dir as unknown option", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - const result = await npxShadcn( - fixturePath, - ["init", "--defaults", "--src-dir"], - { env: MOCK_ENV } - ) + const result = await npxShadcn(fixturePath, [ + "init", + "--defaults", + "--src-dir", + ]) expect(result.exitCode).toBe(1) }) @@ -725,7 +676,7 @@ describe("shadcn init - existing components.json", () => { const fixturePath = await createFixtureTestDirectory("next-app") // Run init with default configuration. - await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) + await npxShadcn(fixturePath, ["init", "--defaults"]) // Override style in components.json. const componentsJsonPath = path.join(fixturePath, "components.json") @@ -734,9 +685,7 @@ describe("shadcn init - existing components.json", () => { await fs.writeJson(componentsJsonPath, config) // Reinit with --force. - await npxShadcn(fixturePath, ["init", "--force", "--defaults"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--force", "--defaults"]) const newConfig = await fs.readJson(componentsJsonPath) expect(newConfig.style).toBe("new-york") @@ -781,7 +730,7 @@ describe("shadcn init - existing components.json", () => { const fixturePath = await createFixtureTestDirectory("next-app") // Run init with default configuration. - await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) + await npxShadcn(fixturePath, ["init", "--defaults"]) // Inject a custom registries object into components.json. const componentsJsonPath = path.join(fixturePath, "components.json") @@ -792,9 +741,7 @@ describe("shadcn init - existing components.json", () => { await fs.writeJson(componentsJsonPath, config) // Reinit with --force. - await npxShadcn(fixturePath, ["init", "--force", "--defaults"], { - env: MOCK_ENV, - }) + await npxShadcn(fixturePath, ["init", "--force", "--defaults"]) // components.json should exist with no .bak leftover. expect(await fs.pathExists(componentsJsonPath)).toBe(true) diff --git a/packages/tests/src/utils/helpers.ts b/packages/tests/src/utils/helpers.ts index 89ba66f5c5..31b0a10397 100644 --- a/packages/tests/src/utils/helpers.ts +++ b/packages/tests/src/utils/helpers.ts @@ -72,18 +72,15 @@ export async function npxShadcn( { debug = false, timeout, - env, }: { debug?: boolean timeout?: number - env?: Record } = {} ) { const result = await runCommand(cwd, args, { env: { REGISTRY_URL: getRegistryUrl(), SHADCN_TEMPLATE_DIR: TEMPLATES_DIR, - ...env, }, timeout, }) diff --git a/packages/tests/src/utils/registry.ts b/packages/tests/src/utils/registry.ts index 7f03c58100..44d4e330f4 100644 --- a/packages/tests/src/utils/registry.ts +++ b/packages/tests/src/utils/registry.ts @@ -2,178 +2,19 @@ import { createServer } from "http" import path from "path" import fs from "fs-extra" -// Neutral theme CSS vars used by the mock /init handler. -const NEUTRAL_THEME_LIGHT: Record = { - background: "oklch(1 0 0)", - foreground: "oklch(0.145 0 0)", - card: "oklch(1 0 0)", - "card-foreground": "oklch(0.145 0 0)", - popover: "oklch(1 0 0)", - "popover-foreground": "oklch(0.145 0 0)", - primary: "oklch(0.205 0 0)", - "primary-foreground": "oklch(0.985 0 0)", - secondary: "oklch(0.97 0 0)", - "secondary-foreground": "oklch(0.205 0 0)", - muted: "oklch(0.97 0 0)", - "muted-foreground": "oklch(0.556 0 0)", - accent: "oklch(0.97 0 0)", - "accent-foreground": "oklch(0.205 0 0)", - destructive: "oklch(0.58 0.22 27)", - border: "oklch(0.922 0 0)", - input: "oklch(0.922 0 0)", - ring: "oklch(0.708 0 0)", - "chart-1": "oklch(0.809 0.105 251.813)", - "chart-2": "oklch(0.623 0.214 259.815)", - "chart-3": "oklch(0.546 0.245 262.881)", - "chart-4": "oklch(0.488 0.243 264.376)", - "chart-5": "oklch(0.424 0.199 265.638)", - radius: "0.625rem", - sidebar: "oklch(0.985 0 0)", - "sidebar-foreground": "oklch(0.145 0 0)", - "sidebar-primary": "oklch(0.205 0 0)", - "sidebar-primary-foreground": "oklch(0.985 0 0)", - "sidebar-accent": "oklch(0.97 0 0)", - "sidebar-accent-foreground": "oklch(0.205 0 0)", - "sidebar-border": "oklch(0.922 0 0)", - "sidebar-ring": "oklch(0.708 0 0)", -} - -const NEUTRAL_THEME_DARK: Record = { - background: "oklch(0.145 0 0)", - foreground: "oklch(0.985 0 0)", - card: "oklch(0.205 0 0)", - "card-foreground": "oklch(0.985 0 0)", - popover: "oklch(0.205 0 0)", - "popover-foreground": "oklch(0.985 0 0)", - primary: "oklch(0.87 0.00 0)", - "primary-foreground": "oklch(0.205 0 0)", - secondary: "oklch(0.269 0 0)", - "secondary-foreground": "oklch(0.985 0 0)", - muted: "oklch(0.269 0 0)", - "muted-foreground": "oklch(0.708 0 0)", - accent: "oklch(0.371 0 0)", - "accent-foreground": "oklch(0.985 0 0)", - destructive: "oklch(0.704 0.191 22.216)", - border: "oklch(1 0 0 / 10%)", - input: "oklch(1 0 0 / 15%)", - ring: "oklch(0.556 0 0)", - "chart-1": "oklch(0.809 0.105 251.813)", - "chart-2": "oklch(0.623 0.214 259.815)", - "chart-3": "oklch(0.546 0.245 262.881)", - "chart-4": "oklch(0.488 0.243 264.376)", - "chart-5": "oklch(0.424 0.199 265.638)", - sidebar: "oklch(0.205 0 0)", - "sidebar-foreground": "oklch(0.985 0 0)", - "sidebar-primary": "oklch(0.488 0.243 264.376)", - "sidebar-primary-foreground": "oklch(0.985 0 0)", - "sidebar-accent": "oklch(0.269 0 0)", - "sidebar-accent-foreground": "oklch(0.985 0 0)", - "sidebar-border": "oklch(1 0 0 / 10%)", - "sidebar-ring": "oklch(0.556 0 0)", -} - -// Base dependencies by base name. -const BASE_DEPENDENCIES: Record = { - radix: ["radix-ui"], - base: ["@base-ui/react"], -} - -// Build a mock /init response matching buildRegistryBase() output. -function buildMockInitResponse(params: URLSearchParams) { - const base = params.get("base") ?? "base" - const style = params.get("style") ?? "nova" - const iconLibrary = params.get("iconLibrary") ?? "lucide" - const baseColor = params.get("baseColor") ?? "neutral" - const font = params.get("font") ?? "geist" - const rtl = params.get("rtl") === "true" - const menuAccent = params.get("menuAccent") ?? "subtle" - const menuColor = params.get("menuColor") ?? "default" - const template = params.get("template") ?? undefined - - const dependencies = [ - "shadcn@latest", - "class-variance-authority", - "tw-animate-css", - ...(BASE_DEPENDENCIES[base] ?? []), - "lucide-react", - ] - - const registryDependencies = ["utils"] - if (font) { - registryDependencies.push(`font-${font}`) - } - - return { - name: `${base}-${style}`, - extends: "none", - type: "registry:base", - config: { - style: `${base}-${style}`, - iconLibrary, - rtl, - menuColor, - menuAccent, - tailwind: { - baseColor, - }, - }, - dependencies, - registryDependencies, - cssVars: { - light: { ...NEUTRAL_THEME_LIGHT }, - dark: { ...NEUTRAL_THEME_DARK }, - }, - css: { - '@import "tw-animate-css"': {}, - '@import "shadcn/tailwind.css"': {}, - "@layer base": { - "*": { "@apply border-border outline-ring/50": {} }, - body: { "@apply bg-background text-foreground": {} }, - }, - }, - ...(rtl && { - docs: `To learn how to set up the RTL provider and fonts for your app, see https://ui.shadcn.com/docs/rtl/${ - template === "next-monorepo" ? "next" : template ?? "next" - }`, - }), - } -} - export async function createRegistryServer( items: Array<{ name: string; type: string } & Record>, { port = 4444, - path: basePath = "/r", - publicDir, + path = "/r", }: { port?: number path?: string - publicDir?: string } ) { const server = createServer((request, response) => { const urlRaw = request.url?.split("?")[0] - // Handle /init endpoint. - if (request.url?.startsWith("/init")) { - const params = new URLSearchParams(request.url.split("?")[1] ?? "") - const initResponse = buildMockInitResponse(params) - response.writeHead(200, { "Content-Type": "application/json" }) - response.end(JSON.stringify(initResponse)) - return - } - - // When publicDir is set, try serving static JSON files first. - if (publicDir && urlRaw?.startsWith(basePath + "/")) { - const relativePath = urlRaw.slice(basePath.length + 1) - const filePath = path.join(publicDir, relativePath) - if (fs.pathExistsSync(filePath)) { - response.writeHead(200, { "Content-Type": "application/json" }) - response.end(fs.readFileSync(filePath, "utf-8")) - return - } - } - // Handle registries.json endpoint (don't strip .json for this one). if (urlRaw?.endsWith("/registries.json")) { response.writeHead(200, { "Content-Type": "application/json" }) @@ -336,7 +177,7 @@ export async function createRegistryServer( } const match = urlWithoutQuery?.match( - new RegExp(`^${basePath}/(?:.*/)?([^/]+)$`) + new RegExp(`^${path}/(?:.*/)?([^/]+)$`) ) const itemName = match?.[1] const item = itemName