Compare commits

...

11 Commits

Author SHA1 Message Date
github-actions[bot]
cf1851ca09 chore(release): version packages (#7625)
* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:45:23 +04:00
Manuel Schiller
c86c27a2ff fix TanStack Start detection (#7601)
* fix tanstack start detection

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:24:41 +04:00
Gaëtan H
8847126c65 chore(vscode): set custom Tailwind config path for monorepo UI (#7618)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:18:27 +04:00
shadcn
65350857a4 ci: fix stale bot (#7624) 2025-06-16 15:02:53 +04:00
shadcn
40c7473c7e fix(www): update open-in-v0-cta.tsx 2025-06-14 06:20:16 +04:00
Taesu
4698ee960f chore: update react-day-picker version to match updated calendar component (#7585)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-12 15:44:40 +04:00
github-actions[bot]
2ae0e5a07b chore(release): version packages (#7595)
* chore(release): version packages

* deps: install

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-11 20:06:18 +04:00
shadcn
431af4f7ff fix(shadcn): semicolon in code style (#7594)
* fix(shadcn): handle semicolon in code style

* chore: changeset

* fix: format
2025-06-11 19:54:04 +04:00
github-actions[bot]
c1357982e8 chore(release): version packages (#7591)
* chore(release): version packages

* deps: update

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-11 13:57:06 +04:00
shadcn
92cfb9a30e fix(shadcn): flaky create-project tests (#7590)
* fix(shadcn): flaky create-project tests

* fix

* fix
2025-06-11 13:50:35 +04:00
shadcn
c5d90c718a feat: add migrate radix command (#7586)
* feat(shadcn): add migrate-radix command

* feat(shadcn): fix and test edge cases

* test(shadcn): add tests for all primitives

* fix(shadcn): edge cases and add yes option

* fix

* chore(shadcn): add changeset

* style: fix code styles

* docs: update changelog

* fix: format

* feat: update changelog

* fix: format
2025-06-11 13:20:47 +04:00
20 changed files with 1703 additions and 33 deletions

View File

@@ -18,15 +18,15 @@ jobs:
repo-token: ${{ secrets.STALE_TOKEN }}
ascending: true
days-before-issue-close: 7
days-before-issue-stale: 365 # ~2 years
days-before-issue-stale: 365
days-before-pr-stale: -1
days-before-pr-close: -1
remove-issue-stale-when-updated: true
stale-issue-label: "stale?"
exempt-issue-labels: "roadmap,next,bug"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
exempt-issue-labels: "roadmap,next"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300
- uses: actions/stale@v9
id: pr-state
name: "Mark stale PRs, close stale PRs"
@@ -36,10 +36,10 @@ jobs:
days-before-issue-close: -1
days-before-issue-stale: -1
days-before-pr-close: 7
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
days-before-pr-stale: 365
remove-pr-stale-when-updated: true
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
exempt-pr-labels: "roadmap,next,bug"
stale-pr-label: "stale?"
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300

View File

@@ -13,7 +13,7 @@ export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
Deploy your shadcn/ui app on Vercel
</div>
<div className="text-muted-foreground">
Trusted by OpenAI, Sonos, Chick-fil-A, and more.
Trusted by OpenAI, Sonos, Adobe, and more.
</div>
<div className="text-muted-foreground">
Vercel provides tools and infrastructure to deploy apps and features at

View File

@@ -4,6 +4,25 @@ description: Latest updates and announcements.
toc: false
---
## June 2025 - `radix-ui`
We've added a new command to migrate to the new `radix-ui` package. This command will replace all `@radix-ui/react-*` imports with `radix-ui`.
```bash
npx shadcn@latest migrate radix
```
It will automatically update all imports in your `ui` components and install `radix-ui` as a dependency.
```diff showLineNumbers title="components/ui/alert-dialog.tsx"
- import * as AlertDialogPrimitive from "@radix-ui/react-dialog"
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
```
Make sure to test your components and project after running the command.
**Note:** To update imports for newly added components, run the migration command again.
## June 2025 - Calendar Component
We've upgraded the `Calendar` component to the latest version of [React DayPicker](https://daypicker.dev).
@@ -42,9 +61,9 @@ Learn more in the thread here: https://x.com/shadcn/status/1917597228513853603
We tagged shadcn 2.5.0 earlier this week. It comes with a pretty cool feature: **resolve anywhere**.
Registries can now place files anywhere in an app and well properly resolve imports. No need to stick to a fixed file structure. It can even add files outside the registry itself.
Registries can now place files anywhere in an app and we'll properly resolve imports. No need to stick to a fixed file structure. It can even add files outside the registry itself.
On install, we track all files and perform a multi-pass resolution to correctly handle imports and aliases. Its fast.
On install, we track all files and perform a multi-pass resolution to correctly handle imports and aliases. It's fast.
## March 2025 - Cross-framework Route Support
@@ -61,7 +80,7 @@ What's New:
- The CLI can now initialize projects with Tailwind v4.
- Full support for the new @theme directive and @theme inline option.
- All components are updated for Tailwind v4 and React 19.
- Weve removed the forwardRefs and adjusted the types.
- We've removed the forwardRefs and adjusted the types.
- Every primitive now has a data-slot attribute for styling.
- We've fixed and cleaned up the style of the components.
- We're deprecating the toast component in favor of sonner.
@@ -139,7 +158,7 @@ The new CLI is now available. It's a complete rewrite with a lot of new features
This is a major step towards distributing code that you and your LLMs can access and use.
1. First up, the cli now has support for all major React framework out of the box. Next.js, Remix, Vite and Laravel. And when you init into a new app, we update your existing Tailwind files instead of overriding.
2. A component now ship its own dependencies. Take the accordion for example, it can define its Tailwind keyframes. When you add it to your project, well update your tailwind.config.ts file accordingly.
2. A component now ship its own dependencies. Take the accordion for example, it can define its Tailwind keyframes. When you add it to your project, we'll update your tailwind.config.ts file accordingly.
3. You can also install remote components using url. `npx shadcn add https://acme.com/registry/navbar.json`.
4. We have also improve the init command. It does framework detection and can even init a brand new Next.js app in one command. `npx shadcn init`.
5. We have created a new schema that you can use to ship your own component registry. And since it has support for urls, you can even use it to distribute private components.

View File

@@ -41,7 +41,7 @@ npx shadcn@latest add calendar
<Step>Install the following dependencies:</Step>
```bash
npm install react-day-picker@8.10.1 date-fns
npm install react-day-picker date-fns
```
<Step>Add the `Button` component to your project.</Step>

View File

@@ -52,7 +52,7 @@ export const mdxComponents = {
.replace(/\?/g, "")
.toLowerCase()}
className={cn(
"font-heading mt-12 scroll-m-28 text-2xl font-medium tracking-tight first:mt-0 lg:mt-20 [&+p]:!mt-4",
"font-heading mt-12 scroll-m-28 text-2xl font-medium tracking-tight first:mt-0 lg:mt-20 [&+p]:!mt-4 *:[code]:text-2xl",
className
)}
{...props}
@@ -62,7 +62,7 @@ export const mdxComponents = {
h3: ({ className, ...props }: React.ComponentProps<"h3">) => (
<h3
className={cn(
"font-heading mt-8 scroll-m-28 text-xl font-semibold tracking-tight",
"font-heading mt-8 scroll-m-28 text-xl font-semibold tracking-tight *:[code]:text-xl",
className
)}
{...props}

View File

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

View File

@@ -39,7 +39,7 @@ npx shadcn@latest add calendar
<Step>Install the following dependencies:</Step>
```bash
npm install react-day-picker@8.10.1 date-fns
npm install react-day-picker date-fns
```
<Step>Add the `Button` component to your project.</Step>

View File

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

View File

@@ -79,7 +79,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,7 +1,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,7 +1,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,5 +1,25 @@
# @shadcn/ui
## 2.6.4
### Patch Changes
- [#7601](https://github.com/shadcn-ui/ui/pull/7601) [`c86c27a2ffb8d186770afa42bfb62ab46e3db975`](https://github.com/shadcn-ui/ui/commit/c86c27a2ffb8d186770afa42bfb62ab46e3db975) Thanks [@schiller-manuel](https://github.com/schiller-manuel)! - fix tanstack start detection
## 2.6.3
### Patch Changes
- [#7594](https://github.com/shadcn-ui/ui/pull/7594) [`431af4f7ff294af032c0687b8b655ed6db2e690f`](https://github.com/shadcn-ui/ui/commit/431af4f7ff294af032c0687b8b655ed6db2e690f) Thanks [@shadcn](https://github.com/shadcn)! - fix: semicolon in code style
## 2.6.2
### Patch Changes
- [#7586](https://github.com/shadcn-ui/ui/pull/7586) [`c5d90c718a186dd6fd90e022c56089eb569a1c10`](https://github.com/shadcn-ui/ui/commit/c5d90c718a186dd6fd90e022c56089eb569a1c10) Thanks [@shadcn](https://github.com/shadcn)! - add migrate-radix
- [#7590](https://github.com/shadcn-ui/ui/pull/7590) [`92cfb9a30e976697ab8770f00393bd5325f9a16a`](https://github.com/shadcn-ui/ui/commit/92cfb9a30e976697ab8770f00393bd5325f9a16a) Thanks [@shadcn](https://github.com/shadcn)! - fix flacky tests
## 2.6.1
### Patch Changes

View File

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

View File

@@ -1,5 +1,6 @@
import path from "path"
import { migrateIcons } from "@/src/migrations/migrate-icons"
import { migrateRadix } from "@/src/migrations/migrate-radix"
import { preFlightMigrate } from "@/src/preflights/preflight-migrate"
import * as ERRORS from "@/src/utils/errors"
import { handleError } from "@/src/utils/handle-error"
@@ -12,11 +13,16 @@ export const migrations = [
name: "icons",
description: "migrate your ui components to a different icon library.",
},
{
name: "radix",
description: "migrate to radix-ui.",
},
] as const
export const migrateOptionsSchema = z.object({
cwd: z.string(),
list: z.boolean(),
yes: z.boolean(),
migration: z
.string()
.refine(
@@ -40,12 +46,14 @@ export const migrate = new Command()
process.cwd()
)
.option("-l, --list", "list all migrations.", false)
.option("-y, --yes", "skip confirmation prompt.", false)
.action(async (migration, opts) => {
try {
const options = migrateOptionsSchema.parse({
cwd: path.resolve(opts.cwd),
migration,
list: opts.list,
yes: opts.yes,
})
if (options.list || !options.migration) {
@@ -82,6 +90,10 @@ export const migrate = new Command()
if (options.migration === "icons") {
await migrateIcons(config)
}
if (options.migration === "radix") {
await migrateRadix(config, { yes: options.yes })
}
} catch (error) {
logger.break()
handleError(error)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
import { promises as fs } from "fs"
import path from "path"
import { Config } from "@/src/utils/get-config"
import { getPackageInfo } from "@/src/utils/get-package-info"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import { spinner } from "@/src/utils/spinner"
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
import fg from "fast-glob"
import prompts from "prompts"
function toPascalCase(str: string): string {
return str
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("")
}
function processNamedImports(
namedImports: string,
isTypeOnly: boolean,
imports: Array<{ name: string; alias?: string; isType?: boolean }>,
packageName: string
) {
// Clean up multi-line imports.
// Remove comments and whitespace.
const cleanedImports = namedImports
.replace(/\/\/.*$/gm, "")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\s+/g, " ")
.trim()
const namedImportList = cleanedImports
.split(",")
.map((importItem) => importItem.trim())
.filter(Boolean)
for (const importItem of namedImportList) {
const inlineTypeMatch = importItem.match(/^type\s+(\w+)(?:\s+as\s+(\w+))?$/)
const aliasMatch = importItem.match(/^(\w+)\s+as\s+(\w+)$/)
if (inlineTypeMatch) {
// Inline type: "type DialogProps" or "type DialogProps as Props"
const importName = inlineTypeMatch[1]
const importAlias = inlineTypeMatch[2]
if (packageName === "slot" && importName === "Slot" && !importAlias) {
imports.push({
name: "Slot",
alias: "SlotPrimitive",
isType: true,
})
} else {
imports.push({
name: importName,
alias: importAlias,
isType: true,
})
}
} else if (aliasMatch) {
// Regular import with alias: "Root as DialogRoot"
const importName = aliasMatch[1]
const importAlias = aliasMatch[2]
if (
packageName === "slot" &&
importName === "Slot" &&
importAlias === "Slot"
) {
imports.push({
name: "Slot",
alias: "SlotPrimitive",
isType: isTypeOnly,
})
} else {
imports.push({
name: importName,
alias: importAlias,
isType: isTypeOnly,
})
}
} else {
// Simple import: "Root"
// Special handling for Slot: always alias it as SlotPrimitive
if (packageName === "slot" && importItem === "Slot") {
imports.push({
name: "Slot",
alias: "SlotPrimitive",
isType: isTypeOnly,
})
} else {
imports.push({
name: importItem,
isType: isTypeOnly,
})
}
}
}
}
export async function migrateRadix(
config: Config,
options: { yes?: boolean } = {}
) {
if (!config.resolvedPaths.ui) {
throw new Error(
"We could not find a valid `ui` path in your `components.json` file. Please ensure you have a valid `ui` path in your `components.json` file."
)
}
const uiPath = config.resolvedPaths.ui
const files = await fg("**/*.{js,ts,jsx,tsx}", {
cwd: uiPath,
})
if (!options.yes) {
const { confirm } = await prompts({
type: "confirm",
name: "confirm",
initial: true,
message: `We will migrate ${highlighter.info(
files.length
)} files in ${highlighter.info(
`./${path.relative(config.resolvedPaths.cwd, uiPath)}`
)} to ${highlighter.info("radix-ui")}. Continue?`,
})
if (!confirm) {
logger.info("Migration cancelled.")
process.exit(0)
}
}
const migrationSpinner = spinner(`Migrating imports...`)?.start()
const foundPackages = new Set<string>()
await Promise.all(
files.map(async (file) => {
migrationSpinner.text = `Migrating ${file}...`
const filePath = path.join(uiPath, file)
const fileContent = await fs.readFile(filePath, "utf-8")
const { content, replacedPackages } = await migrateRadixFile(fileContent)
// Track which packages we found
replacedPackages.forEach((pkg) => foundPackages.add(pkg))
await fs.writeFile(filePath, content)
})
)
migrationSpinner.succeed("Migrating imports.")
// Update package.json dependencies
const packageSpinner = spinner(`Updating package.json...`)?.start()
try {
const packageJson = getPackageInfo(config.resolvedPaths.cwd, false)
if (!packageJson) {
packageSpinner.fail("Could not read package.json")
logger.warn(
"Could not update package.json. You may need to manually replace @radix-ui/react-* packages with radix-ui"
)
return
}
const foundPackagesArray = Array.from(foundPackages)
// Remove packages from both dependencies and devDependencies if found in source files
const dependencyTypes = ["dependencies", "devDependencies"] as const
for (const depType of dependencyTypes) {
if (packageJson[depType]) {
for (const pkg of foundPackagesArray) {
if (packageJson[depType]![pkg]) {
delete packageJson[depType]![pkg]
}
}
}
}
// Add radix-ui if we found any Radix packages.
if (foundPackagesArray.length > 0) {
if (!packageJson.dependencies) {
packageJson.dependencies = {}
}
packageJson.dependencies["radix-ui"] = "latest"
const packageJsonPath = path.join(
config.resolvedPaths.cwd,
"package.json"
)
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + "\n"
)
packageSpinner.succeed(`Updated package.json.`)
// Install radix-ui dependency.
await updateDependencies(["radix-ui"], [], config, { silent: false })
} else {
packageSpinner.succeed("No packages found in source files.")
}
} catch (error) {
packageSpinner.fail("Failed to update package.json")
logger.warn(
"You may need to manually replace @radix-ui/react-* packages with radix-ui"
)
}
}
export async function migrateRadixFile(
content: string
): Promise<{ content: string; replacedPackages: string[] }> {
// Enhanced regex to handle type-only imports, but exclude react-icons
// Also capture optional semicolon at the end
const radixImportPattern =
/import\s+(?:(type)\s+)?(?:\*\s+as\s+(\w+)|{([^}]+)})\s+from\s+(["'])@radix-ui\/react-([^"']+)\4(;?)/g
const imports: Array<{ name: string; alias?: string; isType?: boolean }> = []
const linesToRemove: string[] = []
const replacedPackages: string[] = []
let quoteStyle = '"' // Default to double quotes
let hasSemicolon = false // Track if any import had a semicolon
let result = content
let match
// Find all Radix imports
while ((match = radixImportPattern.exec(content)) !== null) {
const [
fullMatch,
typeKeyword,
namespaceAlias,
namedImports,
quote,
packageName,
semicolon,
] = match
// Skip react-icons package and any sub-paths (like react-icons/dist/types)
if (packageName === "icons" || packageName.startsWith("icons/")) {
continue
}
linesToRemove.push(fullMatch)
// Use the quote style and semicolon style from the first import
if (linesToRemove.length === 1) {
quoteStyle = quote
hasSemicolon = semicolon === ";"
}
// Track which package we're replacing
replacedPackages.push(`@radix-ui/react-${packageName}`)
const isTypeOnly = Boolean(typeKeyword)
if (namespaceAlias) {
// Handle namespace imports: import * as DialogPrimitive from "@radix-ui/react-dialog"
const componentName = toPascalCase(packageName)
imports.push({
name: componentName,
alias: namespaceAlias,
isType: isTypeOnly,
})
} else if (namedImports) {
// Handle named imports: import { Root, Trigger } from "@radix-ui/react-dialog"
// or import type { DialogProps } from "@radix-ui/react-dialog"
// or import { type DialogProps, Root } from "@radix-ui/react-dialog"
processNamedImports(namedImports, isTypeOnly, imports, packageName)
}
}
if (imports.length === 0) {
return {
content,
replacedPackages: [],
}
}
// Remove duplicates.
// Considering name, alias, and type status.
const uniqueImports = imports.filter(
(importName, index, self) =>
index ===
self.findIndex(
(i) =>
i.name === importName.name &&
i.alias === importName.alias &&
i.isType === importName.isType
)
)
// Create the unified import with preserved quote style and type annotations
const importList = uniqueImports
.map((imp) => {
const typePrefix = imp.isType ? "type " : ""
return imp.alias
? `${typePrefix}${imp.name} as ${imp.alias}`
: `${typePrefix}${imp.name}`
})
.join(", ")
const unifiedImport = `import { ${importList} } from ${quoteStyle}radix-ui${quoteStyle}${
hasSemicolon ? ";" : ""
}`
// Replace first import with unified import, remove the rest
result = linesToRemove.reduce((acc, line, index) => {
return acc.replace(line, index === 0 ? unifiedImport : "")
}, result)
// Clean up extra blank lines
result = result.replace(/\n\s*\n\s*\n/g, "\n\n")
// Handle special case for Slot usage transformation
// Now that we import { Slot as SlotPrimitive }, we need to:
// 1. Transform: const Comp = asChild ? Slot : [ANYTHING] -> const Comp = asChild ? SlotPrimitive.Slot : [ANYTHING]
// 2. Transform: React.ComponentProps<typeof Slot> -> React.ComponentProps<typeof SlotPrimitive.Slot>
const hasSlotImport = uniqueImports.some(
(imp) => imp.name === "Slot" && imp.alias === "SlotPrimitive"
)
if (hasSlotImport) {
// Find all lines that are NOT import lines to avoid transforming the import statement itself
const lines = result.split("\n")
const transformedLines = lines.map((line) => {
// Skip import lines
if (line.trim().startsWith("import ")) {
return line
}
let transformedLine = line
// Handle all Slot references in one comprehensive pass
// Use placeholders to avoid double replacements
// First, mark specific patterns with placeholders
transformedLine = transformedLine.replace(
/\b(asChild\s*\?\s*)Slot(\s*:)/g,
"$1__SLOT_PLACEHOLDER__$2"
)
transformedLine = transformedLine.replace(
/\bReact\.ComponentProps<typeof\s+Slot>/g,
"React.ComponentProps<typeof __SLOT_PLACEHOLDER__>"
)
transformedLine = transformedLine.replace(
/\bComponentProps<typeof\s+Slot>/g,
"ComponentProps<typeof __SLOT_PLACEHOLDER__>"
)
transformedLine = transformedLine.replace(
/(<\/?)Slot(\s*\/?>)/g,
"$1__SLOT_PLACEHOLDER__$2"
)
// Handle any other standalone Slot usage
transformedLine = transformedLine.replace(
/\bSlot\b/g,
(match, offset, string) => {
// Don't transform if it's inside quotes
const beforeMatch = string.substring(0, offset)
const openQuotes = (beforeMatch.match(/"/g) || []).length
const openSingleQuotes = (beforeMatch.match(/'/g) || []).length
// If we're inside quotes, don't transform
if (openQuotes % 2 !== 0 || openSingleQuotes % 2 !== 0) {
return match
}
return "__SLOT_PLACEHOLDER__"
}
)
// Finally, replace all placeholders with SlotPrimitive.Slot
transformedLine = transformedLine.replace(
/__SLOT_PLACEHOLDER__/g,
"SlotPrimitive.Slot"
)
return transformedLine
})
result = transformedLines.join("\n")
}
// Remove duplicate packages
const uniqueReplacedPackages = Array.from(new Set(replacedPackages))
return {
content: result,
replacedPackages: uniqueReplacedPackages,
}
}

View File

@@ -1,8 +1,17 @@
import { fetchRegistry } from "@/src/registry/api"
import { spinner } from "@/src/utils/spinner"
import { execa } from "execa"
import fs from "fs-extra"
import prompts from "prompts"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
type MockInstance,
} from "vitest"
import { TEMPLATES, createProject } from "./create-project"
@@ -14,16 +23,85 @@ vi.mock("@/src/registry/api")
vi.mock("@/src/utils/get-package-manager", () => ({
getPackageManager: vi.fn().mockResolvedValue("npm"),
}))
vi.mock("@/src/utils/spinner")
vi.mock("@/src/utils/logger", () => ({
logger: {
break: vi.fn(),
error: vi.fn(),
info: vi.fn(),
},
}))
describe("createProject", () => {
let mockExit: MockInstance
beforeEach(() => {
vi.clearAllMocks()
// Reset all fs mocks
vi.mocked(fs.access).mockResolvedValue(undefined)
vi.mocked(fs.existsSync).mockReturnValue(false)
vi.mocked(fs.ensureDir).mockResolvedValue(undefined)
vi.mocked(fs.writeFile).mockResolvedValue(undefined)
vi.mocked(fs.move).mockResolvedValue(undefined)
vi.mocked(fs.remove).mockResolvedValue(undefined)
// Mock execa to resolve immediately without actual execution
vi.mocked(execa).mockResolvedValue({
stdout: "",
stderr: "",
exitCode: 0,
signal: undefined,
signalDescription: undefined,
command: "",
escapedCommand: "",
failed: false,
timedOut: false,
isCanceled: false,
killed: false,
} as any)
// Mock fetch for monorepo template
global.fetch = vi.fn().mockResolvedValue({
ok: true,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
} as any)
// Reset prompts mock
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
// Reset registry mock
vi.mocked(fetchRegistry).mockResolvedValue([])
// Mock spinner function
const mockSpinner = {
start: vi.fn().mockReturnThis(),
succeed: vi.fn().mockReturnThis(),
fail: vi.fn().mockReturnThis(),
stop: vi.fn().mockReturnThis(),
text: "",
prefixText: "",
suffixText: "",
color: "cyan" as const,
indent: 0,
spinner: "dots" as const,
isSpinning: false,
interval: 100,
stream: process.stderr,
clear: vi.fn(),
render: vi.fn(),
frame: vi.fn(),
stopAndPersist: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
}
vi.mocked(spinner).mockReturnValue(mockSpinner as any)
})
afterEach(() => {
vi.resetAllMocks()
mockExit?.mockRestore()
delete (global as any).fetch
})
it("should create a Next.js project with default options", async () => {
@@ -84,10 +162,13 @@ describe("createProject", () => {
})
it("should throw error if project path already exists", async () => {
vi.mocked(fs.existsSync).mockReturnValue(true)
// Mock fs.existsSync to return true only for the specific package.json path
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
return path.toString().includes("existing-app/package.json")
})
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "existing-app" })
const mockExit = vi
mockExit = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never)
@@ -103,7 +184,7 @@ describe("createProject", () => {
vi.mocked(fs.access).mockRejectedValue(new Error("Permission denied"))
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
const mockExit = vi
mockExit = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never)

View File

@@ -121,11 +121,10 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
// TanStack Start.
if (
configFiles.find((file) => file.startsWith("app.config."))?.length &&
[
...Object.keys(packageJson?.dependencies ?? {}),
...Object.keys(packageJson?.devDependencies ?? {}),
].find((dep) => dep.startsWith("@tanstack/start"))
].find((dep) => dep.startsWith("@tanstack/react-start"))
) {
type.framework = FRAMEWORKS["tanstack-start"]
return type

4
pnpm-lock.yaml generated
View File

@@ -322,7 +322,7 @@ importers:
specifier: ^6.0.1
version: 6.0.1
shadcn:
specifier: 2.6.1
specifier: 2.6.4
version: link:../../packages/shadcn
shiki:
specifier: ^1.10.1
@@ -602,7 +602,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: 2.6.1
specifier: 2.6.4
version: link:../../packages/shadcn
sharp:
specifier: ^0.32.6

View File

@@ -0,0 +1,3 @@
{
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
}