Merge branch 'main' of github.com:shadcn-ui/ui

This commit is contained in:
shadcn
2025-10-24 10:44:39 +04:00
12 changed files with 689 additions and 12 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": minor
---
add Next.js 16 support for init command

View File

@@ -89,7 +89,7 @@
"recharts": "2.15.1",
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"shadcn": "3.4.2",
"shadcn": "3.5.0",
"shiki": "^1.10.1",
"sonner": "^2.0.0",
"tailwind-merge": "^3.0.1",

View File

@@ -17,7 +17,7 @@
"@clerk": "https://clerk.com/r/{name}.json",
"@coss": "https://coss.com/ui/r/{name}.json",
"@cult-ui": "https://cult-ui.com/r/{name}.json",
"@efferd-ui": "https://ui.efferd.com/r/{name}.json",
"@efferd": "https://efferd.com/r/{name}.json",
"@eldoraui": "https://eldoraui.site/r/{name}.json",
"@elements": "https://tryelements.dev/r/{name}.json",
"@elevenlabs-ui": "https://ui.elevenlabs.io/r/{name}.json",

View File

@@ -88,7 +88,7 @@
"react-resizable-panels": "^2.0.22",
"react-wrap-balancer": "^0.4.1",
"recharts": "2.12.7",
"shadcn": "3.4.2",
"shadcn": "3.5.0",
"sharp": "^0.32.6",
"sonner": "^1.2.3",
"swr": "2.2.6-beta.3",

View File

@@ -1,5 +1,13 @@
# @shadcn/ui
## 3.5.0
### Minor Changes
- [#8555](https://github.com/shadcn-ui/ui/pull/8555) [`d7e0dc3ec81676d47351e8f7134639e0dd0c3e3c`](https://github.com/shadcn-ui/ui/commit/d7e0dc3ec81676d47351e8f7134639e0dd0c3e3c) Thanks [@shadcn](https://github.com/shadcn)! - rename middleware to proxy for Next.js 16
- [#8550](https://github.com/shadcn-ui/ui/pull/8550) [`6bddba986d75da35e18343dbb6254fed4537b7d7`](https://github.com/shadcn-ui/ui/commit/6bddba986d75da35e18343dbb6254fed4537b7d7) Thanks [@shadcn](https://github.com/shadcn)! - add Next.js 16 support for init command
## 3.4.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "3.4.2",
"version": "3.5.0",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"

View File

@@ -18,6 +18,7 @@ export type ProjectInfo = {
tailwindConfigFile: string | null
tailwindCssFile: string | null
tailwindVersion: TailwindVersion
frameworkVersion: string | null
aliasPrefix: string | null
}
@@ -75,6 +76,7 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
tailwindConfigFile,
tailwindCssFile,
tailwindVersion,
frameworkVersion: null,
aliasPrefix,
}
@@ -84,6 +86,10 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
? FRAMEWORKS["next-app"]
: FRAMEWORKS["next-pages"]
type.isRSC = isUsingAppDir
type.frameworkVersion = await getFrameworkVersion(
type.framework,
packageJson
)
return type
}
@@ -165,6 +171,42 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
return type
}
export async function getFrameworkVersion(
framework: Framework,
packageJson: ReturnType<typeof getPackageInfo>
) {
if (!packageJson) {
return null
}
// Only detect Next.js version for now.
if (!["next-app", "next-pages"].includes(framework.name)) {
return null
}
const version =
packageJson.dependencies?.next || packageJson.devDependencies?.next
if (!version) {
return null
}
// Extract full semver (major.minor.patch), handling ^, ~, etc.
const versionMatch = version.match(/^[\^~]?(\d+\.\d+\.\d+)/)
if (versionMatch) {
return versionMatch[1] // e.g., "16.0.0"
}
// For ranges like ">=15.0.0 <16.0.0", extract the first version.
const rangeMatch = version.match(/(\d+\.\d+\.\d+)/)
if (rangeMatch) {
return rangeMatch[1]
}
// For "latest", "canary", "rc", etc., return the tag as-is.
return version
}
export async function getTailwindVersion(
cwd: string
): Promise<ProjectInfo["tailwindVersion"]> {

View File

@@ -0,0 +1,424 @@
import { FRAMEWORKS } from "@/src/utils/frameworks"
import { type Config } from "@/src/utils/get-config"
import { transformNext } from "@/src/utils/transformers/transform-next"
import { describe, expect, test, vi } from "vitest"
import { transform } from "../transformers"
const testConfig: Config = {
style: "new-york",
tsx: true,
rsc: true,
tailwind: {
baseColor: "neutral",
cssVariables: true,
config: "tailwind.config.ts",
css: "tailwind.css",
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
resolvedPaths: {
cwd: "/test-project",
components: "/test-project/components",
utils: "/test-project/lib/utils",
ui: "/test-project/ui",
lib: "/test-project/lib",
hooks: "/test-project/hooks",
tailwindConfig: "tailwind.config.ts",
tailwindCss: "tailwind.css",
},
}
vi.mock("@/src/utils/get-project-info", () => ({
getProjectInfo: vi.fn(),
}))
describe("transformNext", () => {
describe("Next.js 16+ transformations", () => {
test("should transform function declaration export", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
expect(
await transform(
{
filename: "middleware.ts",
raw: `import { NextResponse } from "next/server"
export function middleware(request: Request) {
return NextResponse.next()
}`,
config: testConfig,
},
[transformNext]
)
).toMatchInlineSnapshot(`
"import { NextResponse } from "next/server"
export function proxy(request: Request) {
return NextResponse.next()
}"
`)
})
test("should transform async function declaration", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.1.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
expect(
await transform(
{
filename: "middleware.ts",
raw: `import { NextResponse } from "next/server"
export async function middleware(request: Request) {
return NextResponse.next()
}`,
config: testConfig,
},
[transformNext]
)
).toMatchInlineSnapshot(`
"import { NextResponse } from "next/server"
export async function proxy(request: Request) {
return NextResponse.next()
}"
`)
})
test("should transform const arrow function export", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
expect(
await transform(
{
filename: "middleware.ts",
raw: `import { NextResponse } from "next/server"
export const middleware = (request: Request) => {
return NextResponse.next()
}`,
config: testConfig,
},
[transformNext]
)
).toMatchInlineSnapshot(`
"import { NextResponse } from "next/server"
export const proxy = (request: Request) => {
return NextResponse.next()
}"
`)
})
test("should transform named export with alias", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
expect(
await transform(
{
filename: "middleware.ts",
raw: `import { NextResponse } from "next/server"
function handler(request: Request) {
return NextResponse.next()
}
export { handler as middleware }`,
config: testConfig,
},
[transformNext]
)
).toMatchInlineSnapshot(`
"import { NextResponse } from "next/server"
function handler(request: Request) {
return NextResponse.next()
}
export { handler as proxy }"
`)
})
})
describe("Next.js < 16 or unknown versions (no transformation)", () => {
test("should not transform for Next.js 15", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "15.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `import { NextResponse } from "next/server"
export function middleware(request: Request) {
return NextResponse.next()
}`
expect(
await transform(
{
filename: "middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for Next.js 15
)
).toBe(input)
})
test("should not transform when frameworkVersion is null", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: null,
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `import { NextResponse } from "next/server"
export function middleware(request: Request) {
return NextResponse.next()
}`
expect(
await transform(
{
filename: "middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext when frameworkVersion is null
)
).toBe(input)
})
test("should not transform for canary tag (unknown version)", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "canary",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `import { NextResponse } from "next/server"
export function middleware(request: Request) {
return NextResponse.next()
}`
expect(
await transform(
{
filename: "middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for canary tag
)
).toBe(input)
})
test("should not transform for latest tag (unknown version)", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "latest",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `import { NextResponse } from "next/server"
export function middleware(request: Request) {
return NextResponse.next()
}`
expect(
await transform(
{
filename: "middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for latest tag
)
).toBe(input)
})
})
describe("Non-middleware files", () => {
test("should not transform non-middleware files", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `export function middleware() {
return "not a middleware file"
}`
expect(
await transform(
{
filename: "utils.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for non-middleware files
)
).toBe(input)
})
test("should not transform nested middleware files", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["next-app"],
frameworkVersion: "16.0.0",
isSrcDir: false,
isRSC: true,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `export function middleware() {
return "nested middleware"
}`
// Nested middleware files should not be transformed
expect(
await transform(
{
filename: "lib/middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for nested middleware files
)
).toBe(input)
expect(
await transform(
{
filename: "lib/supabase/middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for nested middleware files
)
).toBe(input)
})
})
describe("Non-Next.js projects", () => {
test("should not transform for Vite projects", async () => {
const { getProjectInfo } = await import("@/src/utils/get-project-info")
vi.mocked(getProjectInfo).mockResolvedValue({
framework: FRAMEWORKS["vite"],
frameworkVersion: null,
isSrcDir: false,
isRSC: false,
isTsx: true,
tailwindConfigFile: null,
tailwindCssFile: null,
tailwindVersion: "v4",
aliasPrefix: "@",
})
const input = `export function middleware() {
return "some middleware"
}`
expect(
await transform(
{
filename: "middleware.ts",
raw: input,
config: testConfig,
},
[] // Don't include transformNext for non-Next.js projects
)
).toBe(input)
})
})
})

View File

@@ -0,0 +1,33 @@
import { Transformer } from "@/src/utils/transformers"
export const transformNext: Transformer = async ({ sourceFile }) => {
// export function middleware.
sourceFile.getFunctions().forEach((func) => {
if (func.getName() === "middleware") {
func.rename("proxy")
}
})
// export const middleware.
sourceFile.getVariableDeclarations().forEach((variable) => {
if (variable.getName() === "middleware") {
variable.rename("proxy")
}
})
// export { handler as middleware }.
sourceFile.getExportDeclarations().forEach((exportDecl) => {
const namedExports = exportDecl.getNamedExports()
namedExports.forEach((namedExport) => {
if (namedExport.getName() === "middleware") {
namedExport.setName("proxy")
}
const aliasNode = namedExport.getAliasNode()
if (aliasNode?.getText() === "middleware") {
namedExport.setAlias("proxy")
}
})
})
return sourceFile
}

View File

@@ -21,6 +21,7 @@ import { transform } from "@/src/utils/transformers"
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
import { transformIcons } from "@/src/utils/transformers/transform-icons"
import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformNext } from "@/src/utils/transformers/transform-next"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
import prompts from "prompts"
@@ -138,6 +139,9 @@ export async function updateFiles(
transformCssVars,
transformTwPrefixes,
transformIcons,
...(_isNext16Middleware(filePath, projectInfo, config)
? [transformNext]
: []),
]
)
@@ -186,6 +190,11 @@ export async function updateFiles(
}
}
// Rename middleware.ts to proxy.ts for Next.js 16+.
if (_isNext16Middleware(filePath, projectInfo, config)) {
filePath = filePath.replace(/middleware\.(ts|js)$/, "proxy.$1")
}
// Create the target directory if it doesn't exist.
if (!existsSync(targetDir)) {
await fs.mkdir(targetDir, { recursive: true })
@@ -715,3 +724,26 @@ export function toAliasedImport(
// but usually config.aliases already include it.
return `${aliasBase}${suffix}${keepExt}`
}
function _isNext16Middleware(
filePath: string,
projectInfo: ProjectInfo | null,
config: Config
) {
const isRootMiddleware =
filePath === path.join(config.resolvedPaths.cwd, "middleware.ts") ||
filePath === path.join(config.resolvedPaths.cwd, "middleware.js")
const isNextJs =
projectInfo?.framework.name === "next-app" ||
projectInfo?.framework.name === "next-pages"
if (!isRootMiddleware || !isNextJs || !projectInfo?.frameworkVersion) {
return false
}
const majorVersion = parseInt(projectInfo.frameworkVersion.split(".")[0])
const isNext16Plus = !isNaN(majorVersion) && majorVersion >= 16
return isNext16Plus
}

View File

@@ -2,7 +2,10 @@ import path from "path"
import { describe, expect, test } from "vitest"
import { FRAMEWORKS } from "../../src/utils/frameworks"
import { getProjectInfo } from "../../src/utils/get-project-info"
import {
getFrameworkVersion,
getProjectInfo,
} from "../../src/utils/get-project-info"
describe("get project info", async () => {
test.each([
@@ -16,6 +19,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "app/globals.css",
tailwindVersion: "v3",
frameworkVersion: null,
aliasPrefix: "@",
},
},
@@ -29,6 +33,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/app/styles.css",
tailwindVersion: "v3",
frameworkVersion: null,
aliasPrefix: "#",
},
},
@@ -42,6 +47,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "styles/globals.css",
tailwindVersion: "v4",
frameworkVersion: null,
aliasPrefix: "~",
},
},
@@ -55,6 +61,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/styles/globals.css",
tailwindVersion: "v4",
frameworkVersion: null,
aliasPrefix: "@",
},
},
@@ -68,6 +75,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/styles/globals.css",
tailwindVersion: "v3",
frameworkVersion: "14.2.4",
aliasPrefix: "~",
},
},
@@ -81,6 +89,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "src/styles/globals.css",
tailwindVersion: "v3",
frameworkVersion: "13.4.2",
aliasPrefix: "~",
},
},
@@ -94,6 +103,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "app/tailwind.css",
tailwindVersion: "v3",
frameworkVersion: null,
aliasPrefix: "~",
},
},
@@ -107,6 +117,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.ts",
tailwindCssFile: "app/tailwind.css",
tailwindVersion: "v3",
frameworkVersion: null,
aliasPrefix: "~",
},
},
@@ -120,6 +131,7 @@ describe("get project info", async () => {
tailwindConfigFile: "tailwind.config.js",
tailwindCssFile: "src/index.css",
tailwindVersion: "v3",
frameworkVersion: null,
aliasPrefix: null,
},
},
@@ -131,3 +143,134 @@ describe("get project info", async () => {
).toStrictEqual(type)
})
})
describe("getFrameworkVersion", () => {
describe("Next.js version detection", () => {
test.each([
{
name: "exact semver",
input: "16.0.0",
framework: "next-app",
expected: "16.0.0",
},
{
name: "caret prefix",
input: "^16.1.2",
framework: "next-app",
expected: "16.1.2",
},
{
name: "tilde prefix",
input: "~15.0.3",
framework: "next-app",
expected: "15.0.3",
},
{
name: "version range",
input: ">=15.0.0 <16.0.0",
framework: "next-app",
expected: "15.0.0",
},
{
name: "latest tag",
input: "latest",
framework: "next-app",
expected: "latest",
},
{
name: "canary tag",
input: "canary",
framework: "next-app",
expected: "canary",
},
{
name: "rc tag",
input: "rc",
framework: "next-app",
expected: "rc",
},
])(
`should extract $name ($input) -> $expected`,
async ({ input, framework, expected }) => {
const packageJson = {
dependencies: {
next: input,
},
}
const version = await getFrameworkVersion(
FRAMEWORKS[framework as keyof typeof FRAMEWORKS],
packageJson
)
expect(version).toBe(expected)
}
)
test("should handle version in devDependencies", async () => {
const packageJson = {
devDependencies: {
next: "16.0.0",
},
}
const version = await getFrameworkVersion(
FRAMEWORKS["next-pages"],
packageJson
)
expect(version).toBe("16.0.0")
})
test("should return null when next is not in dependencies", async () => {
const packageJson = {
dependencies: {
react: "^18.0.0",
},
}
const version = await getFrameworkVersion(
FRAMEWORKS["next-app"],
packageJson
)
expect(version).toBe(null)
})
test("should return null when packageJson is null", async () => {
const version = await getFrameworkVersion(FRAMEWORKS["next-app"], null)
expect(version).toBe(null)
})
})
describe("Other frameworks", () => {
test.each([
{
name: "Vite",
framework: "vite",
package: "vite",
version: "^5.0.0",
},
{
name: "Remix",
framework: "remix",
package: "@remix-run/react",
version: "^2.0.0",
},
{
name: "Astro",
framework: "astro",
package: "astro",
version: "^4.0.0",
},
])(
`should return null for $name`,
async ({ framework, package: pkg, version: ver }) => {
const packageJson = {
dependencies: {
[pkg]: ver,
},
}
const version = await getFrameworkVersion(
FRAMEWORKS[framework as keyof typeof FRAMEWORKS],
packageJson
)
expect(version).toBe(null)
}
)
})
})

4
pnpm-lock.yaml generated
View File

@@ -331,7 +331,7 @@ importers:
specifier: ^6.0.1
version: 6.0.1
shadcn:
specifier: 3.4.2
specifier: 3.5.0
version: link:../../packages/shadcn
shiki:
specifier: ^1.10.1
@@ -611,7 +611,7 @@ importers:
specifier: 2.12.7
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
shadcn:
specifier: 3.4.2
specifier: 3.5.0
version: link:../../packages/shadcn
sharp:
specifier: ^0.32.6