test: update to use mock server

This commit is contained in:
shadcn
2026-02-17 12:34:45 +04:00
parent dd3e942057
commit a6f3ef591f
3 changed files with 238 additions and 37 deletions

View File

@@ -1,5 +1,6 @@
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"
@@ -10,13 +11,29 @@ 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"])
await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV })
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
@@ -57,7 +74,9 @@ 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"])
await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"], {
env: MOCK_ENV,
})
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
@@ -67,7 +86,9 @@ describe("shadcn init - next-app", () => {
it("should init with components", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "button"])
await npxShadcn(fixturePath, ["init", "--defaults", "button"], {
env: MOCK_ENV,
})
expect(
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
@@ -78,7 +99,9 @@ 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"])
await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"], {
env: MOCK_ENV,
})
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
@@ -212,7 +235,13 @@ 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"])
await npxShadcn(
fixturePath,
["init", "http://localhost:4445/r/style.json"],
{
env: MOCK_ENV,
}
)
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
@@ -273,10 +302,11 @@ 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",
])
await npxShadcn(
fixturePath,
["init", "http://localhost:4445/r/style-extended.json"],
{ env: MOCK_ENV }
)
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
@@ -341,10 +371,11 @@ 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",
])
await npxShadcn(
fixturePath,
["init", "http://localhost:4445/r/style-extend-none.json"],
{ env: MOCK_ENV }
)
// We still expect components.json to be created.
// With some defaults.
@@ -398,7 +429,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"])
await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV })
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
@@ -419,7 +450,9 @@ 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"])
await npxShadcn(fixturePath, ["init", "--defaults", "button"], {
env: MOCK_ENV,
})
expect(
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
@@ -444,7 +477,9 @@ 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"])
await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"], {
env: MOCK_ENV,
})
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
@@ -476,6 +511,7 @@ describe("shadcn init - --name flag", () => {
await npxShadcn(emptyDir, ["init", "--defaults", "--name", projectName], {
timeout: 120000,
env: MOCK_ENV,
})
const projectPath = path.join(emptyDir, projectName)
@@ -506,7 +542,7 @@ describe("shadcn init - --name flag", () => {
await npxShadcn(
emptyDir,
["init", "--defaults", "--name", projectName, "-t", "vite"],
{ timeout: 120000 }
{ timeout: 120000, env: MOCK_ENV }
)
const projectPath = path.join(emptyDir, projectName)
@@ -551,7 +587,7 @@ describe("shadcn init - next-monorepo", () => {
"--preset",
"radix-nova",
],
{ timeout: 300000 }
{ timeout: 300000, env: MOCK_ENV }
)
expect(result.exitCode).toBe(0)
@@ -599,10 +635,9 @@ 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.
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`
// 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`
const result = await npxShadcn(
testBaseDir,
@@ -615,7 +650,7 @@ describe("shadcn init - next-monorepo", () => {
"--preset",
initUrl,
],
{ timeout: 300000 }
{ timeout: 300000, env: MOCK_ENV }
)
expect(result.exitCode).toBe(0)
@@ -659,11 +694,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",
])
const result = await npxShadcn(
fixturePath,
["init", "--defaults", "--src-dir"],
{ env: MOCK_ENV }
)
expect(result.exitCode).toBe(1)
})
@@ -676,7 +711,7 @@ describe("shadcn init - existing components.json", () => {
const fixturePath = await createFixtureTestDirectory("next-app")
// Run init with default configuration.
await npxShadcn(fixturePath, ["init", "--defaults"])
await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV })
// Override style in components.json.
const componentsJsonPath = path.join(fixturePath, "components.json")
@@ -685,7 +720,9 @@ describe("shadcn init - existing components.json", () => {
await fs.writeJson(componentsJsonPath, config)
// Reinit with --force.
await npxShadcn(fixturePath, ["init", "--force", "--defaults"])
await npxShadcn(fixturePath, ["init", "--force", "--defaults"], {
env: MOCK_ENV,
})
const newConfig = await fs.readJson(componentsJsonPath)
expect(newConfig.style).toBe("new-york")
@@ -730,7 +767,7 @@ describe("shadcn init - existing components.json", () => {
const fixturePath = await createFixtureTestDirectory("next-app")
// Run init with default configuration.
await npxShadcn(fixturePath, ["init", "--defaults"])
await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV })
// Inject a custom registries object into components.json.
const componentsJsonPath = path.join(fixturePath, "components.json")
@@ -741,7 +778,9 @@ describe("shadcn init - existing components.json", () => {
await fs.writeJson(componentsJsonPath, config)
// Reinit with --force.
await npxShadcn(fixturePath, ["init", "--force", "--defaults"])
await npxShadcn(fixturePath, ["init", "--force", "--defaults"], {
env: MOCK_ENV,
})
// components.json should exist with no .bak leftover.
expect(await fs.pathExists(componentsJsonPath)).toBe(true)

View File

@@ -72,15 +72,18 @@ export async function npxShadcn(
{
debug = false,
timeout,
env,
}: {
debug?: boolean
timeout?: number
env?: Record<string, string>
} = {}
) {
const result = await runCommand(cwd, args, {
env: {
REGISTRY_URL: getRegistryUrl(),
SHADCN_TEMPLATE_DIR: TEMPLATES_DIR,
...env,
},
timeout,
})

View File

@@ -2,19 +2,178 @@ 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<string, string> = {
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<string, string> = {
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<string, string[]> = {
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<string, unknown>>,
{
port = 4444,
path = "/r",
path: basePath = "/r",
publicDir,
}: {
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" })
@@ -177,7 +336,7 @@ export async function createRegistryServer(
}
const match = urlWithoutQuery?.match(
new RegExp(`^${path}/(?:.*/)?([^/]+)$`)
new RegExp(`^${basePath}/(?:.*/)?([^/]+)$`)
)
const itemName = match?.[1]
const item = itemName