mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 04:05:48 +00:00
Compare commits
25 Commits
shadcn@2.6
...
shadcn@2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7443edcfb0 | ||
|
|
9d9a33be52 | ||
|
|
d544a7f7a5 | ||
|
|
48fe0d709f | ||
|
|
ed244ea0b5 | ||
|
|
b8fede1742 | ||
|
|
84d6c83bad | ||
|
|
5b8ee41511 | ||
|
|
7c3d34cdc9 | ||
|
|
56c4c83511 | ||
|
|
2821cb0e39 | ||
|
|
3c87402de2 | ||
|
|
20a88e1f15 | ||
|
|
cb19ab8464 | ||
|
|
cf1851ca09 | ||
|
|
c86c27a2ff | ||
|
|
8847126c65 | ||
|
|
65350857a4 | ||
|
|
40c7473c7e | ||
|
|
4698ee960f | ||
|
|
2ae0e5a07b | ||
|
|
431af4f7ff | ||
|
|
c1357982e8 | ||
|
|
92cfb9a30e | ||
|
|
c5d90c718a |
20
.github/workflows/issue-stale.yml
vendored
20
.github/workflows/issue-stale.yml
vendored
@@ -18,15 +18,15 @@ jobs:
|
|||||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
days-before-issue-close: 7
|
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-stale: -1
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
remove-issue-stale-when-updated: true
|
remove-issue-stale-when-updated: true
|
||||||
stale-issue-label: "stale?"
|
stale-issue-label: "stale?"
|
||||||
exempt-issue-labels: "roadmap,next,bug"
|
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 there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
|
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 there’s 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 you’re 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!"
|
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re 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 # 1 operation per 100 issues, the rest is to label/comment/close
|
operations-per-run: 300
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
id: pr-state
|
id: pr-state
|
||||||
name: "Mark stale PRs, close stale PRs"
|
name: "Mark stale PRs, close stale PRs"
|
||||||
@@ -36,10 +36,10 @@ jobs:
|
|||||||
days-before-issue-close: -1
|
days-before-issue-close: -1
|
||||||
days-before-issue-stale: -1
|
days-before-issue-stale: -1
|
||||||
days-before-pr-close: 7
|
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
|
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-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 there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
|
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 there’s 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!"
|
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 # 1 operation per 100 issues, the rest is to label/comment/close
|
operations-per-run: 300
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ To run the CLI locally, you can follow the workflow:
|
|||||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm www:dev
|
pnpm v4:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the development script for the CLI:
|
2. Run the development script for the CLI:
|
||||||
|
|||||||
2
apps/v4/.env.example
Normal file
2
apps/v4/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
NEXT_PUBLIC_V0_URL=https://v0.dev
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:4000
|
||||||
1
apps/v4/.gitignore
vendored
1
apps/v4/.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function Callout({
|
|||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-4",
|
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function CodeCollapsibleWrapper({
|
|||||||
<Collapsible
|
<Collapsible
|
||||||
open={isOpened}
|
open={isOpened}
|
||||||
onOpenChange={setIsOpened}
|
onOpenChange={setIsOpened}
|
||||||
className={cn("group/collapsible relative md:-mx-4", className)}
|
className={cn("group/collapsible relative md:-mx-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function ComponentPreviewTabs({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<div
|
<div
|
||||||
data-tab={tab}
|
data-tab={tab}
|
||||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-4"
|
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-slot="preview"
|
data-slot="preview"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function ComponentPreview({
|
|||||||
|
|
||||||
if (type === "block") {
|
if (type === "block") {
|
||||||
return (
|
return (
|
||||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-4">
|
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
||||||
<Image
|
<Image
|
||||||
src={`/r/styles/new-york-v4/${name}-light.png`}
|
src={`/r/styles/new-york-v4/${name}-light.png`}
|
||||||
alt={name}
|
alt={name}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
|
|||||||
Deploy your shadcn/ui app on Vercel
|
Deploy your shadcn/ui app on Vercel
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
Trusted by OpenAI, Sonos, Chick-fil-A, and more.
|
Trusted by OpenAI, Sonos, Adobe, and more.
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
Vercel provides tools and infrastructure to deploy apps and features at
|
Vercel provides tools and infrastructure to deploy apps and features at
|
||||||
|
|||||||
@@ -4,6 +4,25 @@ description: Latest updates and announcements.
|
|||||||
toc: false
|
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
|
## June 2025 - Calendar Component
|
||||||
|
|
||||||
We've upgraded the `Calendar` component to the latest version of [React DayPicker](https://daypicker.dev).
|
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**.
|
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 we’ll 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. It’s 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
|
## March 2025 - Cross-framework Route Support
|
||||||
|
|
||||||
@@ -61,7 +80,7 @@ What's New:
|
|||||||
- The CLI can now initialize projects with Tailwind v4.
|
- The CLI can now initialize projects with Tailwind v4.
|
||||||
- Full support for the new @theme directive and @theme inline option.
|
- Full support for the new @theme directive and @theme inline option.
|
||||||
- All components are updated for Tailwind v4 and React 19.
|
- All components are updated for Tailwind v4 and React 19.
|
||||||
- We’ve 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.
|
- Every primitive now has a data-slot attribute for styling.
|
||||||
- We've fixed and cleaned up the style of the components.
|
- We've fixed and cleaned up the style of the components.
|
||||||
- We're deprecating the toast component in favor of sonner.
|
- 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.
|
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.
|
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, we’ll 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`.
|
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`.
|
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.
|
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.
|
||||||
|
|||||||
@@ -21,15 +21,14 @@ Usage: shadcn init [options] [components...]
|
|||||||
initialize your project and install dependencies
|
initialize your project and install dependencies
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
components the components to add or a url to the component.
|
components name, url or local path to component
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-t, --template <template> the template to use. (next, next-monorepo)
|
-t, --template <template> the template to use. (next, next-monorepo)
|
||||||
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
|
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
|
||||||
-y, --yes skip confirmation prompt. (default: true)
|
-y, --yes skip confirmation prompt. (default: true)
|
||||||
-f, --force force overwrite of existing configuration. (default: false)
|
-f, --force force overwrite of existing configuration. (default: false)
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
|
||||||
-s, --silent mute output. (default: false)
|
-s, --silent mute output. (default: false)
|
||||||
--src-dir use the src directory when creating a new project. (default: false)
|
--src-dir use the src directory when creating a new project. (default: false)
|
||||||
--no-src-dir do not use the src directory when creating a new project.
|
--no-src-dir do not use the src directory when creating a new project.
|
||||||
@@ -54,12 +53,12 @@ Usage: shadcn add [options] [components...]
|
|||||||
add a component to your project
|
add a component to your project
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
components the components to add or a url to the component.
|
components name, url or local path to component
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-y, --yes skip confirmation prompt. (default: false)
|
-y, --yes skip confirmation prompt. (default: false)
|
||||||
-o, --overwrite overwrite existing files. (default: false)
|
-o, --overwrite overwrite existing files. (default: false)
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
-a, --all add all available components (default: false)
|
-a, --all add all available components (default: false)
|
||||||
-p, --path <path> the path to add the component to.
|
-p, --path <path> the path to add the component to.
|
||||||
-s, --silent mute output. (default: false)
|
-s, --silent mute output. (default: false)
|
||||||
@@ -92,8 +91,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
-o, --output <path> destination directory for json files (default: "./public/r")
|
-o, --output <path> destination directory for json files (default: "./public/r")
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ description: Every component recreated in Figma. With customizable props, typogr
|
|||||||
## Free
|
## Free
|
||||||
|
|
||||||
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
||||||
|
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ npx shadcn@latest add calendar
|
|||||||
<Step>Install the following dependencies:</Step>
|
<Step>Install the following dependencies:</Step>
|
||||||
|
|
||||||
```bash
|
```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>
|
<Step>Add the `Button` component to your project.</Step>
|
||||||
|
|||||||
@@ -328,6 +328,20 @@ Add custom theme variables to the `theme` object.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Add custom plugins
|
||||||
|
|
||||||
|
```json title="example-plugin.json" showLineNumbers
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "custom-plugin",
|
||||||
|
"type": "registry:component",
|
||||||
|
"css": {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin foo": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Add custom animations
|
## Add custom animations
|
||||||
|
|
||||||
Note: you need to define both `@keyframes` in css and `theme` in cssVars to use animations.
|
Note: you need to define both `@keyframes` in css and `theme` in cssVars to use animations.
|
||||||
|
|||||||
@@ -260,11 +260,13 @@ Use to define CSS variables for your registry item.
|
|||||||
|
|
||||||
### css
|
### css
|
||||||
|
|
||||||
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, etc.
|
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, `@plugin`, etc.
|
||||||
|
|
||||||
```json title="registry-item.json" showLineNumbers
|
```json title="registry-item.json" showLineNumbers
|
||||||
{
|
{
|
||||||
"css": {
|
"css": {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin foo": {},
|
||||||
"@layer base": {
|
"@layer base": {
|
||||||
"body": {
|
"body": {
|
||||||
"font-size": "var(--text-base)",
|
"font-size": "var(--text-base)",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const mdxComponents = {
|
|||||||
.replace(/\?/g, "")
|
.replace(/\?/g, "")
|
||||||
.toLowerCase()}
|
.toLowerCase()}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -62,7 +62,7 @@ export const mdxComponents = {
|
|||||||
h3: ({ className, ...props }: React.ComponentProps<"h3">) => (
|
h3: ({ className, ...props }: React.ComponentProps<"h3">) => (
|
||||||
<h3
|
<h3
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
"recharts": "2.15.1",
|
"recharts": "2.15.1",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"shadcn": "2.6.1",
|
"shadcn": "2.8.0",
|
||||||
"shiki": "^1.10.1",
|
"shiki": "^1.10.1",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
||||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
||||||
"type": "registry:component"
|
"type": "registry:component"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export default function Calendar24() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label htmlFor="date" className="px-1">
|
<Label htmlFor="date-picker" className="px-1">
|
||||||
Date
|
Date
|
||||||
</Label>
|
</Label>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
id="date"
|
id="date-picker"
|
||||||
className="w-32 justify-between font-normal"
|
className="w-32 justify-between font-normal"
|
||||||
>
|
>
|
||||||
{date ? date.toLocaleDateString() : "Select date"}
|
{date ? date.toLocaleDateString() : "Select date"}
|
||||||
@@ -48,12 +48,12 @@ export default function Calendar24() {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label htmlFor="time" className="px-1">
|
<Label htmlFor="time-picker" className="px-1">
|
||||||
Time
|
Time
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type="time"
|
type="time"
|
||||||
id="time"
|
id="time-picker"
|
||||||
step="1"
|
step="1"
|
||||||
defaultValue="10:30:00"
|
defaultValue="10:30:00"
|
||||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
outline: none;
|
outline: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
@apply md:-mx-4;
|
@apply md:-mx-1;
|
||||||
|
|
||||||
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
||||||
top: calc(var(--spacing) * 1.5) !important;
|
top: calc(var(--spacing) * 1.5) !important;
|
||||||
|
|||||||
6
apps/www/README.md
Normal file
6
apps/www/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# ui.shadcn.com
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The site at ui.shadcn.com now points to `apps/v4`. All changes should be made in the `apps/v4` directory.
|
||||||
|
|
||||||
|
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||||
@@ -39,7 +39,7 @@ npx shadcn@latest add calendar
|
|||||||
<Step>Install the following dependencies:</Step>
|
<Step>Install the following dependencies:</Step>
|
||||||
|
|
||||||
```bash
|
```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>
|
<Step>Add the `Button` component to your project.</Step>
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ description: Every component recreated in Figma. With customizable props, typogr
|
|||||||
## Free
|
## Free
|
||||||
|
|
||||||
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
||||||
|
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"react-resizable-panels": "^2.0.22",
|
"react-resizable-panels": "^2.0.22",
|
||||||
"react-wrap-balancer": "^0.4.1",
|
"react-wrap-balancer": "^0.4.1",
|
||||||
"recharts": "2.12.7",
|
"recharts": "2.12.7",
|
||||||
"shadcn": "2.6.1",
|
"shadcn": "2.8.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"sonner": "^1.2.3",
|
"sonner": "^1.2.3",
|
||||||
"swr": "2.2.6-beta.3",
|
"swr": "2.2.6-beta.3",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
||||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
||||||
"type": "registry:component"
|
"type": "registry:component"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -1,5 +1,47 @@
|
|||||||
# @shadcn/ui
|
# @shadcn/ui
|
||||||
|
|
||||||
|
## 2.8.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7720](https://github.com/shadcn-ui/ui/pull/7720) [`d544a7f7a519cd5b171d9ee7cb2fd1a226659ece`](https://github.com/shadcn-ui/ui/commit/d544a7f7a519cd5b171d9ee7cb2fd1a226659ece) Thanks [@shadcn](https://github.com/shadcn)! - refactor registry dependencies resolution
|
||||||
|
|
||||||
|
- [#7717](https://github.com/shadcn-ui/ui/pull/7717) [`48fe0d709fd2b244314f95f56e7afb38b117ed8a`](https://github.com/shadcn-ui/ui/commit/48fe0d709fd2b244314f95f56e7afb38b117ed8a) Thanks [@shadcn](https://github.com/shadcn)! - add support for local registry item
|
||||||
|
|
||||||
|
- [#6330](https://github.com/shadcn-ui/ui/pull/6330) [`ed244ea0b5abf7db50ac5fcf26e2993133fe94f7`](https://github.com/shadcn-ui/ui/commit/ed244ea0b5abf7db50ac5fcf26e2993133fe94f7) Thanks [@KitsuneDev](https://github.com/KitsuneDev)! - add support for vinxi based framework
|
||||||
|
|
||||||
|
## 2.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7540](https://github.com/shadcn-ui/ui/pull/7540) [`cb19ab84646fc017c15fadc81fc47b695560a04c`](https://github.com/shadcn-ui/ui/commit/cb19ab84646fc017c15fadc81fc47b695560a04c) Thanks [@mrzachnugent](https://github.com/mrzachnugent)! - add support for expo
|
||||||
|
|
||||||
|
- [#7640](https://github.com/shadcn-ui/ui/pull/7640) [`7c3d34cdc91681815f8897709917ec9fbcd69245`](https://github.com/shadcn-ui/ui/commit/7c3d34cdc91681815f8897709917ec9fbcd69245) Thanks [@shadcn](https://github.com/shadcn)! - add support for @plugin in css
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7609](https://github.com/shadcn-ui/ui/pull/7609) [`5b8ee41511fb5ff468d9218f97b8545e145d773c`](https://github.com/shadcn-ui/ui/commit/5b8ee41511fb5ff468d9218f97b8545e145d773c) Thanks [@xabierlameiro](https://github.com/xabierlameiro)! - fix typo in function name unnsetSpreadElements
|
||||||
|
|
||||||
|
## 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
|
## 2.6.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "2.6.1",
|
"version": "2.8.0",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { runInit } from "@/src/commands/init"
|
import { runInit } from "@/src/commands/init"
|
||||||
import { preFlightAdd } from "@/src/preflights/preflight-add"
|
import { preFlightAdd } from "@/src/preflights/preflight-add"
|
||||||
import { getRegistryIndex, getRegistryItem, isUrl } from "@/src/registry/api"
|
import { getRegistryIndex, getRegistryItem } from "@/src/registry/api"
|
||||||
import { registryItemTypeSchema } from "@/src/registry/schema"
|
import { registryItemTypeSchema } from "@/src/registry/schema"
|
||||||
|
import { isLocalFile, isUrl } from "@/src/registry/utils"
|
||||||
import { addComponents } from "@/src/utils/add-components"
|
import { addComponents } from "@/src/utils/add-components"
|
||||||
import { createProject } from "@/src/utils/create-project"
|
import { createProject } from "@/src/utils/create-project"
|
||||||
import * as ERRORS from "@/src/utils/errors"
|
import * as ERRORS from "@/src/utils/errors"
|
||||||
@@ -46,10 +47,7 @@ export const addOptionsSchema = z.object({
|
|||||||
export const add = new Command()
|
export const add = new Command()
|
||||||
.name("add")
|
.name("add")
|
||||||
.description("add a component to your project")
|
.description("add a component to your project")
|
||||||
.argument(
|
.argument("[components...]", "names, url or local path to component")
|
||||||
"[components...]",
|
|
||||||
"the components to add or a url to the component."
|
|
||||||
)
|
|
||||||
.option("-y, --yes", "skip confirmation prompt.", false)
|
.option("-y, --yes", "skip confirmation prompt.", false)
|
||||||
.option("-o, --overwrite", "overwrite existing files.", false)
|
.option("-o, --overwrite", "overwrite existing files.", false)
|
||||||
.option(
|
.option(
|
||||||
@@ -81,7 +79,10 @@ export const add = new Command()
|
|||||||
|
|
||||||
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
||||||
|
|
||||||
if (components.length > 0 && isUrl(components[0])) {
|
if (
|
||||||
|
components.length > 0 &&
|
||||||
|
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||||
|
) {
|
||||||
const item = await getRegistryItem(components[0], "")
|
const item = await getRegistryItem(components[0], "")
|
||||||
itemType = item?.type
|
itemType = item?.type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
getRegistryBaseColors,
|
getRegistryBaseColors,
|
||||||
getRegistryItem,
|
getRegistryItem,
|
||||||
getRegistryStyles,
|
getRegistryStyles,
|
||||||
isUrl,
|
|
||||||
} from "@/src/registry/api"
|
} from "@/src/registry/api"
|
||||||
|
import { isLocalFile, isUrl } from "@/src/registry/utils"
|
||||||
import { addComponents } from "@/src/utils/add-components"
|
import { addComponents } from "@/src/utils/add-components"
|
||||||
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
||||||
import * as ERRORS from "@/src/utils/errors"
|
import * as ERRORS from "@/src/utils/errors"
|
||||||
@@ -82,10 +82,7 @@ export const initOptionsSchema = z.object({
|
|||||||
export const init = new Command()
|
export const init = new Command()
|
||||||
.name("init")
|
.name("init")
|
||||||
.description("initialize your project and install dependencies")
|
.description("initialize your project and install dependencies")
|
||||||
.argument(
|
.argument("[components...]", "names, url or local path to component")
|
||||||
"[components...]",
|
|
||||||
"the components to add or a url to the component."
|
|
||||||
)
|
|
||||||
.option(
|
.option(
|
||||||
"-t, --template <template>",
|
"-t, --template <template>",
|
||||||
"the template to use. (next, next-monorepo)"
|
"the template to use. (next, next-monorepo)"
|
||||||
@@ -128,7 +125,10 @@ export const init = new Command()
|
|||||||
// We need to check if we're initializing with a new style.
|
// We need to check if we're initializing with a new style.
|
||||||
// We fetch the payload of the first item.
|
// We fetch the payload of the first item.
|
||||||
// This is okay since the request is cached and deduped.
|
// This is okay since the request is cached and deduped.
|
||||||
if (components.length > 0 && isUrl(components[0])) {
|
if (
|
||||||
|
components.length > 0 &&
|
||||||
|
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||||
|
) {
|
||||||
const item = await getRegistryItem(components[0], "")
|
const item = await getRegistryItem(components[0], "")
|
||||||
|
|
||||||
// Skip base color if style.
|
// Skip base color if style.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { migrateIcons } from "@/src/migrations/migrate-icons"
|
import { migrateIcons } from "@/src/migrations/migrate-icons"
|
||||||
|
import { migrateRadix } from "@/src/migrations/migrate-radix"
|
||||||
import { preFlightMigrate } from "@/src/preflights/preflight-migrate"
|
import { preFlightMigrate } from "@/src/preflights/preflight-migrate"
|
||||||
import * as ERRORS from "@/src/utils/errors"
|
import * as ERRORS from "@/src/utils/errors"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
@@ -12,11 +13,16 @@ export const migrations = [
|
|||||||
name: "icons",
|
name: "icons",
|
||||||
description: "migrate your ui components to a different icon library.",
|
description: "migrate your ui components to a different icon library.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "radix",
|
||||||
|
description: "migrate to radix-ui.",
|
||||||
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const migrateOptionsSchema = z.object({
|
export const migrateOptionsSchema = z.object({
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
list: z.boolean(),
|
list: z.boolean(),
|
||||||
|
yes: z.boolean(),
|
||||||
migration: z
|
migration: z
|
||||||
.string()
|
.string()
|
||||||
.refine(
|
.refine(
|
||||||
@@ -40,12 +46,14 @@ export const migrate = new Command()
|
|||||||
process.cwd()
|
process.cwd()
|
||||||
)
|
)
|
||||||
.option("-l, --list", "list all migrations.", false)
|
.option("-l, --list", "list all migrations.", false)
|
||||||
|
.option("-y, --yes", "skip confirmation prompt.", false)
|
||||||
.action(async (migration, opts) => {
|
.action(async (migration, opts) => {
|
||||||
try {
|
try {
|
||||||
const options = migrateOptionsSchema.parse({
|
const options = migrateOptionsSchema.parse({
|
||||||
cwd: path.resolve(opts.cwd),
|
cwd: path.resolve(opts.cwd),
|
||||||
migration,
|
migration,
|
||||||
list: opts.list,
|
list: opts.list,
|
||||||
|
yes: opts.yes,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.list || !options.migration) {
|
if (options.list || !options.migration) {
|
||||||
@@ -82,6 +90,10 @@ export const migrate = new Command()
|
|||||||
if (options.migration === "icons") {
|
if (options.migration === "icons") {
|
||||||
await migrateIcons(config)
|
await migrateIcons(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.migration === "radix") {
|
||||||
|
await migrateRadix(config, { yes: options.yes })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.break()
|
logger.break()
|
||||||
handleError(error)
|
handleError(error)
|
||||||
|
|||||||
1136
packages/shadcn/src/migrations/migrate-radix.test.ts
Normal file
1136
packages/shadcn/src/migrations/migrate-radix.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
400
packages/shadcn/src/migrations/migrate-radix.ts
Normal file
400
packages/shadcn/src/migrations/migrate-radix.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,54 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { tmpdir } from "os"
|
||||||
|
import path from "path"
|
||||||
import { HttpResponse, http } from "msw"
|
import { HttpResponse, http } from "msw"
|
||||||
import { setupServer } from "msw/node"
|
import { setupServer } from "msw/node"
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest"
|
||||||
|
|
||||||
import { clearRegistryCache, fetchRegistry } from "./api"
|
import {
|
||||||
|
clearRegistryCache,
|
||||||
|
fetchRegistry,
|
||||||
|
getRegistryItem,
|
||||||
|
registryResolveItemsTree,
|
||||||
|
} from "./api"
|
||||||
|
|
||||||
|
// Mock the handleError function to prevent process.exit in tests
|
||||||
|
vi.mock("@/src/utils/handle-error", () => ({
|
||||||
|
handleError: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the logger to prevent console output in tests
|
||||||
|
vi.mock("@/src/utils/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
error: vi.fn(),
|
||||||
|
break: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const REGISTRY_URL = "https://ui.shadcn.com/r"
|
const REGISTRY_URL = "https://ui.shadcn.com/r"
|
||||||
|
|
||||||
const server = setupServer(
|
const server = setupServer(
|
||||||
|
http.get(`${REGISTRY_URL}/index.json`, () => {
|
||||||
|
return HttpResponse.json([
|
||||||
|
{
|
||||||
|
name: "button",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "card",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
http.get(`${REGISTRY_URL}/styles/new-york/button.json`, () => {
|
http.get(`${REGISTRY_URL}/styles/new-york/button.json`, () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
name: "button",
|
name: "button",
|
||||||
@@ -112,3 +154,277 @@ describe("fetchRegistry", () => {
|
|||||||
expect(result[1]).toMatchObject({ name: "card" })
|
expect(result[1]).toMatchObject({ name: "card" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getRegistryItem with local files", () => {
|
||||||
|
it("should read and parse a valid local JSON file", async () => {
|
||||||
|
// Create a temporary file
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "test-component.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "test-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
dependencies: ["@radix-ui/react-dialog"],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/test-component.tsx",
|
||||||
|
content: "// test component content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "test-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
dependencies: ["@radix-ui/react-dialog"],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/test-component.tsx",
|
||||||
|
content: "// test component content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle relative paths", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "relative-component.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "relative-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change to temp directory to test relative path
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
process.chdir(tempDir)
|
||||||
|
|
||||||
|
const result = await getRegistryItem(
|
||||||
|
"./relative-component.json",
|
||||||
|
"unused-style"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "relative-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle tilde (~) home directory paths", async () => {
|
||||||
|
const os = await import("os")
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
const tempFile = path.join(homeDir, "shadcn-test-tilde.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "tilde-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test with tilde path
|
||||||
|
const tildeePath = "~/shadcn-test-tilde.json"
|
||||||
|
const result = await getRegistryItem(tildeePath, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "tilde-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for non-existent files", async () => {
|
||||||
|
const result = await getRegistryItem(
|
||||||
|
"/non/existent/file.json",
|
||||||
|
"unused-style"
|
||||||
|
)
|
||||||
|
expect(result).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for invalid JSON", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "invalid.json")
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, "{ invalid json }")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
expect(result).toBe(null)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for JSON that doesn't match registry schema", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "invalid-schema.json")
|
||||||
|
|
||||||
|
const invalidData = {
|
||||||
|
notAValidRegistryItem: true,
|
||||||
|
missing: "required fields",
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(invalidData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
expect(result).toBe(null)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should still handle URLs and component names", async () => {
|
||||||
|
// Test that existing functionality still works
|
||||||
|
const result = await getRegistryItem("button", "new-york")
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "button",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle local files with URL dependencies", async () => {
|
||||||
|
// Mock a URL endpoint for dependency
|
||||||
|
const dependencyUrl = "https://example.com/dependency.json"
|
||||||
|
server.use(
|
||||||
|
http.get(dependencyUrl, () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
name: "url-dependency",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/url-dependency.tsx",
|
||||||
|
content: "// url dependency content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl, "button"], // Mix of URL and registry name
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/component-with-url-deps.tsx",
|
||||||
|
content: "// component with url deps content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl, "button"],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("registryResolveItemsTree with URL dependencies", () => {
|
||||||
|
it("should resolve URL dependencies from local files", async () => {
|
||||||
|
// Mock a URL endpoint for dependency
|
||||||
|
const dependencyUrl = "https://example.com/dependency.json"
|
||||||
|
server.use(
|
||||||
|
http.get(dependencyUrl, () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
name: "url-dependency",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/url-dependency.tsx",
|
||||||
|
content: "// url dependency content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl], // URL dependency
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/component-with-url-deps.tsx",
|
||||||
|
content: "// component with url deps content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mockConfig = {
|
||||||
|
style: "new-york",
|
||||||
|
tailwind: { baseColor: "neutral", cssVariables: true },
|
||||||
|
resolvedPaths: { cwd: process.cwd() },
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = await registryResolveItemsTree([tempFile], mockConfig)
|
||||||
|
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result?.files).toBeDefined()
|
||||||
|
// Should contain files from both the main component and its URL dependency
|
||||||
|
const filePaths = result?.files?.map((f: any) => f.path) ?? []
|
||||||
|
expect(filePaths).toContain("ui/component-with-url-deps.tsx")
|
||||||
|
expect(filePaths).toContain("ui/url-dependency.tsx")
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { homedir } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { isLocalFile } from "@/src/registry/utils"
|
||||||
import { Config, getTargetStyleFromConfig } from "@/src/utils/get-config"
|
import { Config, getTargetStyleFromConfig } from "@/src/utils/get-config"
|
||||||
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
@@ -85,6 +88,12 @@ export async function getRegistryIcons() {
|
|||||||
|
|
||||||
export async function getRegistryItem(name: string, style: string) {
|
export async function getRegistryItem(name: string, style: string) {
|
||||||
try {
|
try {
|
||||||
|
// Handle local file paths
|
||||||
|
if (isLocalFile(name)) {
|
||||||
|
return await getLocalRegistryItem(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle URLs and component names
|
||||||
const [result] = await fetchRegistry([
|
const [result] = await fetchRegistry([
|
||||||
isUrl(name) ? name : `styles/${style}/${name}.json`,
|
isUrl(name) ? name : `styles/${style}/${name}.json`,
|
||||||
])
|
])
|
||||||
@@ -97,6 +106,26 @@ export async function getRegistryItem(name: string, style: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getLocalRegistryItem(filePath: string) {
|
||||||
|
try {
|
||||||
|
// Handle tilde expansion for home directory
|
||||||
|
let expandedPath = filePath
|
||||||
|
if (filePath.startsWith("~/")) {
|
||||||
|
expandedPath = path.join(homedir(), filePath.slice(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.resolve(expandedPath)
|
||||||
|
const content = await fs.readFile(resolvedPath, "utf8")
|
||||||
|
const parsed = JSON.parse(content)
|
||||||
|
|
||||||
|
return registryItemSchema.parse(parsed)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to read local registry file: ${filePath}`)
|
||||||
|
handleError(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRegistryBaseColors() {
|
export async function getRegistryBaseColors() {
|
||||||
return BASE_COLORS
|
return BASE_COLORS
|
||||||
}
|
}
|
||||||
@@ -263,26 +292,144 @@ export function clearRegistryCache() {
|
|||||||
registryCache.clear()
|
registryCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveDependenciesRecursively(
|
||||||
|
dependencies: string[],
|
||||||
|
config?: Config,
|
||||||
|
visited: Set<string> = new Set()
|
||||||
|
): Promise<{
|
||||||
|
items: z.infer<typeof registryItemSchema>[]
|
||||||
|
registryNames: string[]
|
||||||
|
}> {
|
||||||
|
const items: z.infer<typeof registryItemSchema>[] = []
|
||||||
|
const registryNames: string[] = []
|
||||||
|
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
// Avoid infinite recursion.
|
||||||
|
if (visited.has(dep)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited.add(dep)
|
||||||
|
|
||||||
|
if (isUrl(dep) || isLocalFile(dep)) {
|
||||||
|
const item = await getRegistryItem(dep, "")
|
||||||
|
if (item) {
|
||||||
|
items.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
const nested = await resolveDependenciesRecursively(
|
||||||
|
item.registryDependencies,
|
||||||
|
config,
|
||||||
|
visited
|
||||||
|
)
|
||||||
|
items.push(...nested.items)
|
||||||
|
registryNames.push(...nested.registryNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Registry name - add it to the list
|
||||||
|
registryNames.push(dep)
|
||||||
|
|
||||||
|
// If we have config, we can also fetch the item to get its dependencies
|
||||||
|
if (config) {
|
||||||
|
const style = config.resolvedPaths?.cwd
|
||||||
|
? await getTargetStyleFromConfig(
|
||||||
|
config.resolvedPaths.cwd,
|
||||||
|
config.style
|
||||||
|
)
|
||||||
|
: config.style
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = await getRegistryItem(dep, style)
|
||||||
|
if (item && item.registryDependencies) {
|
||||||
|
const nested = await resolveDependenciesRecursively(
|
||||||
|
item.registryDependencies,
|
||||||
|
config,
|
||||||
|
visited
|
||||||
|
)
|
||||||
|
items.push(...nested.items)
|
||||||
|
registryNames.push(...nested.registryNames)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't fetch the registry item, that's okay - we'll still include the name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items, registryNames }
|
||||||
|
}
|
||||||
|
|
||||||
export async function registryResolveItemsTree(
|
export async function registryResolveItemsTree(
|
||||||
names: z.infer<typeof registryItemSchema>["name"][],
|
names: z.infer<typeof registryItemSchema>["name"][],
|
||||||
config: Config
|
config: Config
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const index = await getRegistryIndex()
|
// Separate local files, URLs, and registry names.
|
||||||
if (!index) {
|
const localFiles = names.filter((name) => isLocalFile(name))
|
||||||
return null
|
const urls = names.filter((name) => isUrl(name))
|
||||||
|
const registryNames = names.filter(
|
||||||
|
(name) => !isLocalFile(name) && !isUrl(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
const payload: z.infer<typeof registryItemSchema>[] = []
|
||||||
|
|
||||||
|
// Handle local files and URLs directly, collecting their dependencies.
|
||||||
|
const allDependencies: string[] = []
|
||||||
|
|
||||||
|
for (const localFile of localFiles) {
|
||||||
|
const item = await getRegistryItem(localFile, "")
|
||||||
|
if (item) {
|
||||||
|
payload.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
allDependencies.push(...item.registryDependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're resolving the index, we want it to go first.
|
for (const url of urls) {
|
||||||
if (names.includes("index")) {
|
const item = await getRegistryItem(url, "")
|
||||||
names.unshift("index")
|
if (item) {
|
||||||
|
payload.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
allDependencies.push(...item.registryDependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let registryItems = await resolveRegistryItems(names, config)
|
// Recursively resolve all dependencies.
|
||||||
let result = await fetchRegistry(registryItems)
|
const { items: dependencyItems, registryNames: dependencyRegistryNames } =
|
||||||
const payload = z.array(registryItemSchema).parse(result)
|
await resolveDependenciesRecursively(allDependencies, config)
|
||||||
|
|
||||||
if (!payload) {
|
payload.push(...dependencyItems)
|
||||||
|
|
||||||
|
// Handle registry names using existing resolveRegistryItems logic.
|
||||||
|
const allRegistryNames = [...registryNames, ...dependencyRegistryNames]
|
||||||
|
if (allRegistryNames.length > 0) {
|
||||||
|
const index = await getRegistryIndex()
|
||||||
|
if (!index) {
|
||||||
|
// If we only have local files or URLs, that's fine.
|
||||||
|
if (payload.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove duplicates.
|
||||||
|
const uniqueRegistryNames = Array.from(new Set(allRegistryNames))
|
||||||
|
|
||||||
|
// If we're resolving the index, we want it to go first.
|
||||||
|
if (uniqueRegistryNames.includes("index")) {
|
||||||
|
uniqueRegistryNames.unshift("index")
|
||||||
|
}
|
||||||
|
|
||||||
|
let registryItems = await resolveRegistryItems(
|
||||||
|
uniqueRegistryNames,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
let result = await fetchRegistry(registryItems)
|
||||||
|
const registryPayload = z.array(registryItemSchema).parse(result)
|
||||||
|
payload.push(...registryPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +437,7 @@ export async function registryResolveItemsTree(
|
|||||||
// the theme item if a base color is provided.
|
// the theme item if a base color is provided.
|
||||||
// We do this for index only.
|
// We do this for index only.
|
||||||
// Other components will ship with their theme tokens.
|
// Other components will ship with their theme tokens.
|
||||||
if (names.includes("index")) {
|
if (allRegistryNames.includes("index")) {
|
||||||
if (config.tailwind.baseColor) {
|
if (config.tailwind.baseColor) {
|
||||||
const theme = await registryGetTheme(config.tailwind.baseColor, config)
|
const theme = await registryGetTheme(config.tailwind.baseColor, config)
|
||||||
if (theme) {
|
if (theme) {
|
||||||
@@ -352,44 +499,17 @@ async function resolveRegistryDependencies(
|
|||||||
url: string,
|
url: string,
|
||||||
config: Config
|
config: Config
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const visited = new Set<string>()
|
const { registryNames } = await resolveDependenciesRecursively([url], config)
|
||||||
const payload: string[] = []
|
|
||||||
|
|
||||||
const style = config.resolvedPaths?.cwd
|
const style = config.resolvedPaths?.cwd
|
||||||
? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style)
|
? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style)
|
||||||
: config.style
|
: config.style
|
||||||
|
|
||||||
async function resolveDependencies(itemUrl: string) {
|
const urls = registryNames.map((name) =>
|
||||||
const url = getRegistryUrl(
|
getRegistryUrl(isUrl(name) ? name : `styles/${style}/${name}.json`)
|
||||||
isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json`
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (visited.has(url)) {
|
return Array.from(new Set(urls))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.add(url)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [result] = await fetchRegistry([url])
|
|
||||||
const item = registryItemSchema.parse(result)
|
|
||||||
payload.push(url)
|
|
||||||
|
|
||||||
if (item.registryDependencies) {
|
|
||||||
for (const dependency of item.registryDependencies) {
|
|
||||||
await resolveDependencies(dependency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error fetching or parsing registry item at ${itemUrl}:`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await resolveDependencies(url)
|
|
||||||
return Array.from(new Set(payload))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registryGetTheme(name: string, config: Config) {
|
export async function registryGetTheme(name: string, config: Config) {
|
||||||
@@ -495,7 +615,13 @@ export function isUrl(path: string) {
|
|||||||
// TODO: We're double-fetching here. Use a cache.
|
// TODO: We're double-fetching here. Use a cache.
|
||||||
export async function resolveRegistryItems(names: string[], config: Config) {
|
export async function resolveRegistryItems(names: string[], config: Config) {
|
||||||
let registryDependencies: string[] = []
|
let registryDependencies: string[] = []
|
||||||
for (const name of names) {
|
|
||||||
|
// Filter out local files and URLs - these should be handled directly by getRegistryItem
|
||||||
|
const registryNames = names.filter(
|
||||||
|
(name) => !isLocalFile(name) && !isUrl(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const name of registryNames) {
|
||||||
const itemRegistryDependencies = await resolveRegistryDependencies(
|
const itemRegistryDependencies = await resolveRegistryDependencies(
|
||||||
name,
|
name,
|
||||||
config
|
config
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
import { getDependencyFromModuleSpecifier } from "./utils"
|
import { getDependencyFromModuleSpecifier, isLocalFile, isUrl } from "./utils"
|
||||||
|
|
||||||
describe("getDependencyFromModuleSpecifier", () => {
|
describe("getDependencyFromModuleSpecifier", () => {
|
||||||
it("should return the first part of a non-scoped package with path", () => {
|
it("should return the first part of a non-scoped package with path", () => {
|
||||||
@@ -74,3 +74,59 @@ describe("getDependencyFromModuleSpecifier", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("isUrl", () => {
|
||||||
|
it("should return true for valid URLs", () => {
|
||||||
|
expect(isUrl("https://example.com")).toBe(true)
|
||||||
|
expect(isUrl("http://example.com")).toBe(true)
|
||||||
|
expect(isUrl("https://example.com/path")).toBe(true)
|
||||||
|
expect(isUrl("https://subdomain.example.com")).toBe(true)
|
||||||
|
expect(isUrl("https://ui.shadcn.com/r/styles/new-york/button.json")).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for non-URLs", () => {
|
||||||
|
expect(isUrl("./local-file.json")).toBe(false)
|
||||||
|
expect(isUrl("../relative/path.json")).toBe(false)
|
||||||
|
expect(isUrl("/absolute/path.json")).toBe(false)
|
||||||
|
expect(isUrl("component-name")).toBe(false)
|
||||||
|
expect(isUrl("")).toBe(false)
|
||||||
|
expect(isUrl("just-text")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isLocalFile", () => {
|
||||||
|
it("should return true for local JSON files", () => {
|
||||||
|
expect(isLocalFile("./component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("../shared/button.json")).toBe(true)
|
||||||
|
expect(isLocalFile("/absolute/path/card.json")).toBe(true)
|
||||||
|
expect(isLocalFile("local-component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("nested/directory/dialog.json")).toBe(true)
|
||||||
|
expect(isLocalFile("~/Desktop/component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("~/Documents/shared/button.json")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for URLs ending with .json", () => {
|
||||||
|
expect(isLocalFile("https://example.com/component.json")).toBe(false)
|
||||||
|
expect(isLocalFile("http://registry.com/button.json")).toBe(false)
|
||||||
|
expect(
|
||||||
|
isLocalFile("https://ui.shadcn.com/r/styles/new-york/button.json")
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for non-JSON files", () => {
|
||||||
|
expect(isLocalFile("./component.tsx")).toBe(false)
|
||||||
|
expect(isLocalFile("../shared/button.ts")).toBe(false)
|
||||||
|
expect(isLocalFile("/absolute/path/card.js")).toBe(false)
|
||||||
|
expect(isLocalFile("local-component.css")).toBe(false)
|
||||||
|
expect(isLocalFile("component-name")).toBe(false)
|
||||||
|
expect(isLocalFile("")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for directory paths", () => {
|
||||||
|
expect(isLocalFile("./components/")).toBe(false)
|
||||||
|
expect(isLocalFile("../shared")).toBe(false)
|
||||||
|
expect(isLocalFile("/absolute/path")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -242,3 +242,17 @@ function determineFileType(
|
|||||||
|
|
||||||
return "registry:component"
|
return "registry:component"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional utility functions for local file support
|
||||||
|
export function isUrl(path: string) {
|
||||||
|
try {
|
||||||
|
new URL(path)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalFile(path: string) {
|
||||||
|
return path.endsWith(".json") && !isUrl(path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { fetchRegistry } from "@/src/registry/api"
|
import { fetchRegistry } from "@/src/registry/api"
|
||||||
|
import { spinner } from "@/src/utils/spinner"
|
||||||
import { execa } from "execa"
|
import { execa } from "execa"
|
||||||
import fs from "fs-extra"
|
import fs from "fs-extra"
|
||||||
import prompts from "prompts"
|
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"
|
import { TEMPLATES, createProject } from "./create-project"
|
||||||
|
|
||||||
@@ -14,16 +23,85 @@ vi.mock("@/src/registry/api")
|
|||||||
vi.mock("@/src/utils/get-package-manager", () => ({
|
vi.mock("@/src/utils/get-package-manager", () => ({
|
||||||
getPackageManager: vi.fn().mockResolvedValue("npm"),
|
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", () => {
|
describe("createProject", () => {
|
||||||
|
let mockExit: MockInstance
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
// Reset all fs mocks
|
||||||
vi.mocked(fs.access).mockResolvedValue(undefined)
|
vi.mocked(fs.access).mockResolvedValue(undefined)
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
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(() => {
|
afterEach(() => {
|
||||||
vi.resetAllMocks()
|
vi.resetAllMocks()
|
||||||
|
mockExit?.mockRestore()
|
||||||
|
delete (global as any).fetch
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a Next.js project with default options", async () => {
|
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 () => {
|
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" })
|
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "existing-app" })
|
||||||
|
|
||||||
const mockExit = vi
|
mockExit = vi
|
||||||
.spyOn(process, "exit")
|
.spyOn(process, "exit")
|
||||||
.mockImplementation(() => undefined as never)
|
.mockImplementation(() => undefined as never)
|
||||||
|
|
||||||
@@ -103,7 +184,7 @@ describe("createProject", () => {
|
|||||||
vi.mocked(fs.access).mockRejectedValue(new Error("Permission denied"))
|
vi.mocked(fs.access).mockRejectedValue(new Error("Permission denied"))
|
||||||
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
|
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
|
||||||
|
|
||||||
const mockExit = vi
|
mockExit = vi
|
||||||
.spyOn(process, "exit")
|
.spyOn(process, "exit")
|
||||||
.mockImplementation(() => undefined as never)
|
.mockImplementation(() => undefined as never)
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ export const FRAMEWORKS = {
|
|||||||
tailwind: "https://tailwindcss.com/docs/guides/gatsby",
|
tailwind: "https://tailwindcss.com/docs/guides/gatsby",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expo: {
|
||||||
|
name: "expo",
|
||||||
|
label: "Expo",
|
||||||
|
links: {
|
||||||
|
installation: "https://ui.shadcn.com/docs/installation/expo",
|
||||||
|
tailwind: "https://www.nativewind.dev/docs/getting-started/installation",
|
||||||
|
},
|
||||||
|
},
|
||||||
manual: {
|
manual: {
|
||||||
name: "manual",
|
name: "manual",
|
||||||
label: "Manual",
|
label: "Manual",
|
||||||
|
|||||||
@@ -121,11 +121,10 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
|||||||
|
|
||||||
// TanStack Start.
|
// TanStack Start.
|
||||||
if (
|
if (
|
||||||
configFiles.find((file) => file.startsWith("app.config."))?.length &&
|
|
||||||
[
|
[
|
||||||
...Object.keys(packageJson?.dependencies ?? {}),
|
...Object.keys(packageJson?.dependencies ?? {}),
|
||||||
...Object.keys(packageJson?.devDependencies ?? {}),
|
...Object.keys(packageJson?.devDependencies ?? {}),
|
||||||
].find((dep) => dep.startsWith("@tanstack/start"))
|
].find((dep) => dep.startsWith("@tanstack/react-start"))
|
||||||
) {
|
) {
|
||||||
type.framework = FRAMEWORKS["tanstack-start"]
|
type.framework = FRAMEWORKS["tanstack-start"]
|
||||||
return type
|
return type
|
||||||
@@ -147,6 +146,26 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vinxi-based (such as @tanstack/start and @solidjs/solid-start)
|
||||||
|
// They are vite-based, and the same configurations used for Vite should work flawlessly
|
||||||
|
const appConfig = configFiles.find((file) => file.startsWith("app.config"))
|
||||||
|
if (appConfig?.length) {
|
||||||
|
const appConfigContents = await fs.readFile(
|
||||||
|
path.resolve(cwd, appConfig),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
if (appConfigContents.includes("defineConfig")) {
|
||||||
|
type.framework = FRAMEWORKS["vite"]
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expo.
|
||||||
|
if (packageJson?.dependencies?.expo) {
|
||||||
|
type.framework = FRAMEWORKS["expo"]
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,70 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
|
|||||||
|
|
||||||
const [, name, params] = atRuleMatch
|
const [, name, params] = atRuleMatch
|
||||||
|
|
||||||
|
// Special handling for plugins - place them after imports
|
||||||
|
if (name === "plugin") {
|
||||||
|
// Find existing plugin with same params
|
||||||
|
const existingPlugin = root.nodes?.find(
|
||||||
|
(node): node is AtRule =>
|
||||||
|
node.type === "atrule" &&
|
||||||
|
node.name === "plugin" &&
|
||||||
|
node.params === params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!existingPlugin) {
|
||||||
|
const pluginRule = postcss.atRule({
|
||||||
|
name: "plugin",
|
||||||
|
params,
|
||||||
|
raws: { semicolon: true, before: "\n" },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find the last import or plugin node to insert after
|
||||||
|
const importNodes = root.nodes?.filter(
|
||||||
|
(node): node is AtRule =>
|
||||||
|
node.type === "atrule" && node.name === "import"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pluginNodes = root.nodes?.filter(
|
||||||
|
(node): node is AtRule =>
|
||||||
|
node.type === "atrule" && node.name === "plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pluginNodes && pluginNodes.length > 0) {
|
||||||
|
// Insert after the last existing plugin
|
||||||
|
const lastPlugin = pluginNodes[pluginNodes.length - 1]
|
||||||
|
root.insertAfter(lastPlugin, pluginRule)
|
||||||
|
} else if (importNodes && importNodes.length > 0) {
|
||||||
|
// Insert after the last import if no plugins exist
|
||||||
|
const lastImport = importNodes[importNodes.length - 1]
|
||||||
|
root.insertAfter(lastImport, pluginRule)
|
||||||
|
// Add a break comment before the first plugin to create spacing
|
||||||
|
root.insertBefore(
|
||||||
|
pluginRule,
|
||||||
|
postcss.comment({ text: "---break---" })
|
||||||
|
)
|
||||||
|
// Add a break comment after the plugin for spacing from other content
|
||||||
|
root.insertAfter(
|
||||||
|
pluginRule,
|
||||||
|
postcss.comment({ text: "---break---" })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// If no imports or plugins, insert at the beginning
|
||||||
|
root.prepend(pluginRule)
|
||||||
|
// Add a break comment before the first plugin for spacing
|
||||||
|
root.insertBefore(
|
||||||
|
pluginRule,
|
||||||
|
postcss.comment({ text: "---break---" })
|
||||||
|
)
|
||||||
|
// Add a break comment after the plugin for spacing from other content
|
||||||
|
root.insertAfter(
|
||||||
|
pluginRule,
|
||||||
|
postcss.comment({ text: "---break---" })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Special handling for keyframes - place them under @theme inline
|
// Special handling for keyframes - place them under @theme inline
|
||||||
if (name === "keyframes") {
|
else if (name === "keyframes") {
|
||||||
let themeInline = root.nodes?.find(
|
let themeInline = root.nodes?.find(
|
||||||
(node): node is AtRule =>
|
(node): node is AtRule =>
|
||||||
node.type === "atrule" &&
|
node.type === "atrule" &&
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function updateDependencies(
|
|||||||
const dependenciesSpinner = spinner(`Installing dependencies.`, {
|
const dependenciesSpinner = spinner(`Installing dependencies.`, {
|
||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
})?.start()
|
})?.start()
|
||||||
const packageManager = await getPackageManager(config.resolvedPaths.cwd)
|
const packageManager = await getUpdateDependenciesPackageManager(config)
|
||||||
|
|
||||||
// Offer to use --force or --legacy-peer-deps if using React 19 with npm.
|
// Offer to use --force or --legacy-peer-deps if using React 19 with npm.
|
||||||
let flag = ""
|
let flag = ""
|
||||||
@@ -62,38 +62,13 @@ export async function updateDependencies(
|
|||||||
|
|
||||||
dependenciesSpinner?.start()
|
dependenciesSpinner?.start()
|
||||||
|
|
||||||
if (dependencies?.length) {
|
await installWithPackageManager(
|
||||||
await execa(
|
packageManager,
|
||||||
packageManager,
|
dependencies,
|
||||||
[
|
devDependencies,
|
||||||
packageManager === "npm" ? "install" : "add",
|
config.resolvedPaths.cwd,
|
||||||
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
|
flag
|
||||||
...(packageManager === "deno"
|
)
|
||||||
? dependencies.map((dep) => `npm:${dep}`)
|
|
||||||
: dependencies),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: config.resolvedPaths.cwd,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devDependencies?.length) {
|
|
||||||
await execa(
|
|
||||||
packageManager,
|
|
||||||
[
|
|
||||||
packageManager === "npm" ? "install" : "add",
|
|
||||||
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
|
|
||||||
"-D",
|
|
||||||
...(packageManager === "deno"
|
|
||||||
? devDependencies.map((dep) => `npm:${dep}`)
|
|
||||||
: devDependencies),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: config.resolvedPaths.cwd,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesSpinner?.succeed()
|
dependenciesSpinner?.succeed()
|
||||||
}
|
}
|
||||||
@@ -113,3 +88,107 @@ function shouldPromptForNpmFlag(config: Config) {
|
|||||||
|
|
||||||
return hasReact19 && hasReactDayPicker8
|
return hasReact19 && hasReactDayPicker8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUpdateDependenciesPackageManager(config: Config) {
|
||||||
|
const expoVersion = getPackageInfo(config.resolvedPaths.cwd, false)
|
||||||
|
?.dependencies?.expo
|
||||||
|
|
||||||
|
if (expoVersion) {
|
||||||
|
// Ensures package versions match the React Native version.
|
||||||
|
// https://docs.expo.dev/more/expo-cli/#install
|
||||||
|
return "expo"
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPackageManager(config.resolvedPaths.cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installWithPackageManager(
|
||||||
|
packageManager: Awaited<
|
||||||
|
ReturnType<typeof getUpdateDependenciesPackageManager>
|
||||||
|
>,
|
||||||
|
dependencies: string[],
|
||||||
|
devDependencies: string[],
|
||||||
|
cwd: string,
|
||||||
|
flag?: string
|
||||||
|
) {
|
||||||
|
if (packageManager === "npm") {
|
||||||
|
return installWithNpm(dependencies, devDependencies, cwd, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageManager === "deno") {
|
||||||
|
return installWithDeno(dependencies, devDependencies, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageManager === "expo") {
|
||||||
|
return installWithExpo(dependencies, devDependencies, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependencies?.length) {
|
||||||
|
await execa(packageManager, ["add", ...dependencies], {
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devDependencies?.length) {
|
||||||
|
await execa(packageManager, ["add", "-D", ...devDependencies], { cwd })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installWithNpm(
|
||||||
|
dependencies: string[],
|
||||||
|
devDependencies: string[],
|
||||||
|
cwd: string,
|
||||||
|
flag?: string
|
||||||
|
) {
|
||||||
|
if (dependencies.length) {
|
||||||
|
await execa(
|
||||||
|
"npm",
|
||||||
|
["install", ...(flag ? [`--${flag}`] : []), ...dependencies],
|
||||||
|
{ cwd }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devDependencies.length) {
|
||||||
|
await execa(
|
||||||
|
"npm",
|
||||||
|
["install", ...(flag ? [`--${flag}`] : []), "-D", ...devDependencies],
|
||||||
|
{ cwd }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installWithDeno(
|
||||||
|
dependencies: string[],
|
||||||
|
devDependencies: string[],
|
||||||
|
cwd: string
|
||||||
|
) {
|
||||||
|
if (dependencies?.length) {
|
||||||
|
await execa("deno", ["add", ...dependencies.map((dep) => `npm:${dep}`)], {
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devDependencies?.length) {
|
||||||
|
await execa(
|
||||||
|
"deno",
|
||||||
|
["add", "-D", ...devDependencies.map((dep) => `npm:${dep}`)],
|
||||||
|
{ cwd }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installWithExpo(
|
||||||
|
dependencies: string[],
|
||||||
|
devDependencies: string[],
|
||||||
|
cwd: string
|
||||||
|
) {
|
||||||
|
if (dependencies.length) {
|
||||||
|
await execa("npx", ["expo", "install", ...dependencies], { cwd })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devDependencies.length) {
|
||||||
|
await execa("npx", ["expo", "install", "-- -D", ...devDependencies], {
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
|
|||||||
initializer &&
|
initializer &&
|
||||||
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
|
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
|
||||||
) {
|
) {
|
||||||
unnsetSpreadElements(
|
unsetSpreadElements(
|
||||||
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
|
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -387,7 +387,7 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
|
export function unsetSpreadElements(arr: ArrayLiteralExpression) {
|
||||||
const elements = arr.getElements()
|
const elements = arr.getElements()
|
||||||
for (let j = 0; j < elements.length; j++) {
|
for (let j = 0; j < elements.length; j++) {
|
||||||
const element = elements[j]
|
const element = elements[j]
|
||||||
@@ -398,7 +398,7 @@ export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
|
|||||||
)
|
)
|
||||||
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
||||||
// Recursive check on nested arrays
|
// Recursive check on nested arrays
|
||||||
unnsetSpreadElements(
|
unsetSpreadElements(
|
||||||
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
|
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
|
||||||
)
|
)
|
||||||
} else if (element.isKind(SyntaxKind.StringLiteral)) {
|
} else if (element.isKind(SyntaxKind.StringLiteral)) {
|
||||||
|
|||||||
@@ -328,4 +328,290 @@ describe("transformCss", () => {
|
|||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should add plugin directive", async () => {
|
||||||
|
const input = `@import "tailwindcss";`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin foo;"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should group plugins together after imports", async () => {
|
||||||
|
const input = `@import "tailwindcss";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
"@plugin bar": {},
|
||||||
|
"@layer components": {
|
||||||
|
".card": {
|
||||||
|
padding: "1rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin foo;
|
||||||
|
@plugin bar;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not add duplicate plugins", async () => {
|
||||||
|
const input = `@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin foo;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
"@plugin bar": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin foo;
|
||||||
|
|
||||||
|
@plugin bar;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should add plugin when no imports exist", async () => {
|
||||||
|
const input = `@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
@plugin foo;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle plugins with quoted parameters", async () => {
|
||||||
|
const input = `@import "tailwindcss";`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin ./custom-plugin.js": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin @tailwindcss/typography;
|
||||||
|
@plugin ./custom-plugin.js;"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle plugins with complex parameters", async () => {
|
||||||
|
const input = `@import "tailwindcss";`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin tailwindcss/plugin": {},
|
||||||
|
"@plugin @headlessui/tailwindcss": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin tailwindcss/plugin;
|
||||||
|
@plugin @headlessui/tailwindcss;"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle multiple imports with plugins", async () => {
|
||||||
|
const input = `@import "tailwindcss";
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
"@plugin bar": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
|
||||||
|
|
||||||
|
@plugin foo;
|
||||||
|
@plugin bar;"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should add plugins to empty file", async () => {
|
||||||
|
const input = ``
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin foo": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
@plugin foo"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should maintain plugin order with existing plugins", async () => {
|
||||||
|
const input = `@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin existing-plugin;
|
||||||
|
@plugin another-existing;`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin new-plugin": {},
|
||||||
|
"@plugin final-plugin": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin existing-plugin;
|
||||||
|
@plugin another-existing;
|
||||||
|
@plugin new-plugin;
|
||||||
|
@plugin final-plugin;"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle comprehensive CSS with plugins", async () => {
|
||||||
|
const input = `@import "tailwindcss";
|
||||||
|
@import url("fonts.css");
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}`
|
||||||
|
|
||||||
|
const result = await transformCss(input, {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin ./custom": {},
|
||||||
|
"@layer components": {
|
||||||
|
".btn": {
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
"&:hover": {
|
||||||
|
"background-color": "blue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"@utility animate-fast": {
|
||||||
|
"animation-duration": "0.1s",
|
||||||
|
},
|
||||||
|
"@keyframes spin-fast": {
|
||||||
|
"0%": { transform: "rotate(0deg)" },
|
||||||
|
"100%": { transform: "rotate(360deg)" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"@import "tailwindcss";
|
||||||
|
@import url("fonts.css");
|
||||||
|
|
||||||
|
@plugin @tailwindcss/typography;
|
||||||
|
@plugin ./custom;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility animate-fast {
|
||||||
|
animation-duration: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
@keyframes spin-fast {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
buildTailwindThemeColorsFromCssVars, nestSpreadElements,
|
buildTailwindThemeColorsFromCssVars, nestSpreadElements,
|
||||||
nestSpreadProperties,
|
nestSpreadProperties,
|
||||||
transformTailwindConfig,
|
transformTailwindConfig,
|
||||||
unnestSpreadProperties, unnsetSpreadElements,
|
unnestSpreadProperties, unsetSpreadElements,
|
||||||
} from "../../../src/utils/updaters/update-tailwind-config"
|
} from "../../../src/utils/updaters/update-tailwind-config"
|
||||||
|
|
||||||
const SHARED_CONFIG = {
|
const SHARED_CONFIG = {
|
||||||
@@ -1186,7 +1186,7 @@ describe("unnestSpreadElements", () => {
|
|||||||
)
|
)
|
||||||
if (!configObject) throw new Error("Config object not found")
|
if (!configObject) throw new Error("Config object not found")
|
||||||
|
|
||||||
unnsetSpreadElements(configObject)
|
unsetSpreadElements(configObject)
|
||||||
|
|
||||||
const result = configObject.getText()
|
const result = configObject.getText()
|
||||||
expect(result.replace(/\s+/g, "")).toBe(expected.replace(/\s+/g, ""))
|
expect(result.replace(/\s+/g, "")).toBe(expected.replace(/\s+/g, ""))
|
||||||
|
|||||||
70
pnpm-lock.yaml
generated
70
pnpm-lock.yaml
generated
@@ -322,7 +322,7 @@ importers:
|
|||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 2.6.1
|
specifier: 2.8.0
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
shiki:
|
shiki:
|
||||||
specifier: ^1.10.1
|
specifier: ^1.10.1
|
||||||
@@ -602,7 +602,7 @@ importers:
|
|||||||
specifier: 2.12.7
|
specifier: 2.12.7
|
||||||
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 2.6.1
|
specifier: 2.8.0
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.32.6
|
specifier: ^0.32.6
|
||||||
@@ -696,16 +696,6 @@ importers:
|
|||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
|
|
||||||
packages/cli:
|
|
||||||
dependencies:
|
|
||||||
chalk:
|
|
||||||
specifier: ^5.4.1
|
|
||||||
version: 5.4.1
|
|
||||||
devDependencies:
|
|
||||||
tsup:
|
|
||||||
specifier: ^6.6.3
|
|
||||||
version: 6.7.0(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)
|
|
||||||
|
|
||||||
packages/shadcn:
|
packages/shadcn:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@antfu/ni':
|
'@antfu/ni':
|
||||||
@@ -4779,10 +4769,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==}
|
resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==}
|
||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
|
|
||||||
chalk@5.4.1:
|
|
||||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
|
||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
|
||||||
|
|
||||||
change-case@5.4.4:
|
change-case@5.4.4:
|
||||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||||
|
|
||||||
@@ -14349,8 +14335,6 @@ snapshots:
|
|||||||
|
|
||||||
chalk@5.2.0: {}
|
chalk@5.2.0: {}
|
||||||
|
|
||||||
chalk@5.4.1: {}
|
|
||||||
|
|
||||||
change-case@5.4.4: {}
|
change-case@5.4.4: {}
|
||||||
|
|
||||||
character-entities-html4@2.1.0: {}
|
character-entities-html4@2.1.0: {}
|
||||||
@@ -18447,14 +18431,6 @@ snapshots:
|
|||||||
postcss: 8.5.1
|
postcss: 8.5.1
|
||||||
ts-node: 10.9.2(@types/node@22.13.0)(typescript@4.9.5)
|
ts-node: 10.9.2(@types/node@22.13.0)(typescript@4.9.5)
|
||||||
|
|
||||||
postcss-load-config@3.1.4(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)):
|
|
||||||
dependencies:
|
|
||||||
lilconfig: 2.1.0
|
|
||||||
yaml: 1.10.2
|
|
||||||
optionalDependencies:
|
|
||||||
postcss: 8.5.1
|
|
||||||
ts-node: 10.9.2(@types/node@22.13.0)(typescript@5.7.3)
|
|
||||||
|
|
||||||
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.7.3)):
|
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.7.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
@@ -20100,25 +20076,6 @@ snapshots:
|
|||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3):
|
|
||||||
dependencies:
|
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
|
||||||
'@tsconfig/node10': 1.0.11
|
|
||||||
'@tsconfig/node12': 1.0.11
|
|
||||||
'@tsconfig/node14': 1.0.3
|
|
||||||
'@tsconfig/node16': 1.0.4
|
|
||||||
'@types/node': 22.13.0
|
|
||||||
acorn: 8.14.0
|
|
||||||
acorn-walk: 8.3.4
|
|
||||||
arg: 4.1.3
|
|
||||||
create-require: 1.1.1
|
|
||||||
diff: 4.0.2
|
|
||||||
make-error: 1.3.6
|
|
||||||
typescript: 5.7.3
|
|
||||||
v8-compile-cache-lib: 3.0.1
|
|
||||||
yn: 3.1.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ts-pattern@5.6.2: {}
|
ts-pattern@5.6.2: {}
|
||||||
|
|
||||||
tsconfck@3.1.4(typescript@5.7.3):
|
tsconfck@3.1.4(typescript@5.7.3):
|
||||||
@@ -20167,29 +20124,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- ts-node
|
- ts-node
|
||||||
|
|
||||||
tsup@6.7.0(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3):
|
|
||||||
dependencies:
|
|
||||||
bundle-require: 4.2.1(esbuild@0.17.19)
|
|
||||||
cac: 6.7.14
|
|
||||||
chokidar: 3.6.0
|
|
||||||
debug: 4.4.0
|
|
||||||
esbuild: 0.17.19
|
|
||||||
execa: 5.1.1
|
|
||||||
globby: 11.1.0
|
|
||||||
joycon: 3.1.1
|
|
||||||
postcss-load-config: 3.1.4(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))
|
|
||||||
resolve-from: 5.0.0
|
|
||||||
rollup: 3.29.5
|
|
||||||
source-map: 0.8.0-beta.0
|
|
||||||
sucrase: 3.35.0
|
|
||||||
tree-kill: 1.2.2
|
|
||||||
optionalDependencies:
|
|
||||||
postcss: 8.5.1
|
|
||||||
typescript: 5.7.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
- ts-node
|
|
||||||
|
|
||||||
tsutils@3.21.0(typescript@5.7.3):
|
tsutils@3.21.0(typescript@5.7.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 1.14.1
|
tslib: 1.14.1
|
||||||
|
|||||||
3
templates/monorepo-next/.vscode/settings.json
vendored
Normal file
3
templates/monorepo-next/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user