snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
2
packages/tests/.eslintignore
Normal file
2
packages/tests/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
fixtures/
|
||||
temp/
|
||||
3
packages/tests/.gitignore
vendored
Normal file
3
packages/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
temp/
|
||||
*.log
|
||||
.cache/
|
||||
1
packages/tests/.prettierignore
Normal file
1
packages/tests/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
fixtures/**
|
||||
34
packages/tests/README.md
Normal file
34
packages/tests/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Tests
|
||||
|
||||
This package contains integration tests that verify the shadcn CLI works correctly with a local registry. The tests run actual CLI commands against test fixtures to ensure files are created and updated properly.
|
||||
|
||||
## Running Tests
|
||||
|
||||
Run the following command from the root of the workspace:
|
||||
|
||||
```bash
|
||||
pnpm tests:test
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createFixtureTestDirectory,
|
||||
fileExists,
|
||||
npxShadcn,
|
||||
} from "../utils/helpers"
|
||||
|
||||
describe("my test suite", () => {
|
||||
it("should do something", async () => {
|
||||
// Create a test directory from a fixture
|
||||
const testDir = await createFixtureTestDirectory("next-app")
|
||||
|
||||
// Run CLI command
|
||||
await npxShadcn(testDir, ["init", "--base-color=neutral"])
|
||||
|
||||
// Make assertions
|
||||
expect(await fileExists(path.join(testDir, "components.json"))).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
21
packages/tests/fixtures/next-app-imports/components.json
vendored
Normal file
21
packages/tests/fixtures/next-app-imports/components.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "#components",
|
||||
"utils": "#utils",
|
||||
"ui": "#components/ui",
|
||||
"lib": "#lib",
|
||||
"hooks": "#hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
4
packages/tests/fixtures/next-app-imports/next.config.ts
vendored
Normal file
4
packages/tests/fixtures/next-app-imports/next.config.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
export default nextConfig
|
||||
38
packages/tests/fixtures/next-app-imports/package.json
vendored
Normal file
38
packages/tests/fixtures/next-app-imports/package.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "next-app-imports",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"imports": {
|
||||
"#components/*": "./src/components/*.tsx",
|
||||
"#components/ui/*": "./src/components/ui/*.tsx",
|
||||
"#lib/*": "./src/lib/*.ts",
|
||||
"#hooks/*": "./src/hooks/*.ts",
|
||||
"#hooks": "./src/hooks/index.ts",
|
||||
"#utils": "./src/lib/utils.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.527.0",
|
||||
"next": "15.4.10",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
packages/tests/fixtures/next-app-imports/postcss.config.mjs
vendored
Normal file
5
packages/tests/fixtures/next-app-imports/postcss.config.mjs
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
}
|
||||
|
||||
export default config
|
||||
123
packages/tests/fixtures/next-app-imports/src/app/globals.css
vendored
Normal file
123
packages/tests/fixtures/next-app-imports/src/app/globals.css
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--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.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--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);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--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.922 0 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.269 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.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
22
packages/tests/fixtures/next-app-imports/src/app/layout.tsx
vendored
Normal file
22
packages/tests/fixtures/next-app-imports/src/app/layout.tsx
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import "./globals.css"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
3
packages/tests/fixtures/next-app-imports/src/app/page.tsx
vendored
Normal file
3
packages/tests/fixtures/next-app-imports/src/app/page.tsx
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Home() {
|
||||
return <main>Hello</main>
|
||||
}
|
||||
6
packages/tests/fixtures/next-app-imports/src/lib/utils.ts
vendored
Normal file
6
packages/tests/fixtures/next-app-imports/src/lib/utils.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
25
packages/tests/fixtures/next-app-imports/tsconfig.json
vendored
Normal file
25
packages/tests/fixtures/next-app-imports/tsconfig.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolvePackageJsonImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
BIN
packages/tests/fixtures/next-app-init/app/favicon.ico
vendored
Normal file
BIN
packages/tests/fixtures/next-app-init/app/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
123
packages/tests/fixtures/next-app-init/app/globals.css
vendored
Normal file
123
packages/tests/fixtures/next-app-init/app/globals.css
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--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.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--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);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--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.922 0 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.269 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.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
22
packages/tests/fixtures/next-app-init/app/layout.tsx
vendored
Normal file
22
packages/tests/fixtures/next-app-init/app/layout.tsx
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import "./globals.css"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
3
packages/tests/fixtures/next-app-init/app/other.css
vendored
Normal file
3
packages/tests/fixtures/next-app-init/app/other.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: red;
|
||||
}
|
||||
113
packages/tests/fixtures/next-app-init/app/page.tsx
vendored
Normal file
113
packages/tests/fixtures/next-app-init/app/page.tsx
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
import Image from "next/image"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Learn{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Templates{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Explore the Next.js 13 playground.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
21
packages/tests/fixtures/next-app-init/components.json
vendored
Normal file
21
packages/tests/fixtures/next-app-init/components.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
6
packages/tests/fixtures/next-app-init/lib/utils.ts
vendored
Normal file
6
packages/tests/fixtures/next-app-init/lib/utils.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
4
packages/tests/fixtures/next-app-init/next.config.ts
vendored
Normal file
4
packages/tests/fixtures/next-app-init/next.config.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
32
packages/tests/fixtures/next-app-init/package.json
vendored
Normal file
32
packages/tests/fixtures/next-app-init/package.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.527.0",
|
||||
"next": "15.4.10",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
packages/tests/fixtures/next-app-init/postcss.config.mjs
vendored
Normal file
5
packages/tests/fixtures/next-app-init/postcss.config.mjs
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
packages/tests/fixtures/next-app-init/tsconfig.json
vendored
Normal file
27
packages/tests/fixtures/next-app-init/tsconfig.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
BIN
packages/tests/fixtures/next-app/app/favicon.ico
vendored
Normal file
BIN
packages/tests/fixtures/next-app/app/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
packages/tests/fixtures/next-app/app/globals.css
vendored
Normal file
1
packages/tests/fixtures/next-app/app/globals.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
22
packages/tests/fixtures/next-app/app/layout.tsx
vendored
Normal file
22
packages/tests/fixtures/next-app/app/layout.tsx
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import "./globals.css"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
3
packages/tests/fixtures/next-app/app/other.css
vendored
Normal file
3
packages/tests/fixtures/next-app/app/other.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: red;
|
||||
}
|
||||
113
packages/tests/fixtures/next-app/app/page.tsx
vendored
Normal file
113
packages/tests/fixtures/next-app/app/page.tsx
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
import Image from "next/image"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Learn{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Templates{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Explore the Next.js 13 playground.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
4
packages/tests/fixtures/next-app/next.config.ts
vendored
Normal file
4
packages/tests/fixtures/next-app/next.config.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
32
packages/tests/fixtures/next-app/package.json
vendored
Normal file
32
packages/tests/fixtures/next-app/package.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.527.0",
|
||||
"next": "15.4.10",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
packages/tests/fixtures/next-app/postcss.config.mjs
vendored
Normal file
5
packages/tests/fixtures/next-app/postcss.config.mjs
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
packages/tests/fixtures/next-app/tsconfig.json
vendored
Normal file
27
packages/tests/fixtures/next-app/tsconfig.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
0
packages/tests/fixtures/no-framework/.gitkeep
vendored
Normal file
0
packages/tests/fixtures/no-framework/.gitkeep
vendored
Normal file
12
packages/tests/fixtures/registry/example-at-property.json
vendored
Normal file
12
packages/tests/fixtures/registry/example-at-property.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-at-property",
|
||||
"type": "registry:style",
|
||||
"css": {
|
||||
"@property --foo": {
|
||||
"syntax": "'<number>'",
|
||||
"inherits": "false",
|
||||
"initial-value": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
packages/tests/fixtures/registry/example-component.json
vendored
Normal file
12
packages/tests/fixtures/registry/example-component.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-component",
|
||||
"type": "registry:component",
|
||||
"files": [
|
||||
{
|
||||
"path": "example/hello-world.tsx",
|
||||
"type": "registry:component",
|
||||
"content": "console.log('Hello, world!')"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/tests/fixtures/registry/example-env-vars.json
vendored
Normal file
10
packages/tests/fixtures/registry/example-env-vars.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-env-vars",
|
||||
"type": "registry:item",
|
||||
"envVars": {
|
||||
"APP_URL": "https://example.com",
|
||||
"EMPTY_VAR": "",
|
||||
"MULTILINE_VAR": "\"line1\nline2\nline3\""
|
||||
}
|
||||
}
|
||||
13
packages/tests/fixtures/registry/example-item-to-root.json
vendored
Normal file
13
packages/tests/fixtures/registry/example-item-to-root.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-item-to-root",
|
||||
"type": "registry:item",
|
||||
"files": [
|
||||
{
|
||||
"path": "example/config.json",
|
||||
"type": "registry:item",
|
||||
"content": "{\"foo\": \"bar\"}",
|
||||
"target": "~/config.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
packages/tests/fixtures/registry/example-item.json
vendored
Normal file
13
packages/tests/fixtures/registry/example-item.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-item",
|
||||
"type": "registry:item",
|
||||
"files": [
|
||||
{
|
||||
"path": "example/foo.txt",
|
||||
"type": "registry:item",
|
||||
"content": "Foo Bar",
|
||||
"target": "path/to/foo.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
packages/tests/fixtures/registry/example-style.json
vendored
Normal file
18
packages/tests/fixtures/registry/example-style.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-style",
|
||||
"type": "registry:style",
|
||||
"dependencies": ["@tabler/icons-react"],
|
||||
"cssVars": {
|
||||
"theme": {
|
||||
"font-sans": "Inter, sans-serif"
|
||||
},
|
||||
"light": {
|
||||
"brand": "oklch(20 14.3% 4.1%)",
|
||||
"brand-foreground": "oklch(24 1.3% 10%)"
|
||||
},
|
||||
"dark": {
|
||||
"brand": "oklch(24 1.3% 10%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
packages/tests/fixtures/registry/example-target-alias-child.json
vendored
Normal file
19
packages/tests/fixtures/registry/example-target-alias-child.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-target-alias-child",
|
||||
"type": "registry:component",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/example-target-alias-child/ui/dependency-button.tsx",
|
||||
"type": "registry:ui",
|
||||
"content": "export function DependencyButton() {\n return <button>Dependency Button</button>\n}\n",
|
||||
"target": "@ui/dependency-button.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/example-target-alias-child/lib/dependency-helper.ts",
|
||||
"type": "registry:lib",
|
||||
"content": "export function dependencyHelper() {\n return \"dependency-helper\"\n}\n",
|
||||
"target": "@lib/dependency-helper.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
packages/tests/fixtures/registry/example-target-alias-parent.json
vendored
Normal file
22
packages/tests/fixtures/registry/example-target-alias-parent.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-target-alias-parent",
|
||||
"type": "registry:component",
|
||||
"registryDependencies": [
|
||||
"../../fixtures/registry/example-target-alias-child.json"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/example-target-alias-parent/components/dependency-panel.tsx",
|
||||
"type": "registry:component",
|
||||
"content": "export function DependencyPanel() {\n return <div>Dependency Panel</div>\n}\n",
|
||||
"target": "@components/dependency-panel.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/example-target-alias-parent/hooks/use-dependency.ts",
|
||||
"type": "registry:hook",
|
||||
"content": "export function useDependency() {\n return \"dependency\"\n}\n",
|
||||
"target": "@hooks/use-dependency.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
packages/tests/fixtures/registry/example-target-alias-type-mismatch.json
vendored
Normal file
13
packages/tests/fixtures/registry/example-target-alias-type-mismatch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-target-alias-type-mismatch",
|
||||
"type": "registry:component",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/example-target-alias-type-mismatch/ui/target-from-ui-type.ts",
|
||||
"type": "registry:ui",
|
||||
"content": "export function targetFromUiType() {\n return \"target-from-ui-type\"\n}\n",
|
||||
"target": "@lib/target-from-ui-type.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
packages/tests/fixtures/registry/example-target-aliases.json
vendored
Normal file
31
packages/tests/fixtures/registry/example-target-aliases.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "example-target-aliases",
|
||||
"type": "registry:component",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/example-target-aliases/ui/target-button.tsx",
|
||||
"type": "registry:ui",
|
||||
"content": "export function TargetButton() {\n return <button>Target Button</button>\n}\n",
|
||||
"target": "@ui/target-button.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/example-target-aliases/components/target-panel.tsx",
|
||||
"type": "registry:component",
|
||||
"content": "export function TargetPanel() {\n return <div>Target Panel</div>\n}\n",
|
||||
"target": "@components/target-panel.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/example-target-aliases/lib/target-helper.ts",
|
||||
"type": "registry:lib",
|
||||
"content": "export function targetHelper() {\n return \"target-helper\"\n}\n",
|
||||
"target": "@lib/target-helper.ts"
|
||||
},
|
||||
{
|
||||
"path": "registry/example-target-aliases/hooks/use-target.ts",
|
||||
"type": "registry:hook",
|
||||
"content": "export function useTarget() {\n return \"target\"\n}\n",
|
||||
"target": "@hooks/use-target.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
packages/tests/fixtures/remix-app/app/globals.css
vendored
Normal file
1
packages/tests/fixtures/remix-app/app/globals.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
28
packages/tests/fixtures/remix-app/package.json
vendored
Normal file
28
packages/tests/fixtures/remix-app/package.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "my-remix-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "remix vite:dev",
|
||||
"build": "remix vite:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/node": "^2.0.0",
|
||||
"@remix-run/react": "^2.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.527.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
packages/tests/fixtures/remix-app/postcss.config.mjs
vendored
Normal file
5
packages/tests/fixtures/remix-app/postcss.config.mjs
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
packages/tests/fixtures/remix-app/tsconfig.json
vendored
Normal file
21
packages/tests/fixtures/remix-app/tsconfig.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
23
packages/tests/fixtures/vite-app/eslint.config.js
vendored
Normal file
23
packages/tests/fixtures/vite-app/eslint.config.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
packages/tests/fixtures/vite-app/index.html
vendored
Normal file
13
packages/tests/fixtures/vite-app/index.html
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
packages/tests/fixtures/vite-app/package.json
vendored
Normal file
37
packages/tests/fixtures/vite-app/package.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "vite-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
42
packages/tests/fixtures/vite-app/src/App.css
vendored
Normal file
42
packages/tests/fixtures/vite-app/src/App.css
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
36
packages/tests/fixtures/vite-app/src/App.tsx
vendored
Normal file
36
packages/tests/fixtures/vite-app/src/App.tsx
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import reactLogo from "./assets/react.svg"
|
||||
import viteLogo from "/vite.svg"
|
||||
import "./App.css"
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
1
packages/tests/fixtures/vite-app/src/index.css
vendored
Normal file
1
packages/tests/fixtures/vite-app/src/index.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
11
packages/tests/fixtures/vite-app/src/main.tsx
vendored
Normal file
11
packages/tests/fixtures/vite-app/src/main.tsx
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
|
||||
import "./index.css"
|
||||
import App from "./App.tsx"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
1
packages/tests/fixtures/vite-app/src/vite-env.d.ts
vendored
Normal file
1
packages/tests/fixtures/vite-app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
33
packages/tests/fixtures/vite-app/tsconfig.app.json
vendored
Normal file
33
packages/tests/fixtures/vite-app/tsconfig.app.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
/* Paths */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#custom/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13
packages/tests/fixtures/vite-app/tsconfig.json
vendored
Normal file
13
packages/tests/fixtures/vite-app/tsconfig.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#custom/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
packages/tests/fixtures/vite-app/tsconfig.node.json
vendored
Normal file
25
packages/tests/fixtures/vite-app/tsconfig.node.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
packages/tests/fixtures/vite-app/vite.config.ts
vendored
Normal file
14
packages/tests/fixtures/vite-app/vite.config.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import path from "path"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"#custom": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
18
packages/tests/fixtures/vite-monorepo-imports/apps/web/components.json
vendored
Normal file
18
packages/tests/fixtures/vite-monorepo-imports/apps/web/components.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "../../packages/ui/src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"aliases": {
|
||||
"components": "#components",
|
||||
"ui": "@workspace/ui/components",
|
||||
"lib": "#lib",
|
||||
"hooks": "#hooks",
|
||||
"utils": "@workspace/ui/lib/utils"
|
||||
}
|
||||
}
|
||||
12
packages/tests/fixtures/vite-monorepo-imports/apps/web/package.json
vendored
Normal file
12
packages/tests/fixtures/vite-monorepo-imports/apps/web/package.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@workspace/ui": "workspace:*",
|
||||
"tailwindcss": "^4.2.1"
|
||||
}
|
||||
}
|
||||
1
packages/tests/fixtures/vite-monorepo-imports/apps/web/src/main.tsx
vendored
Normal file
1
packages/tests/fixtures/vite-monorepo-imports/apps/web/src/main.tsx
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log("web")
|
||||
8
packages/tests/fixtures/vite-monorepo-imports/apps/web/tsconfig.json
vendored
Normal file
8
packages/tests/fixtures/vite-monorepo-imports/apps/web/tsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolvePackageJsonImports": true
|
||||
}
|
||||
}
|
||||
3
packages/tests/fixtures/vite-monorepo-imports/apps/web/vite.config.ts
vendored
Normal file
3
packages/tests/fixtures/vite-monorepo-imports/apps/web/vite.config.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({})
|
||||
5
packages/tests/fixtures/vite-monorepo-imports/package.json
vendored
Normal file
5
packages/tests/fixtures/vite-monorepo-imports/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "vite-monorepo-imports",
|
||||
"private": true,
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
}
|
||||
18
packages/tests/fixtures/vite-monorepo-imports/packages/ui/components.json
vendored
Normal file
18
packages/tests/fixtures/vite-monorepo-imports/packages/ui/components.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"aliases": {
|
||||
"components": "#components",
|
||||
"ui": "#components",
|
||||
"lib": "#lib",
|
||||
"hooks": "#hooks",
|
||||
"utils": "#lib/utils"
|
||||
}
|
||||
}
|
||||
14
packages/tests/fixtures/vite-monorepo-imports/packages/ui/package.json
vendored
Normal file
14
packages/tests/fixtures/vite-monorepo-imports/packages/ui/package.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@workspace/ui",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./components/*": "./src/components/*.tsx",
|
||||
"./lib/*": "./src/lib/*.ts",
|
||||
"./hooks/*": "./src/hooks/*.ts"
|
||||
}
|
||||
}
|
||||
3
packages/tests/fixtures/vite-monorepo-imports/packages/ui/src/lib/utils.ts
vendored
Normal file
3
packages/tests/fixtures/vite-monorepo-imports/packages/ui/src/lib/utils.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export function cn(...classes: Array<string | false | null | undefined>) {
|
||||
return classes.filter(Boolean).join(" ")
|
||||
}
|
||||
1
packages/tests/fixtures/vite-monorepo-imports/packages/ui/src/styles/globals.css
vendored
Normal file
1
packages/tests/fixtures/vite-monorepo-imports/packages/ui/src/styles/globals.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
8
packages/tests/fixtures/vite-monorepo-imports/packages/ui/tsconfig.json
vendored
Normal file
8
packages/tests/fixtures/vite-monorepo-imports/packages/ui/tsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolvePackageJsonImports": true
|
||||
}
|
||||
}
|
||||
3
packages/tests/fixtures/vite-monorepo-imports/pnpm-workspace.yaml
vendored
Normal file
3
packages/tests/fixtures/vite-monorepo-imports/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
26
packages/tests/package.json
Normal file
26
packages/tests/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "tests",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Integration tests for shadcn CLI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"shadcn": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^20.11.27",
|
||||
"execa": "^7.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^3.2.6"
|
||||
}
|
||||
}
|
||||
927
packages/tests/src/tests/add.test.ts
Normal file
927
packages/tests/src/tests/add.test.ts
Normal file
@@ -0,0 +1,927 @@
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
createFixtureTestDirectory,
|
||||
cssHasProperties,
|
||||
getRegistryUrl,
|
||||
npxShadcn,
|
||||
} from "../utils/helpers"
|
||||
import { configureRegistries, createRegistryServer } from "../utils/registry"
|
||||
|
||||
// Note: The tests here intentionally do not use a mocked registry.
|
||||
// We test this against the real registry.
|
||||
|
||||
function expectCommandSuccess(result: Awaited<ReturnType<typeof npxShadcn>>) {
|
||||
expect(
|
||||
result.exitCode,
|
||||
[
|
||||
`Expected command to exit with 0, got ${result.exitCode}.`,
|
||||
"stdout:",
|
||||
result.stdout || "<empty>",
|
||||
"stderr:",
|
||||
result.stderr || "<empty>",
|
||||
].join("\n")
|
||||
).toBe(0)
|
||||
}
|
||||
|
||||
describe("shadcn add", () => {
|
||||
it("should add item to project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "button"])
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add multiple items to project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "button", "card"])
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should install cn for a pre-existing project that never had it", async () => {
|
||||
// next-app-init models a project initialized before the `cn` package
|
||||
// existed: it has a components.json, clsx/tailwind-merge, but no cn
|
||||
// dependency and no cn import anywhere. Adding a component today must
|
||||
// still install cn, since the component now imports it directly.
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
const packageJsonBefore = await fs.readJson(
|
||||
path.join(fixturePath, "package.json")
|
||||
)
|
||||
expect(packageJsonBefore.dependencies).not.toHaveProperty("cn")
|
||||
|
||||
const result = await npxShadcn(fixturePath, ["add", "button", "--yes"])
|
||||
expectCommandSuccess(result)
|
||||
|
||||
const buttonPath = path.join(fixturePath, "components/ui/button.tsx")
|
||||
expect(await fs.pathExists(buttonPath)).toBe(true)
|
||||
expect(await fs.readFile(buttonPath, "utf-8")).toContain(
|
||||
'import { cn } from "cn"'
|
||||
)
|
||||
|
||||
const packageJsonAfter = await fs.readJson(
|
||||
path.join(fixturePath, "package.json")
|
||||
)
|
||||
expect(packageJsonAfter.dependencies).toHaveProperty("cn")
|
||||
})
|
||||
|
||||
it("should add item from url", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
const registryUrl = getRegistryUrl()
|
||||
const url = `${registryUrl}/styles/new-york-v4/login-01.json`
|
||||
await npxShadcn(fixturePath, ["add", url])
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "app/login/page.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/input.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/label.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add component from local file", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-component.json",
|
||||
])
|
||||
|
||||
const helloWorldContent = await fs.readFile(
|
||||
path.join(fixturePath, "components/hello-world.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(helloWorldContent).toBe("console.log('Hello, world!')")
|
||||
})
|
||||
|
||||
it("should add registry:page to the correct path", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "login-03"])
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "app/login/page.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add item with registryDependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "alert-dialog"])
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "components/ui/alert-dialog.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add item with npm dependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-style.json",
|
||||
"--yes",
|
||||
])
|
||||
const packageJson = await fs.readJson(
|
||||
path.join(fixturePath, "package.json")
|
||||
)
|
||||
expect(packageJson.dependencies["@tabler/icons-react"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("should install cssVars", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-style.json",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
const globalCssContent = await fs.readFile(
|
||||
path.join(fixturePath, "app/globals.css"),
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
expect(
|
||||
cssHasProperties(globalCssContent, [
|
||||
{
|
||||
selector: "@theme inline",
|
||||
properties: {
|
||||
"--font-sans": "Inter, sans-serif",
|
||||
"--color-brand": "var(--brand)",
|
||||
"--color-brand-foreground": "var(--brand-foreground)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ":root",
|
||||
properties: {
|
||||
"--brand": "oklch(20 14.3% 4.1%)",
|
||||
"--brand-foreground": "oklch(24 1.3% 10%)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ".dark",
|
||||
properties: {
|
||||
"--brand": "oklch(24 1.3% 10%)",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add item with target", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-item.json",
|
||||
])
|
||||
expect(await fs.pathExists(path.join(fixturePath, "path/to/foo.txt"))).toBe(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "path/to/foo.txt"), "utf-8")
|
||||
).toBe("Foo Bar")
|
||||
})
|
||||
|
||||
it("should preview add changes without writing files", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
const result = await npxShadcn(fixturePath, ["add", "button", "--dry-run"])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(result.stdout).toContain("shadcn add button (dry run)")
|
||||
expect(result.stdout).toContain("components/ui/button.tsx")
|
||||
expect(result.stdout).toContain("Run without --dry-run to apply.")
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should show no changes for identical files with diff", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
await npxShadcn(fixturePath, ["add", "button", "--yes"])
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"button",
|
||||
"--diff",
|
||||
"button",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(result.stdout).toContain("shadcn add button (dry run)")
|
||||
expect(result.stdout).toContain("components/ui/button.tsx (skip)")
|
||||
expect(result.stdout).toContain("No changes.")
|
||||
})
|
||||
|
||||
it("should add item with target to src", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-item.json",
|
||||
])
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "src/path/to/foo.txt"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "src/path/to/foo.txt"), "utf-8")
|
||||
).toBe("Foo Bar")
|
||||
})
|
||||
|
||||
it("should add item with target to root", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-item-to-root.json",
|
||||
])
|
||||
expectCommandSuccess(result)
|
||||
expect(await fs.pathExists(path.join(fixturePath, "config.json"))).toBe(
|
||||
true
|
||||
)
|
||||
expect(await fs.readJson(path.join(fixturePath, "config.json"))).toEqual({
|
||||
foo: "bar",
|
||||
})
|
||||
})
|
||||
|
||||
it("should add item with target to root when src", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-item-to-root.json",
|
||||
])
|
||||
expect(await fs.pathExists(path.join(fixturePath, "config.json"))).toBe(
|
||||
true
|
||||
)
|
||||
expect(await fs.readJson(path.join(fixturePath, "config.json"))).toEqual({
|
||||
foo: "bar",
|
||||
})
|
||||
})
|
||||
|
||||
it("should add item with registry target aliases", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-target-aliases.json",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "components/ui/target-button.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/target-panel.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "lib/target-helper.ts"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "hooks/use-target.ts"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should add registryDependencies with registry target aliases", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-target-alias-parent.json",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "components/ui/dependency-button.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "lib/dependency-helper.ts"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "components/dependency-panel.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "hooks/use-dependency.ts"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should prefer registry target aliases over the file type", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-target-alias-type-mismatch.json",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "lib/target-from-ui-type.ts"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "components/ui/target-from-ui-type.ts")
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should add item with envVars", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-env-vars.json",
|
||||
])
|
||||
expect(await fs.pathExists(path.join(fixturePath, ".env.local"))).toBe(true)
|
||||
expect(await fs.readFile(path.join(fixturePath, ".env.local"), "utf-8"))
|
||||
.toMatchInlineSnapshot(`
|
||||
"APP_URL=https://example.com
|
||||
EMPTY_VAR=
|
||||
MULTILINE_VAR="line1
|
||||
line2
|
||||
line3"
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it("should add monorepo components and rewrite app-local imports with package imports", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory(
|
||||
"vite-monorepo-imports"
|
||||
)
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
["add", "login-03", "-c", "apps/web", "--yes"],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
|
||||
expectCommandSuccess(result)
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/components/login-form.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "packages/ui/src/components/button.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/components/ui/button.tsx")
|
||||
)
|
||||
).toBe(false)
|
||||
|
||||
const loginFormContent = await fs.readFile(
|
||||
path.join(fixturePath, "apps/web/src/components/login-form.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(loginFormContent).toContain('import { cn } from "cn"')
|
||||
expect(loginFormContent).toContain(
|
||||
'import { Button } from "@workspace/ui/components/button"'
|
||||
)
|
||||
|
||||
const buttonContent = await fs.readFile(
|
||||
path.join(fixturePath, "packages/ui/src/components/button.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(buttonContent).toContain('import { cn } from "cn"')
|
||||
}, 300000)
|
||||
|
||||
it("should add monorepo item with registry target aliases and package imports", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory(
|
||||
"vite-monorepo-imports"
|
||||
)
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
[
|
||||
"add",
|
||||
"../../fixtures/registry/example-target-aliases.json",
|
||||
"-c",
|
||||
"apps/web",
|
||||
"--yes",
|
||||
],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "packages/ui/src/components/target-button.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/components/target-panel.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/lib/target-helper.ts")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/hooks/use-target.ts")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/components/ui/target-button.tsx")
|
||||
)
|
||||
).toBe(false)
|
||||
}, 300000)
|
||||
|
||||
it("should prefer registry target aliases over the file type in monorepos", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory(
|
||||
"vite-monorepo-imports"
|
||||
)
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
[
|
||||
"add",
|
||||
"../../fixtures/registry/example-target-alias-type-mismatch.json",
|
||||
"-c",
|
||||
"apps/web",
|
||||
"--yes",
|
||||
],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/lib/target-from-ui-type.ts")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "packages/ui/src/lib/target-from-ui-type.ts")
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(
|
||||
fixturePath,
|
||||
"packages/ui/src/components/target-from-ui-type.ts"
|
||||
)
|
||||
)
|
||||
).toBe(false)
|
||||
}, 300000)
|
||||
|
||||
it("should preview monorepo adds without writing files", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory(
|
||||
"vite-monorepo-imports"
|
||||
)
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
["add", "login-03", "-c", "apps/web", "--dry-run", "--yes"],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("shadcn add login-03 (dry run)")
|
||||
expect(result.stdout).toContain(
|
||||
"../../packages/ui/src/components/button.tsx"
|
||||
)
|
||||
expect(result.stdout).toContain("src/components/login-form.tsx")
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "apps/web/src/components/login-form.tsx")
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "packages/ui/src/components/button.tsx")
|
||||
)
|
||||
).toBe(false)
|
||||
}, 300000)
|
||||
|
||||
it("should show no changes for identical monorepo files with diff", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory(
|
||||
"vite-monorepo-imports"
|
||||
)
|
||||
|
||||
const setupResult = await npxShadcn(
|
||||
fixturePath,
|
||||
["add", "login-03", "-c", "apps/web", "--yes"],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
expectCommandSuccess(setupResult)
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
["add", "login-03", "-c", "apps/web", "--diff", "login-form", "--yes"],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(result.stdout).toContain("shadcn add login-03 (dry run)")
|
||||
expect(result.stdout).toContain("src/components/login-form.tsx (skip)")
|
||||
expect(result.stdout).toContain("No changes.")
|
||||
}, 300000)
|
||||
|
||||
it("should add NOT update existing envVars", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(fixturePath, ".env.local"),
|
||||
"APP_URL=https://foo.com"
|
||||
)
|
||||
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-env-vars.json",
|
||||
])
|
||||
|
||||
expect(await fs.pathExists(path.join(fixturePath, ".env.local"))).toBe(true)
|
||||
expect(await fs.readFile(path.join(fixturePath, ".env.local"), "utf-8"))
|
||||
.toMatchInlineSnapshot(`
|
||||
"APP_URL=https://foo.com
|
||||
|
||||
EMPTY_VAR=
|
||||
MULTILINE_VAR=line1
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it("should use existing .env if it exists", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(fixturePath, ".env"),
|
||||
"APP_URL=https://foo.com"
|
||||
)
|
||||
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-env-vars.json",
|
||||
])
|
||||
|
||||
expect(await fs.pathExists(path.join(fixturePath, ".env.local"))).toBe(
|
||||
false
|
||||
)
|
||||
expect(await fs.readFile(path.join(fixturePath, ".env"), "utf-8"))
|
||||
.toMatchInlineSnapshot(`
|
||||
"APP_URL=https://foo.com
|
||||
|
||||
EMPTY_VAR=
|
||||
MULTILINE_VAR=line1
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it("should add registry:item with no framework", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("no-framework")
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-item.json",
|
||||
])
|
||||
|
||||
expect(await fs.pathExists(path.join(fixturePath, "path/to/foo.txt"))).toBe(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "path/to/foo.txt"), "utf-8")
|
||||
).toBe("Foo Bar")
|
||||
})
|
||||
|
||||
it("should add component to custom file path", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "card", "--path=custom/my-card.tsx"])
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "custom/my-card.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should add component to custom directory", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "card", "--path=custom/components"])
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "custom/components/card.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should add multiple files to custom directory", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "input", "card", "--path=custom/ui"])
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "custom/ui/input.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "custom/ui/card.tsx"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/input.tsx"))
|
||||
).toBe(false)
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should add component to a single-package #imports project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-imports")
|
||||
|
||||
const result = await npxShadcn(fixturePath, ["add", "button", "--yes"])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
|
||||
const buttonPath = path.join(fixturePath, "src/components/ui/button.tsx")
|
||||
expect(await fs.pathExists(buttonPath)).toBe(true)
|
||||
|
||||
const buttonContent = await fs.readFile(buttonPath, "utf-8")
|
||||
expect(buttonContent).toContain('import { cn } from "cn"')
|
||||
expect(buttonContent).not.toContain("@/lib/utils")
|
||||
expect(buttonContent).not.toContain("@/registry/")
|
||||
})
|
||||
|
||||
it("should add multi-file block to a single-package #imports project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-imports")
|
||||
|
||||
const result = await npxShadcn(fixturePath, ["add", "login-03", "--yes"])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
|
||||
const loginFormPath = path.join(
|
||||
fixturePath,
|
||||
"src/components/login-form.tsx"
|
||||
)
|
||||
const buttonPath = path.join(fixturePath, "src/components/ui/button.tsx")
|
||||
expect(await fs.pathExists(loginFormPath)).toBe(true)
|
||||
expect(await fs.pathExists(buttonPath)).toBe(true)
|
||||
|
||||
const loginFormContent = await fs.readFile(loginFormPath, "utf-8")
|
||||
expect(loginFormContent).toContain('import { cn } from "cn"')
|
||||
expect(loginFormContent).toContain(
|
||||
'import { Button } from "#components/ui/button"'
|
||||
)
|
||||
expect(loginFormContent).not.toContain("@/registry/")
|
||||
})
|
||||
|
||||
it("should preview --dry-run for a single-package #imports project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-imports")
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"button",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(result.stdout).toContain("shadcn add button (dry run)")
|
||||
expect(result.stdout).toContain("src/components/ui/button.tsx")
|
||||
expect(result.stdout).toContain("Run without --dry-run to apply.")
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "src/components/ui/button.tsx")
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should show --diff no-op for identical content in a #imports project", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-imports")
|
||||
|
||||
const setup = await npxShadcn(fixturePath, ["add", "button", "--yes"])
|
||||
expectCommandSuccess(setup)
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"button",
|
||||
"--diff",
|
||||
"button",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
expect(result.stdout).toContain("shadcn add button (dry run)")
|
||||
expect(result.stdout).toContain("src/components/ui/button.tsx (skip)")
|
||||
expect(result.stdout).toContain("No changes.")
|
||||
})
|
||||
|
||||
it("should add namespaced registry item to a #imports project", async () => {
|
||||
const registry = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "fancy-card",
|
||||
type: "registry:component",
|
||||
registryDependencies: ["button"],
|
||||
files: [
|
||||
{
|
||||
path: "components/fancy-card.tsx",
|
||||
type: "registry:component",
|
||||
content: `import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function FancyCard() {
|
||||
return <Button className={cn("rounded-lg")}>Fancy</Button>
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
port: 4454,
|
||||
}
|
||||
)
|
||||
await registry.start()
|
||||
|
||||
try {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-imports")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:4454/r/{name}",
|
||||
})
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"@one/fancy-card",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
expectCommandSuccess(result)
|
||||
|
||||
const cardPath = path.join(fixturePath, "src/components/fancy-card.tsx")
|
||||
const buttonPath = path.join(fixturePath, "src/components/ui/button.tsx")
|
||||
|
||||
expect(await fs.pathExists(cardPath)).toBe(true)
|
||||
expect(await fs.pathExists(buttonPath)).toBe(true)
|
||||
|
||||
const cardContent = await fs.readFile(cardPath, "utf-8")
|
||||
expect(cardContent).toContain(
|
||||
'import { Button } from "#components/ui/button"'
|
||||
)
|
||||
expect(cardContent).toContain('import { cn } from "#utils"')
|
||||
expect(cardContent).not.toContain("@/registry/")
|
||||
expect(cardContent).not.toContain("@/lib/utils")
|
||||
} finally {
|
||||
await registry.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it("should add at-property", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await npxShadcn(fixturePath, [
|
||||
"add",
|
||||
"../../fixtures/registry/example-at-property.json",
|
||||
"--yes",
|
||||
])
|
||||
|
||||
const globalCssContent = await fs.readFile(
|
||||
path.join(fixturePath, "app/globals.css"),
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
expect(
|
||||
cssHasProperties(globalCssContent, [
|
||||
{
|
||||
selector: "@property --foo",
|
||||
properties: {
|
||||
syntax: "'<number>'",
|
||||
inherits: "false",
|
||||
"initial-value": "0",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn registry add", () => {
|
||||
it("should add registry from index to components.json", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["registry", "add", "@magicui"])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.registries).toBeDefined()
|
||||
expect(componentsJson.registries["@magicui"]).toBeDefined()
|
||||
expect(componentsJson.registries["@magicui"]).toContain("{name}")
|
||||
})
|
||||
|
||||
it("should add custom registry with URL", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, [
|
||||
"registry",
|
||||
"add",
|
||||
"@mycompany=https://example.com/r/{name}.json",
|
||||
])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.registries["@mycompany"]).toBe(
|
||||
"https://example.com/r/{name}.json"
|
||||
)
|
||||
})
|
||||
|
||||
it("should add multiple registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["registry", "add", "@magicui", "@aceternity"])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.registries["@magicui"]).toBeDefined()
|
||||
expect(componentsJson.registries["@aceternity"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("should skip already configured registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["registry", "add", "@magicui"])
|
||||
|
||||
// Add again - should not error.
|
||||
await npxShadcn(fixturePath, ["registry", "add", "@magicui"])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.registries["@magicui"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("should error for registry not in index without URL", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"registry",
|
||||
"add",
|
||||
"@nonexistent-registry-12345",
|
||||
])
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should error for invalid URL missing {name}", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"registry",
|
||||
"add",
|
||||
"@foo=https://example.com/bad.json",
|
||||
])
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("{name}")
|
||||
})
|
||||
|
||||
it("should require components.json for adding registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("no-framework")
|
||||
|
||||
// No init, so no components.json.
|
||||
const result = await npxShadcn(fixturePath, ["registry", "add", "@magicui"])
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("components.json")
|
||||
})
|
||||
})
|
||||
616
packages/tests/src/tests/apply.test.ts
Normal file
616
packages/tests/src/tests/apply.test.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
createFixtureTestDirectory,
|
||||
getRegistryUrl,
|
||||
npxShadcn,
|
||||
} from "../utils/helpers"
|
||||
|
||||
async function createInitializedProject() {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
await npxShadcn(fixturePath, ["add", "button"])
|
||||
return fixturePath
|
||||
}
|
||||
|
||||
async function createInitializedRtlProject() {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "--rtl"])
|
||||
await npxShadcn(fixturePath, ["add", "button"])
|
||||
return fixturePath
|
||||
}
|
||||
|
||||
async function createInitializedViteRtlProject() {
|
||||
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "--rtl"])
|
||||
await npxShadcn(
|
||||
fixturePath,
|
||||
["add", "breadcrumb", "pagination", "sidebar", "-y"],
|
||||
{
|
||||
timeout: 120000,
|
||||
}
|
||||
)
|
||||
return fixturePath
|
||||
}
|
||||
|
||||
async function createInitializedMonorepoProject() {
|
||||
const rootDir = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-apply-monorepo-${process.pid}-${Date.now()}`
|
||||
)
|
||||
const projectName = `test-apply-monorepo-${process.pid}`
|
||||
await fs.ensureDir(rootDir)
|
||||
const result = await npxShadcn(
|
||||
rootDir,
|
||||
[
|
||||
"init",
|
||||
"--name",
|
||||
projectName,
|
||||
"-t",
|
||||
"vite",
|
||||
"--monorepo",
|
||||
"--preset",
|
||||
"nova",
|
||||
"--base",
|
||||
"radix",
|
||||
],
|
||||
{
|
||||
timeout: 300000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
return {
|
||||
projectPath: path.join(rootDir, projectName),
|
||||
rootDir,
|
||||
}
|
||||
}
|
||||
|
||||
async function createInitializedRadixProject() {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--preset", "a0", "--base", "radix"])
|
||||
await npxShadcn(fixturePath, ["add", "button"])
|
||||
return fixturePath
|
||||
}
|
||||
|
||||
function getMonorepoPresetConfig(config: {
|
||||
style: string
|
||||
tailwind: {
|
||||
baseColor: string
|
||||
cssVariables: boolean
|
||||
}
|
||||
iconLibrary?: string
|
||||
rtl?: boolean
|
||||
menuColor?: string
|
||||
menuAccent?: string
|
||||
}) {
|
||||
return {
|
||||
style: config.style,
|
||||
baseColor: config.tailwind.baseColor,
|
||||
cssVariables: config.tailwind.cssVariables,
|
||||
iconLibrary: config.iconLibrary,
|
||||
rtl: config.rtl ?? false,
|
||||
menuColor: config.menuColor ?? "default",
|
||||
menuAccent: config.menuAccent ?? "subtle",
|
||||
}
|
||||
}
|
||||
|
||||
describe("shadcn apply", () => {
|
||||
const itIfNotCi = process.env.CI ? it.skip : it
|
||||
|
||||
it("should apply a preset with --preset <code>", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"a0",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("Preset applied successfully")
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should apply a preset with positional <code>", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, ["apply", "a0", "-y"])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("Preset applied successfully")
|
||||
})
|
||||
|
||||
it("should allow the same positional and flag preset values", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"a0",
|
||||
"--preset",
|
||||
"a0",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("should reject different positional and flag preset values", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"a0",
|
||||
"--preset",
|
||||
"b0",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("Received two different preset values")
|
||||
})
|
||||
|
||||
it("should offer the preset builder when no preset is provided", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
const createUrl = new URL(
|
||||
`${getRegistryUrl().replace(/\/r\/?$/, "")}/create`
|
||||
)
|
||||
createUrl.searchParams.set("command", "init")
|
||||
createUrl.searchParams.set("template", "next")
|
||||
createUrl.searchParams.set(
|
||||
"base",
|
||||
componentsJson.style.startsWith("base-") ? "base" : "radix"
|
||||
)
|
||||
const result = await npxShadcn(fixturePath, ["apply"], {
|
||||
input: "n\n",
|
||||
})
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("Build your custom preset on")
|
||||
expect(result.stdout).toContain(createUrl.toString())
|
||||
expect(result.stdout).toContain("shadcn apply --preset <preset>")
|
||||
})
|
||||
|
||||
it("should print the preset builder url without prompting when no preset is provided with -y", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, ["apply", "-y"])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("Build your custom preset on")
|
||||
expect(result.stdout).not.toContain("Open in browser?")
|
||||
})
|
||||
|
||||
it("should warn before applying and list detected components", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, ["apply", "--preset", "a0"], {
|
||||
input: "y\n",
|
||||
})
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain(
|
||||
"Applying a new preset will overwrite existing UI components, fonts, and CSS variables."
|
||||
)
|
||||
expect(result.stdout).toContain(
|
||||
"Commit or stash your changes before continuing so you can easily go back."
|
||||
)
|
||||
expect(result.stdout).toContain(
|
||||
"The following components will be re-installed:"
|
||||
)
|
||||
expect(result.stdout).toContain("button")
|
||||
})
|
||||
|
||||
it("should skip confirmation with -y", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"a0",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).not.toContain("Would you like to continue?")
|
||||
})
|
||||
|
||||
it("should suggest init when components.json is missing", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const result = await npxShadcn(fixturePath, ["apply", "--preset", "a0"])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("components.json")
|
||||
expect(result.stdout).toContain("shadcn init --preset a0")
|
||||
})
|
||||
|
||||
it("should not show undefined in init guidance when components.json is missing and no preset is provided", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const result = await npxShadcn(fixturePath, ["apply"])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("components.json")
|
||||
expect(result.stdout).toContain("shadcn init")
|
||||
expect(result.stdout).not.toContain("undefined")
|
||||
})
|
||||
|
||||
it("should fail on invalid components.json", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await fs.writeFile(path.join(fixturePath, "components.json"), "{", "utf8")
|
||||
|
||||
const result = await npxShadcn(fixturePath, ["apply", "--preset", "a0"])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("An invalid components.json file was found")
|
||||
})
|
||||
|
||||
it("should restore components.json when applying a preset fails", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const original = await fs.readFile(componentsJsonPath, "utf8")
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
`${getRegistryUrl()}/does-not-exist.json`,
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(await fs.readFile(componentsJsonPath, "utf8")).toBe(original)
|
||||
expect(await fs.pathExists(`${componentsJsonPath}.bak`)).toBe(false)
|
||||
})
|
||||
|
||||
it("should abort before applying when components.json cannot be backed up", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const buttonPath = path.join(fixturePath, "components/ui/button.tsx")
|
||||
const originalConfig = await fs.readFile(componentsJsonPath, "utf8")
|
||||
const originalButton = await fs.readFile(buttonPath, "utf8")
|
||||
|
||||
await fs.ensureDir(`${componentsJsonPath}.bak`)
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"lyra",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("Could not back up components.json")
|
||||
expect(await fs.readFile(componentsJsonPath, "utf8")).toBe(originalConfig)
|
||||
expect(await fs.readFile(buttonPath, "utf8")).toBe(originalButton)
|
||||
expect((await fs.stat(`${componentsJsonPath}.bak`)).isDirectory()).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should guide the user to a workspace when run from a monorepo root", async () => {
|
||||
const rootDir = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-apply-monorepo-${process.pid}-${Date.now()}`
|
||||
)
|
||||
|
||||
await fs.ensureDir(path.join(rootDir, "apps/web"))
|
||||
await fs.writeJson(path.join(rootDir, "package.json"), {
|
||||
private: true,
|
||||
workspaces: ["apps/*"],
|
||||
})
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "pnpm-workspace.yaml"),
|
||||
'packages:\n - "apps/*"\n',
|
||||
"utf8"
|
||||
)
|
||||
await fs.writeJson(path.join(rootDir, "apps/web/package.json"), {
|
||||
name: "web",
|
||||
version: "0.0.0",
|
||||
})
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "apps/web/next.config.ts"),
|
||||
"",
|
||||
"utf8"
|
||||
)
|
||||
|
||||
const result = await npxShadcn(rootDir, ["apply", "--preset", "a0"])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("monorepo root")
|
||||
expect(result.stdout).toContain(
|
||||
"shadcn apply --preset <preset> -c apps/web"
|
||||
)
|
||||
expect(result.stdout).not.toContain(
|
||||
"shadcn apply --preset <preset> -c packages/ui"
|
||||
)
|
||||
|
||||
await fs.remove(rootDir)
|
||||
})
|
||||
|
||||
it("should update the project config and reinstall detected components", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"lyra",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("base-lyra")
|
||||
expect(componentsJson.iconLibrary).toBe("phosphor")
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should preserve rtl when applying a named preset", async () => {
|
||||
const fixturePath = await createInitializedRtlProject()
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"lyra",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.rtl).toBe(true)
|
||||
})
|
||||
|
||||
it("should preserve the current base for raw preset urls", async () => {
|
||||
const fixturePath = await createInitializedRadixProject()
|
||||
const presetUrl = `${getRegistryUrl().replace(/\/r\/?$/, "")}/init?base=base&style=lyra&baseColor=neutral&theme=neutral&iconLibrary=phosphor&font=manrope&rtl=false&menuAccent=subtle&menuColor=default&radius=default`
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
presetUrl,
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("radix-lyra")
|
||||
})
|
||||
|
||||
it("should preserve rtl for raw preset urls", async () => {
|
||||
const fixturePath = await createInitializedRtlProject()
|
||||
const presetUrl = `${getRegistryUrl().replace(/\/r\/?$/, "")}/init?base=base&style=lyra&baseColor=neutral&theme=neutral&iconLibrary=phosphor&font=manrope&rtl=false&menuAccent=subtle&menuColor=default&radius=default`
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
presetUrl,
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.rtl).toBe(true)
|
||||
expect(componentsJson.style).toBe("base-lyra")
|
||||
})
|
||||
|
||||
it("should preserve non-preset config when applying a preset", async () => {
|
||||
const fixturePath = await createInitializedProject()
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
|
||||
componentsJson.tailwind.prefix = "tw-"
|
||||
await fs.writeJson(componentsJsonPath, componentsJson, { spaces: 2 })
|
||||
|
||||
const result = await npxShadcn(fixturePath, [
|
||||
"apply",
|
||||
"--preset",
|
||||
"lyra",
|
||||
"-y",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const updatedConfig = await fs.readJson(componentsJsonPath)
|
||||
expect(updatedConfig.tailwind.prefix).toBe("tw-")
|
||||
expect(updatedConfig.style).toBe("base-lyra")
|
||||
expect(updatedConfig.iconLibrary).toBe("phosphor")
|
||||
})
|
||||
|
||||
it("should keep vite component output rtl-aware when applying a new preset", async () => {
|
||||
const fixturePath = await createInitializedViteRtlProject()
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const breadcrumbPath = path.join(
|
||||
fixturePath,
|
||||
"src/components/ui/breadcrumb.tsx"
|
||||
)
|
||||
const paginationPath = path.join(
|
||||
fixturePath,
|
||||
"src/components/ui/pagination.tsx"
|
||||
)
|
||||
const sidebarPath = path.join(fixturePath, "src/components/ui/sidebar.tsx")
|
||||
|
||||
const initialConfig = await fs.readJson(componentsJsonPath)
|
||||
const initialBreadcrumb = await fs.readFile(breadcrumbPath, "utf8")
|
||||
const initialPagination = await fs.readFile(paginationPath, "utf8")
|
||||
const initialSidebar = await fs.readFile(sidebarPath, "utf8")
|
||||
|
||||
expect(initialConfig.style).toBe("base-nova")
|
||||
expect(initialConfig.iconLibrary).toBe("lucide")
|
||||
expect(initialConfig.rtl).toBe(true)
|
||||
expect(initialBreadcrumb).toContain("rtl:rotate-180")
|
||||
expect(initialPagination).toContain("ps-1.5!")
|
||||
expect(initialPagination).toContain("pe-1.5!")
|
||||
expect(initialPagination).toContain("rtl:rotate-180")
|
||||
expect(initialSidebar).toContain("start-")
|
||||
expect(initialSidebar).toContain("end-")
|
||||
expect(initialSidebar).toContain("ms-")
|
||||
expect(initialSidebar).toContain("pe-")
|
||||
expect(initialSidebar).toContain("rtl:")
|
||||
|
||||
const result = await npxShadcn(
|
||||
fixturePath,
|
||||
["apply", "--preset", "lyra", "-y"],
|
||||
{
|
||||
timeout: 120000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const updatedConfig = await fs.readJson(componentsJsonPath)
|
||||
const updatedBreadcrumb = await fs.readFile(breadcrumbPath, "utf8")
|
||||
const updatedPagination = await fs.readFile(paginationPath, "utf8")
|
||||
const updatedSidebar = await fs.readFile(sidebarPath, "utf8")
|
||||
|
||||
expect(updatedConfig.style).toBe("base-lyra")
|
||||
expect(updatedConfig.iconLibrary).toBe("phosphor")
|
||||
expect(updatedConfig.rtl).toBe(true)
|
||||
expect(updatedBreadcrumb).toContain("rtl:rotate-180")
|
||||
expect(updatedPagination).toContain("ps-1.5!")
|
||||
expect(updatedPagination).toContain("pe-1.5!")
|
||||
expect(updatedPagination).toContain("rtl:rotate-180")
|
||||
expect(updatedSidebar).toContain("start-")
|
||||
expect(updatedSidebar).toContain("end-")
|
||||
expect(updatedSidebar).toContain("ms-")
|
||||
expect(updatedSidebar).toContain("pe-")
|
||||
expect(updatedSidebar).toContain("rtl:")
|
||||
expect(updatedBreadcrumb).not.toBe(initialBreadcrumb)
|
||||
expect(updatedPagination).not.toBe(initialPagination)
|
||||
expect(updatedSidebar).not.toBe(initialSidebar)
|
||||
})
|
||||
|
||||
itIfNotCi(
|
||||
"should keep the app config updated when linked workspace sync fails",
|
||||
async () => {
|
||||
const { projectPath, rootDir } = await createInitializedMonorepoProject()
|
||||
const presetUrl = `${getRegistryUrl().replace(/\/r\/?$/, "")}/init?base=base&style=lyra&baseColor=neutral&theme=neutral&iconLibrary=phosphor&font=manrope&rtl=true&menuAccent=bold&menuColor=inverted&radius=default`
|
||||
const appConfigPath = path.join(projectPath, "apps/web/components.json")
|
||||
const uiConfigPath = path.join(projectPath, "packages/ui/components.json")
|
||||
const buttonPath = path.join(
|
||||
projectPath,
|
||||
"packages/ui/src/components/button.tsx"
|
||||
)
|
||||
const originalButton = await fs.readFile(buttonPath, "utf8")
|
||||
|
||||
await fs.ensureDir(`${uiConfigPath}.bak`)
|
||||
|
||||
try {
|
||||
const result = await npxShadcn(
|
||||
projectPath,
|
||||
["apply", "--preset", presetUrl, "-y", "-c", "apps/web"],
|
||||
{
|
||||
timeout: 300000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain(
|
||||
"Failed to sync linked workspace configs"
|
||||
)
|
||||
|
||||
const appConfig = await fs.readJson(appConfigPath)
|
||||
const uiConfig = await fs.readJson(uiConfigPath)
|
||||
expect(appConfig.style).toBe("radix-lyra")
|
||||
expect(appConfig.iconLibrary).toBe("phosphor")
|
||||
expect(appConfig.menuColor).toBe("inverted")
|
||||
expect(appConfig.menuAccent).toBe("bold")
|
||||
expect(appConfig.rtl).toBe(false)
|
||||
|
||||
expect(uiConfig.style).toBe("radix-nova")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("neutral")
|
||||
expect(uiConfig.tailwind.cssVariables).toBe(true)
|
||||
expect(uiConfig.iconLibrary).toBe("phosphor")
|
||||
expect(uiConfig.menuColor).toBe("inverted")
|
||||
expect(uiConfig.menuAccent).toBe("bold")
|
||||
expect(uiConfig.rtl).toBe(false)
|
||||
expect(await fs.readFile(buttonPath, "utf8")).not.toBe(originalButton)
|
||||
} finally {
|
||||
await fs.remove(rootDir)
|
||||
}
|
||||
},
|
||||
300000
|
||||
)
|
||||
|
||||
itIfNotCi(
|
||||
"should sync linked workspace configs when applying from apps/web",
|
||||
async () => {
|
||||
const { projectPath, rootDir } = await createInitializedMonorepoProject()
|
||||
const presetUrl = `${getRegistryUrl().replace(/\/r\/?$/, "")}/init?base=base&style=lyra&baseColor=neutral&theme=neutral&iconLibrary=phosphor&font=manrope&rtl=true&menuAccent=bold&menuColor=inverted&radius=default`
|
||||
|
||||
try {
|
||||
const result = await npxShadcn(
|
||||
projectPath,
|
||||
["apply", "--preset", presetUrl, "-y", "-c", "apps/web"],
|
||||
{
|
||||
timeout: 300000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const appConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
|
||||
expect(getMonorepoPresetConfig(appConfig)).toEqual(
|
||||
getMonorepoPresetConfig(uiConfig)
|
||||
)
|
||||
expect(appConfig.style).toBe("radix-lyra")
|
||||
expect(appConfig.iconLibrary).toBe("phosphor")
|
||||
expect(appConfig.menuColor).toBe("inverted")
|
||||
expect(appConfig.menuAccent).toBe("bold")
|
||||
expect(appConfig.rtl).toBe(false)
|
||||
expect(appConfig.tailwind.css).toBe(
|
||||
"../../packages/ui/src/styles/globals.css"
|
||||
)
|
||||
expect(uiConfig.tailwind.css).toBe("src/styles/globals.css")
|
||||
} finally {
|
||||
await fs.remove(rootDir)
|
||||
}
|
||||
},
|
||||
300000
|
||||
)
|
||||
|
||||
itIfNotCi(
|
||||
"should require an app workspace when applying from a monorepo",
|
||||
async () => {
|
||||
const { projectPath, rootDir } = await createInitializedMonorepoProject()
|
||||
const presetUrl = `${getRegistryUrl().replace(/\/r\/?$/, "")}/init?base=base&style=lyra&baseColor=neutral&theme=neutral&iconLibrary=phosphor&font=manrope&rtl=true&menuAccent=bold&menuColor=inverted&radius=default`
|
||||
|
||||
try {
|
||||
const result = await npxShadcn(
|
||||
projectPath,
|
||||
["apply", "--preset", presetUrl, "-y", "-c", "packages/ui"],
|
||||
{
|
||||
timeout: 300000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain(
|
||||
"We could not detect a supported framework"
|
||||
)
|
||||
expect(result.stdout).toContain("packages/ui")
|
||||
} finally {
|
||||
await fs.remove(rootDir)
|
||||
}
|
||||
},
|
||||
300000
|
||||
)
|
||||
})
|
||||
815
packages/tests/src/tests/init.test.ts
Normal file
815
packages/tests/src/tests/init.test.ts
Normal file
@@ -0,0 +1,815 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
createFixtureTestDirectory,
|
||||
cssHasProperties,
|
||||
npxShadcn,
|
||||
} from "../utils/helpers"
|
||||
import { createRegistryServer } from "../utils/registry"
|
||||
|
||||
describe("shadcn init - next-app", () => {
|
||||
it("should init with default configuration", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"], { timeout: 120000 })
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson).toMatchObject({
|
||||
style: "base-nova",
|
||||
rsc: true,
|
||||
tsx: true,
|
||||
tailwind: {
|
||||
config: "",
|
||||
css: "app/globals.css",
|
||||
baseColor: "neutral",
|
||||
cssVariables: true,
|
||||
},
|
||||
aliases: {
|
||||
components: "@/components",
|
||||
utils: "@/lib/utils",
|
||||
ui: "@/components/ui",
|
||||
lib: "@/lib",
|
||||
hooks: "@/hooks",
|
||||
},
|
||||
})
|
||||
|
||||
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "lib/utils.ts"), "utf-8")
|
||||
).toContain('export { cn } from "cn"')
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
path.join(fixturePath, "package.json")
|
||||
)
|
||||
expect(packageJson.dependencies).toHaveProperty("cn")
|
||||
|
||||
const cssPath = path.join(fixturePath, "app/globals.css")
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain("@layer base")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("tw-animate-css")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
})
|
||||
|
||||
it("should init without CSS variables", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"], {
|
||||
timeout: 120000,
|
||||
})
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.tailwind.cssVariables).toBe(false)
|
||||
})
|
||||
|
||||
it("should init with components", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "button"], {
|
||||
timeout: 120000,
|
||||
})
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
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"])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("base-nova")
|
||||
expect(componentsJson.tailwind.baseColor).toBe("neutral")
|
||||
expect(componentsJson.aliases).toMatchObject({
|
||||
components: "#custom/components",
|
||||
utils: "#custom/lib/utils",
|
||||
ui: "#custom/components/ui",
|
||||
lib: "#custom/lib",
|
||||
hooks: "#custom/hooks",
|
||||
})
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "src/components/ui/alert-dialog.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(fixturePath, "src/components/ui/button.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
const alertDialogContent = await fs.readFile(
|
||||
path.join(fixturePath, "src/components/ui/alert-dialog.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(alertDialogContent).toContain(
|
||||
'import { Button } from "#custom/components/ui/button"'
|
||||
)
|
||||
expect(alertDialogContent).toContain('import { cn } from "cn"')
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn init - custom style", async () => {
|
||||
const customRegistry = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
type: "registry:style",
|
||||
files: [
|
||||
{
|
||||
path: "path/to/foo.ts",
|
||||
content: "const foo = 'bar'",
|
||||
type: "registry:lib",
|
||||
},
|
||||
],
|
||||
cssVars: {
|
||||
theme: {
|
||||
"font-sans": "DM Sans, sans-serif",
|
||||
},
|
||||
light: {
|
||||
primary: "#dc2626",
|
||||
"foo-var": "3rem",
|
||||
},
|
||||
dark: {
|
||||
"custom-brand": "#fef3c7",
|
||||
"foo-var": "1rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "style-extended",
|
||||
type: "registry:style",
|
||||
registryDependencies: ["http://localhost:4445/r/style.json"],
|
||||
files: [
|
||||
{
|
||||
path: "path/to/foo.ts",
|
||||
content: "const foo = 'baz-qux'",
|
||||
type: "registry:lib",
|
||||
},
|
||||
],
|
||||
cssVars: {
|
||||
theme: {
|
||||
"font-sans": "Geist Sans, sans-serif",
|
||||
"font-mono": "Geist Mono, monospace",
|
||||
},
|
||||
light: {
|
||||
primary: "#059669",
|
||||
secondary: "#06b6d4",
|
||||
},
|
||||
dark: {
|
||||
"foo-var": "2rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "style-extend-none",
|
||||
type: "registry:style",
|
||||
extends: "none",
|
||||
registryDependencies: ["http://localhost:4445/r/style.json"],
|
||||
files: [
|
||||
{
|
||||
path: "path/to/foo.ts",
|
||||
content: "const foo = 'baz-qux'",
|
||||
type: "registry:lib",
|
||||
},
|
||||
],
|
||||
cssVars: {
|
||||
theme: {
|
||||
"font-sans": "Geist Sans, sans-serif",
|
||||
"font-mono": "Geist Mono, monospace",
|
||||
},
|
||||
light: {
|
||||
primary: "#059669",
|
||||
secondary: "#06b6d4",
|
||||
},
|
||||
dark: {
|
||||
"foo-var": "2rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
port: 4445,
|
||||
}
|
||||
)
|
||||
|
||||
beforeAll(async () => {
|
||||
await customRegistry.start()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await customRegistry.stop()
|
||||
})
|
||||
|
||||
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"])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("new-york")
|
||||
expect(componentsJson.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
// Install utils from shadcn.
|
||||
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
// Then add foo.ts from the custom registry.
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "lib/foo.ts"), "utf-8")
|
||||
).toBe("const foo = 'bar'")
|
||||
|
||||
const globalCssContent = await fs.readFile(
|
||||
path.join(fixturePath, "app/globals.css"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(globalCssContent).toContain("@layer base")
|
||||
expect(globalCssContent).toContain(":root")
|
||||
expect(globalCssContent).toContain(".dark")
|
||||
expect(globalCssContent).toContain("tw-animate-css")
|
||||
expect(
|
||||
cssHasProperties(globalCssContent, [
|
||||
{
|
||||
selector: "@theme inline",
|
||||
properties: {
|
||||
"--font-sans": "DM Sans, sans-serif",
|
||||
"--color-custom-brand": "var(--custom-brand)",
|
||||
"--foo-var": "var(--foo-var)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ":root",
|
||||
properties: {
|
||||
"--background": "oklch(1 0 0)",
|
||||
"--foreground": "oklch(0.145 0 0)",
|
||||
"--primary": "#dc2626",
|
||||
"--foo-var": "3rem",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ".dark",
|
||||
properties: {
|
||||
"--background": "oklch(0.145 0 0)",
|
||||
"--foreground": "oklch(0.985 0 0)",
|
||||
"--custom-brand": "#fef3c7",
|
||||
"--foo-var": "1rem",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
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",
|
||||
])
|
||||
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("new-york")
|
||||
expect(componentsJson.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
// Install utils from shadcn.
|
||||
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
// Then add foo.ts from the custom registry with overriden payload.
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "lib/foo.ts"), "utf-8")
|
||||
).toBe("const foo = 'baz-qux'")
|
||||
|
||||
const globalCssContent = await fs.readFile(
|
||||
path.join(fixturePath, "app/globals.css"),
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
expect(globalCssContent).toContain("@layer base")
|
||||
expect(globalCssContent).toContain(":root")
|
||||
expect(globalCssContent).toContain(".dark")
|
||||
expect(globalCssContent).toContain("tw-animate-css")
|
||||
|
||||
expect(
|
||||
cssHasProperties(globalCssContent, [
|
||||
{
|
||||
selector: "@theme inline",
|
||||
properties: {
|
||||
"--font-sans": "Geist Sans, sans-serif",
|
||||
"--font-mono": "Geist Mono, monospace",
|
||||
"--color-custom-brand": "var(--custom-brand)",
|
||||
"--foo-var": "var(--foo-var)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ":root",
|
||||
properties: {
|
||||
"--background": "oklch(1 0 0)",
|
||||
"--foreground": "oklch(0.145 0 0)",
|
||||
"--primary": "#059669",
|
||||
"--secondary": "#06b6d4",
|
||||
"--foo-var": "3rem",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ".dark",
|
||||
properties: {
|
||||
"--background": "oklch(0.145 0 0)",
|
||||
"--foreground": "oklch(0.985 0 0)",
|
||||
"--custom-brand": "#fef3c7",
|
||||
"--foo-var": "2rem",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
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",
|
||||
])
|
||||
|
||||
// We still expect components.json to be created.
|
||||
// With some defaults.
|
||||
const componentsJson = await fs.readJson(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
expect(componentsJson.style).toBe("new-york")
|
||||
expect(componentsJson.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
// No utils should be installed.
|
||||
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
// But we should have the foo.ts from the custom style.
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "lib/foo.ts"), "utf-8")
|
||||
).toBe("const foo = 'baz-qux'")
|
||||
|
||||
expect(
|
||||
await fs.readFile(path.join(fixturePath, "app/globals.css"), "utf-8")
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: Geist Sans, sans-serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--color-custom-brand: var(--custom-brand);
|
||||
--color-secondary: var(--secondary);
|
||||
--foo-var: var(--foo-var);
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #059669;
|
||||
--foo-var: 3rem;
|
||||
--secondary: #06b6d4;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--custom-brand: #fef3c7;
|
||||
--foo-var: 2rem;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
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"])
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson).toMatchObject({
|
||||
style: "base-nova",
|
||||
tailwind: {
|
||||
baseColor: "neutral",
|
||||
cssVariables: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should init with --defaults and components on unsupported framework", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("remix-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "button"])
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||
).toBe(true)
|
||||
|
||||
const cssPath = path.join(fixturePath, "app/globals.css")
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain("@layer base")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn init - template flag", () => {
|
||||
it("should reject invalid template", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const result = await npxShadcn(fixturePath, ["init", "-t", "invalid"])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("Invalid template")
|
||||
})
|
||||
|
||||
it("should accept valid template with --defaults", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"])
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson.style).toBe("base-nova")
|
||||
expect(componentsJson.tailwind.baseColor).toBe("neutral")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn init - --name flag", () => {
|
||||
// Use os.tmpdir() to create projects outside the monorepo tree.
|
||||
// This prevents pnpm from detecting the monorepo workspace root.
|
||||
let testBaseDir: string
|
||||
|
||||
beforeAll(async () => {
|
||||
testBaseDir = path.join(os.tmpdir(), `shadcn-name-test-${process.pid}`)
|
||||
await fs.ensureDir(testBaseDir)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.remove(testBaseDir)
|
||||
})
|
||||
|
||||
it("should create a new project with the specified name", async () => {
|
||||
const projectName = "my-named-app"
|
||||
const emptyDir = path.join(testBaseDir, "empty-next")
|
||||
await fs.ensureDir(emptyDir)
|
||||
|
||||
await npxShadcn(emptyDir, ["init", "--defaults", "--name", projectName], {
|
||||
timeout: 120000,
|
||||
})
|
||||
|
||||
const projectPath = path.join(emptyDir, projectName)
|
||||
|
||||
// Verify project was created with the correct name.
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
expect(await fs.pathExists(path.join(projectPath, "package.json"))).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
// Verify components.json was created.
|
||||
const componentsJsonPath = path.join(projectPath, "components.json")
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
|
||||
// Verify theme-provider is included from the template.
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(projectPath, "components/theme-provider.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should create a new project with --name and -t vite", async () => {
|
||||
const projectName = "my-vite-app"
|
||||
const emptyDir = path.join(testBaseDir, "empty-vite")
|
||||
await fs.ensureDir(emptyDir)
|
||||
|
||||
await npxShadcn(
|
||||
emptyDir,
|
||||
["init", "--defaults", "--name", projectName, "-t", "vite"],
|
||||
{ timeout: 120000 }
|
||||
)
|
||||
|
||||
const projectPath = path.join(emptyDir, projectName)
|
||||
|
||||
// Verify project was created.
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
|
||||
// Verify it's a vite project with the correct name.
|
||||
const packageJson = await fs.readJson(
|
||||
path.join(projectPath, "package.json")
|
||||
)
|
||||
expect(packageJson.name).toBe(projectName)
|
||||
expect(packageJson.dependencies).toHaveProperty("react")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn init - next-monorepo", () => {
|
||||
// Use os.tmpdir() to create projects outside the monorepo tree.
|
||||
// This prevents pnpm from detecting the monorepo workspace root.
|
||||
let testBaseDir: string
|
||||
const itIfNotCi = process.env.CI ? it.skip : it
|
||||
|
||||
beforeAll(async () => {
|
||||
testBaseDir = path.join(os.tmpdir(), `shadcn-monorepo-test-${process.pid}`)
|
||||
await fs.ensureDir(testBaseDir)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.remove(testBaseDir)
|
||||
})
|
||||
|
||||
itIfNotCi(
|
||||
"should create a monorepo with preset",
|
||||
async () => {
|
||||
const projectName = `test-monorepo-preset-${process.pid}`
|
||||
|
||||
const result = await npxShadcn(
|
||||
testBaseDir,
|
||||
[
|
||||
"init",
|
||||
"--name",
|
||||
projectName,
|
||||
"-t",
|
||||
"next",
|
||||
"--monorepo",
|
||||
"--preset",
|
||||
"nova",
|
||||
"--base",
|
||||
"radix",
|
||||
],
|
||||
{ timeout: 300000, debug: true }
|
||||
)
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const projectPath = path.join(testBaseDir, projectName)
|
||||
|
||||
// Verify project structure exists.
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(projectPath, "apps/web/components.json"))
|
||||
).toBe(true)
|
||||
|
||||
// Verify packages/ui/components.json is updated with preset config.
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
expect(uiConfig.style).toBe("radix-nova")
|
||||
expect(uiConfig.iconLibrary).toBe("lucide")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
// Verify apps/web/components.json is updated with preset config.
|
||||
const webConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
expect(webConfig.style).toBe("radix-nova")
|
||||
// Verify workspace aliases are preserved.
|
||||
expect(webConfig.aliases.components).toBe("@/components")
|
||||
expect(webConfig.aliases.utils).toBe("@workspace/ui/lib/utils")
|
||||
expect(webConfig.aliases.ui).toBe("@workspace/ui/components")
|
||||
|
||||
// Verify CSS was applied to packages/ui.
|
||||
const cssPath = path.join(
|
||||
projectPath,
|
||||
"packages/ui/src/styles/globals.css"
|
||||
)
|
||||
expect(await fs.pathExists(cssPath)).toBe(true)
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain("@layer base")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
expect(cssContent).toContain("--primary")
|
||||
},
|
||||
300000
|
||||
)
|
||||
|
||||
itIfNotCi(
|
||||
"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&chartColor=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next`
|
||||
|
||||
const result = await npxShadcn(
|
||||
testBaseDir,
|
||||
[
|
||||
"init",
|
||||
"--name",
|
||||
projectName,
|
||||
"-t",
|
||||
"next",
|
||||
"--monorepo",
|
||||
"--preset",
|
||||
initUrl,
|
||||
],
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
const projectPath = path.join(testBaseDir, projectName)
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
|
||||
// Verify config reflects the custom URL params.
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
expect(uiConfig.style).toBe("radix-nova")
|
||||
expect(uiConfig.iconLibrary).toBe("lucide")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
const webConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
expect(webConfig.style).toBe("radix-nova")
|
||||
expect(webConfig.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
// Verify CSS has zinc color theme applied.
|
||||
const cssPath = path.join(
|
||||
projectPath,
|
||||
"packages/ui/src/styles/globals.css"
|
||||
)
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
expect(
|
||||
cssHasProperties(cssContent, [
|
||||
{
|
||||
selector: ":root",
|
||||
properties: {
|
||||
"--background": "oklch(1 0 0)",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
},
|
||||
300000
|
||||
)
|
||||
})
|
||||
|
||||
describe("shadcn init - rtl flags", () => {
|
||||
it("should set rtl to true with --rtl", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "--rtl"])
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson.rtl).toBe(true)
|
||||
})
|
||||
|
||||
it("should set rtl to false with --no-rtl", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults", "--no-rtl"])
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson.rtl).toBe(false)
|
||||
})
|
||||
|
||||
it("should default rtl to false when no flag is passed", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const componentsJson = await fs.readJson(componentsJsonPath)
|
||||
expect(componentsJson.rtl).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
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",
|
||||
])
|
||||
|
||||
expect(result.exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("shadcn init - existing components.json", () => {
|
||||
// TODO: Revisit --force behavior. Currently it only skips backup merge,
|
||||
// but doesn't reset config values like style. Need to decide intended behavior.
|
||||
it.skip("should override existing components.json when using --force", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
// Run init with default configuration.
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
// Override style in components.json.
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const config = await fs.readJson(componentsJsonPath)
|
||||
config.style = "custom-style"
|
||||
await fs.writeJson(componentsJsonPath, config)
|
||||
|
||||
// Reinit with --force.
|
||||
await npxShadcn(fixturePath, ["init", "--force", "--defaults"])
|
||||
|
||||
const newConfig = await fs.readJson(componentsJsonPath)
|
||||
expect(newConfig.style).toBe("new-york")
|
||||
expect(newConfig.tailwind.baseColor).toBe("neutral")
|
||||
expect(await fs.pathExists(componentsJsonPath + ".bak")).toBe(false)
|
||||
})
|
||||
|
||||
it("should restore backup components.json on error", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
const existingConfig = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
style: "default",
|
||||
tailwind: {
|
||||
css: "app/globals.css",
|
||||
baseColor: "zinc",
|
||||
cssVariables: false,
|
||||
},
|
||||
rsc: true,
|
||||
tsx: true,
|
||||
aliases: {
|
||||
components: "@/components",
|
||||
utils: "@/lib/utils",
|
||||
},
|
||||
}
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
await fs.writeJson(componentsJsonPath, existingConfig)
|
||||
|
||||
// Run init with an invalid component - this should fail and restore.
|
||||
await npxShadcn(fixturePath, [
|
||||
"init",
|
||||
"invalid-component-that-does-not-exist",
|
||||
])
|
||||
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
const newConfig = await fs.readJson(componentsJsonPath)
|
||||
expect(newConfig).toMatchObject(existingConfig)
|
||||
expect(await fs.pathExists(componentsJsonPath + ".bak")).toBe(false)
|
||||
})
|
||||
|
||||
it("should preserve registries in components.json when using --force", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
// Run init with default configuration.
|
||||
await npxShadcn(fixturePath, ["init", "--defaults"])
|
||||
|
||||
// Inject a custom registries object into components.json.
|
||||
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||
const config = await fs.readJson(componentsJsonPath)
|
||||
config.registries = {
|
||||
"my-registry": { url: "https://example.com/r" },
|
||||
}
|
||||
await fs.writeJson(componentsJsonPath, config)
|
||||
|
||||
// Reinit with --force.
|
||||
await npxShadcn(fixturePath, ["init", "--force", "--defaults"])
|
||||
|
||||
// components.json should exist with no .bak leftover.
|
||||
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
|
||||
expect(await fs.pathExists(componentsJsonPath + ".bak")).toBe(false)
|
||||
|
||||
// The custom registry should be preserved.
|
||||
const newConfig = await fs.readJson(componentsJsonPath)
|
||||
expect(newConfig.registries).toMatchObject({
|
||||
"my-registry": { url: "https://example.com/r" },
|
||||
})
|
||||
|
||||
// Other config values should be from the fresh init.
|
||||
expect(newConfig.style).toBe("base-nova")
|
||||
expect(newConfig.tailwind.baseColor).toBe("neutral")
|
||||
})
|
||||
})
|
||||
1349
packages/tests/src/tests/registries.test.ts
Normal file
1349
packages/tests/src/tests/registries.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
1119
packages/tests/src/tests/search.test.ts
Normal file
1119
packages/tests/src/tests/search.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
97
packages/tests/src/tests/typeset.test.ts
Normal file
97
packages/tests/src/tests/typeset.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
// Typeset's append-stability contract: appending content must never restyle
|
||||
// content already on screen. Enforced statically — banned forward-looking
|
||||
// selectors and one-direction margins are what make the guarantee hold. The
|
||||
// contract covers the core file and every companion stylesheet.
|
||||
const read = (file: string) =>
|
||||
readFileSync(
|
||||
path.resolve(__dirname, `../../../../apps/v4/app/(app)/(typeset)/${file}`),
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
// Comments may mention banned selectors while explaining why they are banned.
|
||||
const strip = (raw: string) => raw.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
|
||||
const css = strip(read("typeset.css"))
|
||||
|
||||
const FILES: Record<string, string> = {
|
||||
"typeset.css": css,
|
||||
}
|
||||
|
||||
// Selectors whose match set changes when a sibling or child is appended.
|
||||
const FORWARD_LOOKING = [
|
||||
":last-child",
|
||||
":last-of-type",
|
||||
":nth-last-child",
|
||||
":nth-last-of-type",
|
||||
":only-child",
|
||||
":only-of-type",
|
||||
":has(",
|
||||
":empty",
|
||||
]
|
||||
|
||||
describe.each(Object.entries(FILES))(
|
||||
"%s append-stability contract",
|
||||
(file, contents) => {
|
||||
it.each(FORWARD_LOOKING)("never uses %s", (selector) => {
|
||||
expect(contents).not.toContain(selector)
|
||||
})
|
||||
|
||||
it("only spaces forward: no margin-bottom, margin-block-end always 0", () => {
|
||||
expect(contents).not.toContain("margin-bottom")
|
||||
const ends = [...contents.matchAll(/margin-block-end: ([^;]+);/g)].map(
|
||||
(match) => match[1]
|
||||
)
|
||||
for (const value of ends) {
|
||||
expect(value).toBe("0")
|
||||
}
|
||||
})
|
||||
|
||||
it("does not use the margin shorthand (hides a block-end value)", () => {
|
||||
expect(contents).not.toMatch(/[^-]margin:/)
|
||||
})
|
||||
|
||||
it("guards every element rule with the not-typeset escape", () => {
|
||||
// Element rules nest inside one guarded umbrella via &:where(); any
|
||||
// flat .typeset descendant rule outside it carries the guard itself.
|
||||
expect(contents).toMatch(
|
||||
/\.typeset\s+\*:not\(\s*:where\(\s*\.not-typeset,\s*\[data-not-typeset\],\s*\.not-typeset \*,\s*\[data-not-typeset\] \*\s*\)\s*\)\s*\{/
|
||||
)
|
||||
const nested = [...contents.matchAll(/&:where\(/g)]
|
||||
expect(nested.length).toBeGreaterThan(file === "typeset.css" ? 50 : 1)
|
||||
const selectors = [...contents.matchAll(/\.typeset[^{}]*?\{/g)]
|
||||
.map((match) => match[0])
|
||||
.filter((selector) => !/\.typeset \{$/.test(selector))
|
||||
// Opt-in utility classes (.typeset-scroll, etc.) don't need the guard.
|
||||
.filter((selector) => !selector.includes(".typeset-"))
|
||||
expect(selectors.length).toBeGreaterThan(1)
|
||||
for (const selector of selectors) {
|
||||
expect(selector).toContain(".not-typeset")
|
||||
expect(selector).toContain("[data-not-typeset]")
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe("typeset.css invariants", () => {
|
||||
it("keeps the config knobs", () => {
|
||||
for (const knob of [
|
||||
"--typeset-font-body: ",
|
||||
"--typeset-font-heading: ",
|
||||
"--typeset-font-mono: ",
|
||||
"--typeset-size: ",
|
||||
"--typeset-leading: ",
|
||||
"--typeset-flow: ",
|
||||
]) {
|
||||
expect(css).toContain(knob)
|
||||
}
|
||||
})
|
||||
|
||||
it("separates table rows on cells, not on row position", () => {
|
||||
expect(css).not.toMatch(/tr:(first|last)/)
|
||||
expect(css).toMatch(/tbody[\s\S]{0,400}border-block-start/)
|
||||
})
|
||||
})
|
||||
833
packages/tests/src/tests/view.test.ts
Normal file
833
packages/tests/src/tests/view.test.ts
Normal file
@@ -0,0 +1,833 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
|
||||
import { createFixtureTestDirectory, npxShadcn } from "../utils/helpers"
|
||||
import { configureRegistries, createRegistryServer } from "../utils/registry"
|
||||
|
||||
const registryShadcn = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
description: "A button component",
|
||||
dependencies: ["@radix-ui/react-slot"],
|
||||
devDependencies: ["@types/react"],
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/button.tsx",
|
||||
content:
|
||||
"export function Button() {\n return <button>Click me</button>\n}",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "card",
|
||||
type: "registry:ui",
|
||||
description: "A card component",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/card.tsx",
|
||||
content:
|
||||
"export function Card() {\n return <div>Card Component</div>\n}",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "alert-dialog",
|
||||
type: "registry:ui",
|
||||
registryDependencies: ["button"],
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/alert-dialog.tsx",
|
||||
content:
|
||||
"export function AlertDialog() {\n return <div>AlertDialog Component</div>\n}",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
port: 9080,
|
||||
path: "/r",
|
||||
}
|
||||
)
|
||||
|
||||
const registryOne = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "foo",
|
||||
type: "registry:component",
|
||||
description: "Foo component from registry one",
|
||||
dependencies: ["clsx", "tailwind-merge"],
|
||||
files: [
|
||||
{
|
||||
path: "components/foo.tsx",
|
||||
content:
|
||||
"export function Foo() {\n return <div>Foo Component from Registry 1</div>\n}",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
tailwind: {
|
||||
config: {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: "#ff0000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
"foo-color": "#ff0000",
|
||||
},
|
||||
dark: {
|
||||
"foo-color": "#00ff00",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bar",
|
||||
type: "registry:component",
|
||||
registryDependencies: ["@one/foo"],
|
||||
files: [
|
||||
{
|
||||
path: "components/bar.tsx",
|
||||
content:
|
||||
"export function Bar() {\n return <div>Bar Component from Registry 1</div>\n}",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "complex",
|
||||
type: "registry:component",
|
||||
registryDependencies: ["@two/item", "@one/foo"],
|
||||
files: [
|
||||
{
|
||||
path: "components/complex.tsx",
|
||||
content:
|
||||
"export function Complex() {\n return <div>Complex Component</div>\n}",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
port: 9081,
|
||||
path: "/r",
|
||||
}
|
||||
)
|
||||
|
||||
const registryTwo = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "item",
|
||||
type: "registry:ui",
|
||||
description: "Item component from registry two",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/item.tsx",
|
||||
content:
|
||||
"export function Item() {\n return <div>Item Component from Registry 2</div>\n}",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "secure-item",
|
||||
type: "registry:component",
|
||||
description: "Secure item requiring authentication",
|
||||
registryDependencies: ["@one/foo"],
|
||||
files: [
|
||||
{
|
||||
path: "components/secure-item.tsx",
|
||||
content:
|
||||
"export function SecureItem() {\n return <div>Secure Item</div>\n}",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
port: 9082,
|
||||
path: "/registry",
|
||||
}
|
||||
)
|
||||
|
||||
beforeAll(async () => {
|
||||
await registryShadcn.start()
|
||||
await registryOne.start()
|
||||
await registryTwo.start()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await registryShadcn.stop()
|
||||
await registryOne.stop()
|
||||
await registryTwo.stop()
|
||||
})
|
||||
|
||||
describe("shadcn view", () => {
|
||||
it("should view a single component from shadcn registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@shadcn": "http://localhost:9080/r/{name}",
|
||||
})
|
||||
const output = await npxShadcn(fixturePath, ["view", "button"])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
|
||||
expect(parsed[0]).toMatchObject({
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "registry/new-york-v4/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
it("should view multiple components from shadcn registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "button", "card"])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
expect(parsed).toHaveLength(2)
|
||||
expect(parsed.map((p: any) => p.name)).toEqual(["button", "card"])
|
||||
})
|
||||
|
||||
it("should view component with registry dependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "alert-dialog"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "alert-dialog",
|
||||
type: "registry:ui",
|
||||
registryDependencies: ["button"],
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "registry/new-york-v4/ui/alert-dialog.tsx",
|
||||
type: "registry:ui",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should view component from URL without needing config", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9081/r/foo.json",
|
||||
])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"cssVars": {
|
||||
"dark": {
|
||||
"foo-color": "#00ff00",
|
||||
},
|
||||
"light": {
|
||||
"foo-color": "#ff0000",
|
||||
},
|
||||
},
|
||||
"dependencies": [
|
||||
"clsx",
|
||||
"tailwind-merge",
|
||||
],
|
||||
"description": "Foo component from registry one",
|
||||
"files": [
|
||||
{
|
||||
"content": "export function Foo() {
|
||||
return <div>Foo Component from Registry 1</div>
|
||||
}",
|
||||
"path": "components/foo.tsx",
|
||||
"type": "registry:component",
|
||||
},
|
||||
],
|
||||
"name": "foo",
|
||||
"tailwind": {
|
||||
"config": {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"colors": {
|
||||
"foo": "#ff0000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"type": "registry:component",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
it("should view multiple URLs without needing config", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9081/r/foo.json",
|
||||
"http://localhost:9082/registry/item.json",
|
||||
])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
expect(parsed).toHaveLength(2)
|
||||
expect(parsed.map((p: any) => p.name)).toEqual(["foo", "item"])
|
||||
})
|
||||
|
||||
it("should view component from configured registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/foo"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "foo",
|
||||
type: "registry:component",
|
||||
description: "Foo component from registry one",
|
||||
dependencies: ["clsx", "tailwind-merge"],
|
||||
tailwind: {
|
||||
config: {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: "#ff0000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should view multiple components from different registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
"@two": "http://localhost:9082/registry/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"@one/foo",
|
||||
"@two/item",
|
||||
"button",
|
||||
])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
expect(parsed).toHaveLength(3)
|
||||
expect(parsed.map((p: any) => p.name)).toEqual(["foo", "item", "button"])
|
||||
})
|
||||
|
||||
it("should view component with nested registry dependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/bar"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "bar",
|
||||
type: "registry:component",
|
||||
registryDependencies: ["@one/foo"],
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/bar.tsx",
|
||||
type: "registry:component",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should view component with cross-registry dependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
"@two": "http://localhost:9082/registry/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/complex"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "complex",
|
||||
type: "registry:component",
|
||||
registryDependencies: expect.arrayContaining(["@two/item", "@one/foo"]),
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/complex.tsx",
|
||||
type: "registry:component",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle authentication for secured registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@two": {
|
||||
url: "http://localhost:9082/registry/bearer/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer EXAMPLE_BEARER_TOKEN",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@two/secure-item"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "secure-item",
|
||||
type: "registry:component",
|
||||
description: "Secure item requiring authentication",
|
||||
registryDependencies: ["@one/foo"],
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/secure-item.tsx",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should fail when viewing secured item without authentication", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@two": "http://localhost:9082/registry/bearer/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@two/secure-item"])
|
||||
expect(output.stdout).toContain("Unauthorized")
|
||||
})
|
||||
|
||||
it("should handle authentication with environment variables", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
"@two": {
|
||||
url: "http://localhost:9082/registry/bearer/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer ${BEARER_TOKEN}",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
process.env.BEARER_TOKEN = "EXAMPLE_BEARER_TOKEN"
|
||||
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"@two/secure-item",
|
||||
"@one/foo",
|
||||
])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "secure-item",
|
||||
type: "registry:component",
|
||||
registryDependencies: ["@one/foo"],
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
type: "registry:component",
|
||||
dependencies: ["clsx", "tailwind-merge"],
|
||||
},
|
||||
])
|
||||
|
||||
delete process.env.BEARER_TOKEN
|
||||
})
|
||||
|
||||
it("should mix URLs and named components", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9082/registry/item.json",
|
||||
"@one/foo",
|
||||
"button",
|
||||
])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
expect(parsed).toHaveLength(3)
|
||||
expect(parsed.map((p: any) => p.name)).toEqual(["item", "foo", "button"])
|
||||
})
|
||||
|
||||
it("should handle non-existent component gracefully", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "non-existent"])
|
||||
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle non-existent registry gracefully", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "@unknown/component"])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@unknown"')
|
||||
})
|
||||
|
||||
it("should work with @shadcn namespace", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"@shadcn/button",
|
||||
"@shadcn/card",
|
||||
])
|
||||
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
expect(parsed).toHaveLength(2)
|
||||
expect(parsed.map((p: any) => p.name)).toEqual(["button", "card"])
|
||||
})
|
||||
|
||||
it("should handle 404 for non-existent URL", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9081/r/does-not-exist.json",
|
||||
])
|
||||
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle multiple errors in batch", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"non-existent",
|
||||
"@one/does-not-exist",
|
||||
])
|
||||
|
||||
// Should fail on first error - non-existent component
|
||||
expect(output.stdout.toLowerCase()).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle invalid URL format", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, ["view", "not-a-valid-url"])
|
||||
|
||||
// With defaults in place, it will try to fetch as a component and fail
|
||||
expect(output.stdout.toLowerCase()).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle network timeouts gracefully", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9999/timeout.json", // Non-existent server
|
||||
])
|
||||
|
||||
// Check for connection error in the output
|
||||
expect(output.stdout.toLowerCase()).toContain("failed, reason:")
|
||||
})
|
||||
|
||||
it("should handle mixed success and failure", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"button",
|
||||
"non-existent-component",
|
||||
])
|
||||
|
||||
// Should fail fast on first error
|
||||
expect(output.stdout).not.toContain('"name": "button"')
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle missing environment variables for authenticated registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
await configureRegistries(fixturePath, {
|
||||
"@auth": {
|
||||
url: "http://localhost:9082/registry/bearer/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer ${MISSING_ENV_VAR}",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@auth/secure-item"])
|
||||
|
||||
expect(output.stdout).toContain("MISSING_ENV_VAR")
|
||||
})
|
||||
|
||||
it("should handle validation errors", async () => {
|
||||
// Create a server that returns invalid schema
|
||||
const badServer = await createRegistryServer(
|
||||
[
|
||||
{
|
||||
name: "invalid-schema",
|
||||
type: "invalid-type", // Invalid type
|
||||
files: [
|
||||
{
|
||||
path: "components/bad.tsx",
|
||||
content: "export function Bad() { return null }",
|
||||
// Missing required 'type' field
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
],
|
||||
{
|
||||
port: 9083,
|
||||
path: "/bad",
|
||||
}
|
||||
)
|
||||
|
||||
await badServer.start()
|
||||
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"http://localhost:9083/bad/invalid-schema.json",
|
||||
])
|
||||
|
||||
// Should handle validation error
|
||||
expect(output.stdout.toLowerCase()).toContain("failed to parse")
|
||||
|
||||
await badServer.stop()
|
||||
})
|
||||
|
||||
it("should handle dependencies that require authentication", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
// Configure both registries - @two requires auth, @one doesn't
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
"@two": {
|
||||
url: "http://localhost:9082/registry/bearer/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer EXAMPLE_BEARER_TOKEN",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// complex depends on @two/item which requires auth
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/complex"])
|
||||
|
||||
// Should just show the component metadata, not fail on auth
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "complex",
|
||||
type: "registry:component",
|
||||
registryDependencies: expect.arrayContaining(["@two/item", "@one/foo"]),
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/complex.tsx",
|
||||
type: "registry:component",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should fail when viewing component with unauthenticated dependencies", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
// Configure registries - @two requires auth but no token provided
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
"@two": "http://localhost:9082/registry/bearer/{name}",
|
||||
})
|
||||
|
||||
// Try to view complex which depends on @two/item (requires auth)
|
||||
// Note: This should succeed for view command as it just shows metadata
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/complex"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "complex",
|
||||
type: "registry:component",
|
||||
registryDependencies: expect.arrayContaining(["@two/item"]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should view authenticated dependencies when directly requested", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
// Configure with proper auth
|
||||
await configureRegistries(fixturePath, {
|
||||
"@two": {
|
||||
url: "http://localhost:9082/registry/bearer/{name}",
|
||||
headers: {
|
||||
Authorization: "Bearer EXAMPLE_BEARER_TOKEN",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Directly view the authenticated item
|
||||
const output = await npxShadcn(fixturePath, ["view", "@two/item"])
|
||||
|
||||
expect(JSON.parse(output.stdout)).toMatchObject([
|
||||
{
|
||||
name: "item",
|
||||
type: "registry:ui",
|
||||
description: "Item component from registry two",
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/ui/item.tsx",
|
||||
type: "registry:ui",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should view component with all metadata fields", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/foo"])
|
||||
const parsed = JSON.parse(output.stdout)
|
||||
|
||||
expect(parsed[0]).toMatchObject({
|
||||
name: "foo",
|
||||
type: "registry:component",
|
||||
description: "Foo component from registry one",
|
||||
dependencies: ["clsx", "tailwind-merge"],
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "components/foo.tsx",
|
||||
type: "registry:component",
|
||||
}),
|
||||
]),
|
||||
tailwind: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
theme: expect.objectContaining({
|
||||
extend: expect.objectContaining({
|
||||
colors: expect.objectContaining({
|
||||
foo: "#ff0000",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cssVars: expect.objectContaining({
|
||||
light: expect.objectContaining({
|
||||
"foo-color": "#ff0000",
|
||||
}),
|
||||
dark: expect.objectContaining({
|
||||
"foo-color": "#00ff00",
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle namespace with special characters", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "@test-123/component"])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@test-123"')
|
||||
})
|
||||
|
||||
it("should handle empty component name in namespace", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "@shadcn/"])
|
||||
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle namespace without @ prefix", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "one/foo"])
|
||||
|
||||
// Without @ prefix, it's treated as a regular component name
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should handle double namespace", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "@@test/component"])
|
||||
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
|
||||
it("should two error for unknown registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, ["view", "@test/component"])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@test"')
|
||||
})
|
||||
|
||||
it("should two error for unknown registry not in first position", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"@shadcn/component",
|
||||
"@does-not-exist/component",
|
||||
])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@does-not-exist"')
|
||||
})
|
||||
|
||||
it("should handle namespace with multiple slashes", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app-init")
|
||||
const output = await npxShadcn(fixturePath, [
|
||||
"view",
|
||||
"@test/path/to/component",
|
||||
])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@test"')
|
||||
})
|
||||
|
||||
it("should view from components.json with registries config only", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/foo"])
|
||||
|
||||
expect(output.stdout).toContain("Foo component from registry one")
|
||||
})
|
||||
|
||||
it("should error when viewing from non-existent registry with configured registries", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
// Configure @one registry, but try to view from @two
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
const output = await npxShadcn(fixturePath, ["view", "@two/item"])
|
||||
|
||||
expect(output.stdout).toContain('Unknown registry "@two"')
|
||||
})
|
||||
|
||||
it("should error when viewing non-existent item from configured registry", async () => {
|
||||
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||
|
||||
// Configure @one registry
|
||||
await configureRegistries(fixturePath, {
|
||||
"@one": "http://localhost:9081/r/{name}",
|
||||
})
|
||||
|
||||
// Try to view an item that doesn't exist in @one registry
|
||||
const output = await npxShadcn(fixturePath, ["view", "@one/does-not-exist"])
|
||||
|
||||
expect(output.stdout).toContain("not found")
|
||||
})
|
||||
})
|
||||
115
packages/tests/src/utils/helpers.ts
Normal file
115
packages/tests/src/utils/helpers.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { randomUUID } from "crypto"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
|
||||
import { TEMP_DIR } from "./setup"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const FIXTURES_DIR = path.join(__dirname, "../../fixtures")
|
||||
const SHADCN_CLI_PATH = path.join(__dirname, "../../../shadcn/dist/index.js")
|
||||
const TEMPLATES_DIR = path.join(__dirname, "../../../../templates")
|
||||
|
||||
export function getRegistryUrl() {
|
||||
return process.env.REGISTRY_URL || "http://localhost:4000/r"
|
||||
}
|
||||
|
||||
export async function createFixtureTestDirectory(fixtureName: string) {
|
||||
const fixturePath = path.join(FIXTURES_DIR, fixtureName)
|
||||
|
||||
const uniqueId = `${process.pid}-${randomUUID().substring(0, 8)}`
|
||||
let testDir = path.join(TEMP_DIR, `test-${uniqueId}-${fixtureName}`)
|
||||
|
||||
await fs.ensureDir(testDir)
|
||||
await fs.copy(fixturePath, testDir)
|
||||
|
||||
return testDir
|
||||
}
|
||||
|
||||
export async function runCommand(
|
||||
cwd: string,
|
||||
args: string[],
|
||||
options?: {
|
||||
env?: Record<string, string>
|
||||
input?: string
|
||||
timeout?: number
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const childProcess = execa("node", [SHADCN_CLI_PATH, ...args], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "0",
|
||||
CI: "true",
|
||||
...options?.env,
|
||||
},
|
||||
input: options?.input,
|
||||
reject: false,
|
||||
timeout: options?.timeout ?? 60000,
|
||||
})
|
||||
|
||||
const result = await childProcess
|
||||
|
||||
return {
|
||||
stdout: result.stdout || "",
|
||||
stderr: result.stderr || "",
|
||||
exitCode: result.exitCode ?? 0,
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || error.message || "",
|
||||
exitCode: error.exitCode ?? 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function npxShadcn(
|
||||
cwd: string,
|
||||
args: string[],
|
||||
{
|
||||
debug = false,
|
||||
input,
|
||||
timeout,
|
||||
}: {
|
||||
debug?: boolean
|
||||
input?: string
|
||||
timeout?: number
|
||||
} = {}
|
||||
) {
|
||||
const result = await runCommand(cwd, args, {
|
||||
env: {
|
||||
REGISTRY_URL: getRegistryUrl(),
|
||||
SHADCN_TEMPLATE_DIR: TEMPLATES_DIR,
|
||||
},
|
||||
input,
|
||||
timeout,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function cssHasProperties(
|
||||
cssContent: string,
|
||||
checks: Array<{
|
||||
selector: string
|
||||
properties: Record<string, string>
|
||||
}>
|
||||
) {
|
||||
return checks.every(({ selector, properties }) => {
|
||||
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
const regex = new RegExp(`${escapedSelector}\\s*{([^}]+)}`, "s")
|
||||
const match = cssContent.match(regex)
|
||||
const block = match ? match[1] : ""
|
||||
|
||||
return Object.entries(properties).every(([property, value]) =>
|
||||
block.includes(`${property}: ${value};`)
|
||||
)
|
||||
})
|
||||
}
|
||||
273
packages/tests/src/utils/registry.ts
Normal file
273
packages/tests/src/utils/registry.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { createServer } from "http"
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
|
||||
export async function createRegistryServer(
|
||||
items: Array<{ name: string; type: string } & Record<string, unknown>>,
|
||||
{
|
||||
port = 4444,
|
||||
path = "/r",
|
||||
}: {
|
||||
port?: number
|
||||
path?: string
|
||||
}
|
||||
) {
|
||||
const server = createServer((request, response) => {
|
||||
const urlRaw = request.url?.split("?")[0]
|
||||
|
||||
// Handle registries.json endpoint (don't strip .json for this one).
|
||||
if (urlRaw?.endsWith("/registries.json")) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
// Return empty registry array for tests - we want to test manual configuration.
|
||||
response.end(JSON.stringify([]))
|
||||
return
|
||||
}
|
||||
|
||||
// For other endpoints, strip .json extension
|
||||
const urlWithoutQuery = urlRaw?.replace(/\.json$/, "")
|
||||
|
||||
if (urlWithoutQuery?.includes("icons/index")) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
AlertCircle: {
|
||||
lucide: "AlertCircle",
|
||||
radix: "ExclamationTriangleIcon",
|
||||
},
|
||||
ArrowLeft: {
|
||||
lucide: "ArrowLeft",
|
||||
radix: "ArrowLeftIcon",
|
||||
},
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (urlWithoutQuery?.includes("colors/neutral")) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
inlineColors: {
|
||||
light: {
|
||||
background: "white",
|
||||
foreground: "neutral-950",
|
||||
},
|
||||
dark: {
|
||||
background: "neutral-950",
|
||||
foreground: "neutral-50",
|
||||
},
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: "0 0% 100%",
|
||||
foreground: "0 0% 3.9%",
|
||||
},
|
||||
dark: {
|
||||
background: "0 0% 3.9%",
|
||||
foreground: "0 0% 98%",
|
||||
},
|
||||
},
|
||||
cssVarsV4: {
|
||||
light: {
|
||||
background: "oklch(1 0 0)",
|
||||
foreground: "oklch(0.145 0 0)",
|
||||
},
|
||||
dark: {
|
||||
background: "oklch(0.145 0 0)",
|
||||
foreground: "oklch(0.985 0 0)",
|
||||
},
|
||||
},
|
||||
inlineColorsTemplate:
|
||||
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n ",
|
||||
cssVarsTemplate:
|
||||
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer ",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (urlWithoutQuery?.includes("styles/index")) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify([
|
||||
{
|
||||
name: "new-york",
|
||||
label: "New York",
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
label: "Default",
|
||||
},
|
||||
])
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Match /styles/{style}/index for the registry style index (e.g., /styles/new-york-v4/index).
|
||||
if (urlWithoutQuery?.match(/\/styles\/[^/]+\/index$/)) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
name: "index",
|
||||
type: "registry:style",
|
||||
registryDependencies: ["utils"],
|
||||
cssVars: {},
|
||||
files: [],
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Match /r/index for the registry index (but NOT paths like /styles/foo/index).
|
||||
if (urlWithoutQuery?.match(/\/r\/index$/)) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify([
|
||||
{
|
||||
name: "alert-dialog",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/alert-dialog.tsx",
|
||||
type: "registry:ui",
|
||||
content:
|
||||
"export function AlertDialog() { return <div>AlertDialog Component from Registry Shadcn</div> }",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
content:
|
||||
"export function Button() { return <div>Button Component from Registry Shadcn</div> }",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a registry.json request
|
||||
if (urlWithoutQuery?.endsWith("/registry")) {
|
||||
// Check if this requires bearer authentication
|
||||
if (request.url?.includes("/bearer/")) {
|
||||
// Validate bearer token
|
||||
const token = request.headers.authorization?.split(" ")[1]
|
||||
if (token !== "EXAMPLE_BEARER_TOKEN") {
|
||||
response.writeHead(401, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
name: "Test Registry",
|
||||
homepage: "https://example.com",
|
||||
items: items,
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const match = urlWithoutQuery?.match(
|
||||
new RegExp(`^${path}/(?:.*/)?([^/]+)$`)
|
||||
)
|
||||
const itemName = match?.[1]
|
||||
const item = itemName
|
||||
? items.find((item) => item.name === itemName)
|
||||
: undefined
|
||||
|
||||
if (!item) {
|
||||
response.writeHead(404, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Item not found" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (request.url?.includes("/bearer/")) {
|
||||
// Validate bearer token
|
||||
const token = request.headers.authorization?.split(" ")[1]
|
||||
if (token !== "EXAMPLE_BEARER_TOKEN") {
|
||||
response.writeHead(401, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (request.url?.includes("/api-key/")) {
|
||||
// Validate api key
|
||||
const apiKey = request.headers["x-api-key"]
|
||||
if (apiKey !== "EXAMPLE_API_KEY") {
|
||||
response.writeHead(401, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (request.url?.includes("/client-secret/")) {
|
||||
// Validate client secret
|
||||
const clientSecret = request.headers["x-client-secret"]
|
||||
const clientId = request.headers["x-client-id"]
|
||||
if (
|
||||
clientSecret !== "EXAMPLE_CLIENT_SECRET" ||
|
||||
clientId !== "EXAMPLE_CLIENT_ID"
|
||||
) {
|
||||
response.writeHead(401, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (request.url?.includes("/params/")) {
|
||||
const token = request.url.split("?")[1]?.split("=")[1]
|
||||
if (token !== "EXAMPLE_REGISTRY_TOKEN") {
|
||||
response.writeHead(401, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.writeHead(200, { "Content-Type": "application/json" })
|
||||
response.end(JSON.stringify(item))
|
||||
})
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(port, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
stop: async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureRegistries(
|
||||
fixturePath: string,
|
||||
payload: Record<string, any>
|
||||
) {
|
||||
if (!fs.pathExistsSync(path.join(fixturePath, "components.json"))) {
|
||||
await fs.writeJSON(path.join(fixturePath, "components.json"), {
|
||||
registries: payload,
|
||||
})
|
||||
}
|
||||
|
||||
const componentsJson = await fs.readJSON(
|
||||
path.join(fixturePath, "components.json")
|
||||
)
|
||||
componentsJson.registries = payload
|
||||
await fs.writeJSON(path.join(fixturePath, "components.json"), componentsJson)
|
||||
}
|
||||
67
packages/tests/src/utils/setup.ts
Normal file
67
packages/tests/src/utils/setup.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import { rimraf } from "rimraf"
|
||||
|
||||
export const TEMP_DIR = path.join(__dirname, "../../temp")
|
||||
|
||||
const SHADCN_CLI_PATH = path.join(__dirname, "../../../shadcn/dist/index.js")
|
||||
|
||||
async function waitForCondition(
|
||||
label: string,
|
||||
check: () => Promise<boolean>,
|
||||
timeoutMs = 60000
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
if (await check()) return
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
throw new Error(`Timed out waiting for: ${label} (${timeoutMs}ms)`)
|
||||
}
|
||||
|
||||
export default async function setup() {
|
||||
await fs.ensureDir(TEMP_DIR)
|
||||
|
||||
// The v4 dev script runs `pnpm --filter=shadcn build` in the background
|
||||
// while `next dev` starts immediately. On fast CI runs the server can be
|
||||
// ready before the CLI binary is built, so we wait for it explicitly.
|
||||
await waitForCondition("shadcn CLI binary", () =>
|
||||
fs.pathExists(SHADCN_CLI_PATH)
|
||||
)
|
||||
|
||||
// The CLI's first request goes to the dynamic /init route. On a cold Next.js
|
||||
// dev server, this route needs to be compiled on first access (~1.8s). That
|
||||
// compilation time, on top of everything else init does, pushes the first
|
||||
// test over the 30s CLI timeout. Warming up the route here ensures it is
|
||||
// already compiled before any test starts.
|
||||
const registryUrl = process.env.REGISTRY_URL || "http://localhost:4000/r"
|
||||
const shadcnUrl = registryUrl.replace(/\/r\/?$/, "")
|
||||
const initWarmupUrl = `${shadcnUrl}/init?base=base&style=nova&baseColor=neutral&theme=neutral&iconLibrary=lucide&font=geist&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next`
|
||||
|
||||
await waitForCondition("init route warm-up", async () => {
|
||||
try {
|
||||
const res = await fetch(initWarmupUrl)
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// The CLI fetches registry paths that may not exist (e.g. font files),
|
||||
// causing Next.js to compile the /_not-found/page route on first 404.
|
||||
// That compilation takes ~4-5s and contributes to the first test timing
|
||||
// out. Trigger one 404 here so the not-found page is pre-compiled.
|
||||
try {
|
||||
await fetch(`${shadcnUrl}/r/styles/new-york-v4/font-geist.json`)
|
||||
} catch {
|
||||
// Best effort — don't block setup if this fails.
|
||||
}
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
await rimraf(TEMP_DIR)
|
||||
} catch (error) {
|
||||
console.error("Failed to clean up temp directory:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
packages/tests/tsconfig.json
Normal file
19
packages/tests/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "temp", "fixtures"]
|
||||
}
|
||||
19
packages/tests/vitest.config.ts
Normal file
19
packages/tests/vitest.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120000,
|
||||
hookTimeout: 120000,
|
||||
globals: true,
|
||||
environment: "node",
|
||||
globalSetup: ["./src/utils/setup.ts"],
|
||||
maxConcurrency: 4,
|
||||
isolate: false,
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user