mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-30 08:04:18 +00:00
Compare commits
1 Commits
shadcn@2.5
...
shadcn/cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5b964460f |
@@ -73,7 +73,7 @@
|
|||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.1",
|
"recharts": "2.15.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"shadcn": "2.5.0",
|
"shadcn": "2.4.1",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"name": "index",
|
"name": "index",
|
||||||
"type": "registry:style",
|
"type": "registry:style",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"tw-animate-css",
|
||||||
"class-variance-authority",
|
"class-variance-authority",
|
||||||
"lucide-react",
|
"lucide-react"
|
||||||
"tw-animate-css"
|
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
"utils"
|
"utils"
|
||||||
@@ -27,7 +27,37 @@
|
|||||||
"path": "registry/new-york-v4/ui/accordion.tsx",
|
"path": "registry/new-york-v4/ui/accordion.tsx",
|
||||||
"type": "registry:ui"
|
"type": "registry:ui"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"tailwind": {
|
||||||
|
"config": {
|
||||||
|
"theme": {
|
||||||
|
"extend": {
|
||||||
|
"keyframes": {
|
||||||
|
"accordion-down": {
|
||||||
|
"from": {
|
||||||
|
"height": "0"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"height": "var(--radix-accordion-content-height)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
"from": {
|
||||||
|
"height": "var(--radix-accordion-content-height)"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"height": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"animation": {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "alert",
|
"name": "alert",
|
||||||
@@ -2803,4 +2833,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,9 @@ const registry = {
|
|||||||
name: "index",
|
name: "index",
|
||||||
type: "registry:style",
|
type: "registry:style",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"tw-animate-css",
|
||||||
"class-variance-authority",
|
"class-variance-authority",
|
||||||
"lucide-react",
|
"lucide-react",
|
||||||
"tw-animate-css",
|
|
||||||
],
|
],
|
||||||
registryDependencies: ["utils"],
|
registryDependencies: ["utils"],
|
||||||
cssVars: {},
|
cssVars: {},
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ I've been working on a new CLI for the past few weeks. It's a complete rewrite.
|
|||||||
### `init`
|
### `init`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn@latest init
|
npx shadcn-ui@latest init
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run the `init` command, you will be asked a few questions to configure `components.json`:
|
When you run the `init` command, you will be asked a few questions to configure `components.json`:
|
||||||
@@ -363,7 +363,7 @@ This means you can now use the CLI with any directory structure including `src`
|
|||||||
### `add`
|
### `add`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn@latest add
|
npx shadcn-ui@latest add
|
||||||
```
|
```
|
||||||
|
|
||||||
The `add` command is now much more capable. You can now add UI components but also import more complex components (coming soon).
|
The `add` command is now much more capable. You can now add UI components but also import more complex components (coming soon).
|
||||||
@@ -373,7 +373,7 @@ The CLI will automatically resolve all components and dependencies, format them
|
|||||||
### `diff` (experimental)
|
### `diff` (experimental)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn diff
|
npx shadcn-ui diff
|
||||||
```
|
```
|
||||||
|
|
||||||
We're also introducing a new `diff` command to help you keep track of upstream updates.
|
We're also introducing a new `diff` command to help you keep track of upstream updates.
|
||||||
@@ -383,7 +383,7 @@ You can use this command to see what has changed in the upstream repository and
|
|||||||
Run the `diff` command to get a list of components that have updates available:
|
Run the `diff` command to get a list of components that have updates available:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn diff
|
npx shadcn-ui diff
|
||||||
```
|
```
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
@@ -398,7 +398,7 @@ The following components have updates available:
|
|||||||
Then run `diff [component]` to see the changes:
|
Then run `diff [component]` to see the changes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn diff alert
|
npx shadcn-ui diff alert
|
||||||
```
|
```
|
||||||
|
|
||||||
```diff /pl-12/
|
```diff /pl-12/
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const onCreateWebpackConfig = ({ actions }) => {
|
|||||||
|
|
||||||
### Run the CLI
|
### Run the CLI
|
||||||
|
|
||||||
Run the `shadcn` init command to setup your project:
|
Run the `shadcn-ui` init command to setup your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn@latest init
|
npx shadcn@latest init
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ npx create-react-router@latest my-app
|
|||||||
|
|
||||||
### Run the CLI
|
### Run the CLI
|
||||||
|
|
||||||
Run the `shadcn` init command to setup your project:
|
Run the `shadcn-ui` init command to setup your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn@latest init
|
npx shadcn@latest init
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ npx create-remix@latest my-app
|
|||||||
|
|
||||||
### Run the CLI
|
### Run the CLI
|
||||||
|
|
||||||
Run the `shadcn` init command to setup your project:
|
Run the `shadcn-ui` init command to setup your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn@latest init
|
npx shadcn@latest init
|
||||||
|
|||||||
@@ -86,7 +86,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.5.0",
|
"shadcn": "2.4.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
"name": "index",
|
"name": "index",
|
||||||
"type": "registry:style",
|
"type": "registry:style",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"tw-animate-css",
|
||||||
"class-variance-authority",
|
"class-variance-authority",
|
||||||
"lucide-react",
|
"lucide-react"
|
||||||
"tw-animate-css"
|
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
"utils"
|
"utils"
|
||||||
|
|||||||
@@ -85,6 +85,11 @@
|
|||||||
"target": {
|
"target": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The target path of the file. This is the path to the file in the project."
|
"description": "The target path of the file. This is the path to the file in the project."
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["append", "prepend"],
|
||||||
|
"description": "The action to perform on the target file. Can be append or prepend."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"if": {
|
"if": {
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
# @shadcn/ui
|
# @shadcn/ui
|
||||||
|
|
||||||
## 2.5.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#7220](https://github.com/shadcn-ui/ui/pull/7220) [`d0306774fe0ecc1eae9ef1e918bf7862e866a9e8`](https://github.com/shadcn-ui/ui/commit/d0306774fe0ecc1eae9ef1e918bf7862e866a9e8) Thanks [@shadcn](https://github.com/shadcn)! - resolve imports from anywhere
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#6985](https://github.com/shadcn-ui/ui/pull/6985) [`f1e5cc4666ced2166a859660d769ccee16cde46e`](https://github.com/shadcn-ui/ui/commit/f1e5cc4666ced2166a859660d769ccee16cde46e) Thanks [@nrjdalal](https://github.com/nrjdalal)! - move tw-animate-css to devDependencies
|
|
||||||
|
|
||||||
- [#6899](https://github.com/shadcn-ui/ui/pull/6899) [`6f702f5fbf2b82a388e7da6ea08bcc84c2ec19c6`](https://github.com/shadcn-ui/ui/commit/6f702f5fbf2b82a388e7da6ea08bcc84c2ec19c6) Thanks [@justjavac](https://github.com/justjavac)! - add deno support
|
|
||||||
|
|
||||||
## 2.4.1
|
## 2.4.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "2.5.0",
|
"version": "2.4.1",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ export const build = new Command()
|
|||||||
|
|
||||||
// Loop through each file in the files array.
|
// Loop through each file in the files array.
|
||||||
for (const file of registryItem.files) {
|
for (const file of registryItem.files) {
|
||||||
|
if (file["content"]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
file["content"] = await fs.readFile(
|
file["content"] = await fs.readFile(
|
||||||
path.resolve(resolvePaths.cwd, file.path),
|
path.resolve(resolvePaths.cwd, file.path),
|
||||||
"utf-8"
|
"utf-8"
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export async function migrateIcons(config: Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (targetLibrary.package) {
|
if (targetLibrary.package) {
|
||||||
await updateDependencies([targetLibrary.package], [], config, {
|
await updateDependencies([targetLibrary.package], config, {
|
||||||
silent: false,
|
silent: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export const registryItemTypeSchema = z.enum([
|
|||||||
"registry:internal",
|
"registry:internal",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const registryItemFileActionSchema = z
|
||||||
|
.enum(["append", "prepend"])
|
||||||
|
.optional()
|
||||||
|
|
||||||
export const registryItemFileSchema = z.discriminatedUnion("type", [
|
export const registryItemFileSchema = z.discriminatedUnion("type", [
|
||||||
// Target is required for registry:file and registry:page
|
// Target is required for registry:file and registry:page
|
||||||
z.object({
|
z.object({
|
||||||
@@ -26,12 +30,14 @@ export const registryItemFileSchema = z.discriminatedUnion("type", [
|
|||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
type: z.enum(["registry:file", "registry:page"]),
|
type: z.enum(["registry:file", "registry:page"]),
|
||||||
target: z.string(),
|
target: z.string(),
|
||||||
|
action: registryItemFileActionSchema,
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
type: registryItemTypeSchema.exclude(["registry:file", "registry:page"]),
|
type: registryItemTypeSchema.exclude(["registry:file", "registry:page"]),
|
||||||
target: z.string().optional(),
|
target: z.string().optional(),
|
||||||
|
action: registryItemFileActionSchema,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ async function addProjectComponents(
|
|||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
|
await updateDependencies(tree.dependencies, config, {
|
||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
})
|
})
|
||||||
await updateFiles(tree.files, config, {
|
await updateFiles(tree.files, config, {
|
||||||
@@ -213,14 +213,9 @@ async function addWorkspaceComponents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update dependencies.
|
// 4. Update dependencies.
|
||||||
await updateDependencies(
|
await updateDependencies(component.dependencies, targetConfig, {
|
||||||
component.dependencies,
|
silent: true,
|
||||||
component.devDependencies,
|
})
|
||||||
targetConfig,
|
|
||||||
{
|
|
||||||
silent: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 5. Update files.
|
// 5. Update files.
|
||||||
const files = await updateFiles(component.files, targetConfig, {
|
const files = await updateFiles(component.files, targetConfig, {
|
||||||
|
|||||||
@@ -9,16 +9,13 @@ import prompts from "prompts"
|
|||||||
|
|
||||||
export async function updateDependencies(
|
export async function updateDependencies(
|
||||||
dependencies: RegistryItem["dependencies"],
|
dependencies: RegistryItem["dependencies"],
|
||||||
devDependencies: RegistryItem["devDependencies"],
|
|
||||||
config: Config,
|
config: Config,
|
||||||
options: {
|
options: {
|
||||||
silent?: boolean
|
silent?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
dependencies = Array.from(new Set(dependencies))
|
dependencies = Array.from(new Set(dependencies))
|
||||||
devDependencies = Array.from(new Set(devDependencies))
|
if (!dependencies?.length) {
|
||||||
|
|
||||||
if (!dependencies?.length && !devDependencies?.length) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,44 +59,23 @@ export async function updateDependencies(
|
|||||||
|
|
||||||
dependenciesSpinner?.start()
|
dependenciesSpinner?.start()
|
||||||
|
|
||||||
if (dependencies?.length) {
|
await execa(
|
||||||
await execa(
|
packageManager,
|
||||||
packageManager,
|
[
|
||||||
[
|
packageManager === "npm" ? "install" : "add",
|
||||||
packageManager === "npm" ? "install" : "add",
|
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
|
||||||
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
|
...dependencies,
|
||||||
...(packageManager === "deno"
|
],
|
||||||
? dependencies.map((dep) => `npm:${dep}`)
|
{
|
||||||
: dependencies),
|
cwd: config.resolvedPaths.cwd,
|
||||||
],
|
}
|
||||||
{
|
)
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsingReact19(config: Config) {
|
function isUsingReact19(config: Config) {
|
||||||
const packageInfo = getPackageInfo(config.resolvedPaths.cwd, false)
|
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
|
||||||
|
|
||||||
if (!packageInfo?.dependencies?.react) {
|
if (!packageInfo?.dependencies?.react) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { existsSync, promises as fs } from "fs"
|
import { existsSync, promises as fs } from "fs"
|
||||||
import { tmpdir } from "os"
|
|
||||||
import path, { basename } from "path"
|
import path, { basename } from "path"
|
||||||
import { getRegistryBaseColor } from "@/src/registry/api"
|
import { getRegistryBaseColor } from "@/src/registry/api"
|
||||||
import { RegistryItem, registryItemFileSchema } from "@/src/registry/schema"
|
import {
|
||||||
|
RegistryItem,
|
||||||
|
registryItemFileActionSchema,
|
||||||
|
registryItemFileSchema,
|
||||||
|
} from "@/src/registry/schema"
|
||||||
import { Config } from "@/src/utils/get-config"
|
import { Config } from "@/src/utils/get-config"
|
||||||
import { ProjectInfo, getProjectInfo } from "@/src/utils/get-project-info"
|
import { ProjectInfo, getProjectInfo } from "@/src/utils/get-project-info"
|
||||||
import { highlighter } from "@/src/utils/highlighter"
|
import { highlighter } from "@/src/utils/highlighter"
|
||||||
import { logger } from "@/src/utils/logger"
|
import { logger } from "@/src/utils/logger"
|
||||||
import { resolveImport } from "@/src/utils/resolve-import"
|
|
||||||
import { spinner } from "@/src/utils/spinner"
|
import { spinner } from "@/src/utils/spinner"
|
||||||
import { transform } from "@/src/utils/transformers"
|
import { transform } from "@/src/utils/transformers"
|
||||||
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
|
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
|
||||||
@@ -16,10 +18,33 @@ import { transformImport } from "@/src/utils/transformers/transform-import"
|
|||||||
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
|
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
|
||||||
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
|
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
|
||||||
import prompts from "prompts"
|
import prompts from "prompts"
|
||||||
import { Project, ScriptKind } from "ts-morph"
|
|
||||||
import { loadConfig } from "tsconfig-paths"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
async function applyFileAction(
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
action: z.infer<typeof registryItemFileActionSchema>
|
||||||
|
) {
|
||||||
|
if (!action) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only try to read existing content if the file exists
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
const existingContent = await fs.readFile(filePath, "utf-8")
|
||||||
|
|
||||||
|
if (action === "append") {
|
||||||
|
return `${existingContent}\n${content}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "prepend") {
|
||||||
|
return `${content}\n${existingContent}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateFiles(
|
export async function updateFiles(
|
||||||
files: RegistryItem["files"],
|
files: RegistryItem["files"],
|
||||||
config: Config,
|
config: Config,
|
||||||
@@ -54,9 +79,9 @@ export async function updateFiles(
|
|||||||
getRegistryBaseColor(config.tailwind.baseColor),
|
getRegistryBaseColor(config.tailwind.baseColor),
|
||||||
])
|
])
|
||||||
|
|
||||||
let filesCreated: string[] = []
|
const filesCreated = []
|
||||||
let filesUpdated: string[] = []
|
const filesUpdated = []
|
||||||
let filesSkipped: string[] = []
|
const filesSkipped = []
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.content) {
|
if (!file.content) {
|
||||||
@@ -113,10 +138,29 @@ export async function updateFiles(
|
|||||||
getNormalizedFileContent(existingFileContent),
|
getNormalizedFileContent(existingFileContent),
|
||||||
getNormalizedFileContent(content),
|
getNormalizedFileContent(content),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Check if content is already the same
|
||||||
if (normalizedExisting === normalizedNew) {
|
if (normalizedExisting === normalizedNew) {
|
||||||
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check if action is already applied
|
||||||
|
if (file.action) {
|
||||||
|
if (file.action === "append") {
|
||||||
|
// Check if the content is already appended
|
||||||
|
if (normalizedExisting.endsWith(normalizedNew)) {
|
||||||
|
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if (file.action === "prepend") {
|
||||||
|
// Check if the content is already prepended
|
||||||
|
if (normalizedExisting.startsWith(normalizedNew)) {
|
||||||
|
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingFile && !options.overwrite) {
|
if (existingFile && !options.overwrite) {
|
||||||
@@ -151,31 +195,31 @@ export async function updateFiles(
|
|||||||
await fs.mkdir(targetDir, { recursive: true })
|
await fs.mkdir(targetDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(filePath, content, "utf-8")
|
let finalContent = content
|
||||||
existingFile
|
if (existingFile && file.action) {
|
||||||
? filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath))
|
const existingFileContent = await fs.readFile(filePath, "utf-8")
|
||||||
: filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath))
|
if (file.action === "append") {
|
||||||
|
finalContent = `${existingFileContent}\n${content}`
|
||||||
|
} else if (file.action === "prepend") {
|
||||||
|
finalContent = `${content}\n${existingFileContent}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, finalContent, "utf-8")
|
||||||
|
|
||||||
|
const relativePath = path.relative(config.resolvedPaths.cwd, filePath)
|
||||||
|
if (existingFile) {
|
||||||
|
filesUpdated.push(relativePath)
|
||||||
|
} else {
|
||||||
|
filesCreated.push(relativePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allFiles = [...filesCreated, ...filesUpdated, ...filesSkipped]
|
|
||||||
const updatedFiles = await resolveImports(allFiles, config)
|
|
||||||
|
|
||||||
// Let's update filesUpdated with the updated files.
|
|
||||||
filesUpdated.push(...updatedFiles)
|
|
||||||
|
|
||||||
// If a file is in filesCreated and filesUpdated, we should remove it from filesUpdated.
|
|
||||||
filesUpdated = filesUpdated.filter((file) => !filesCreated.includes(file))
|
|
||||||
|
|
||||||
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
|
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
|
||||||
if (!hasUpdatedFiles && !filesSkipped.length) {
|
if (!hasUpdatedFiles && !filesSkipped.length) {
|
||||||
filesCreatedSpinner?.info("No files updated.")
|
filesCreatedSpinner?.info("No files updated.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicates.
|
|
||||||
filesCreated = Array.from(new Set(filesCreated))
|
|
||||||
filesUpdated = Array.from(new Set(filesUpdated))
|
|
||||||
filesSkipped = Array.from(new Set(filesSkipped))
|
|
||||||
|
|
||||||
if (filesCreated.length) {
|
if (filesCreated.length) {
|
||||||
filesCreatedSpinner?.succeed(
|
filesCreatedSpinner?.succeed(
|
||||||
`Created ${filesCreated.length} ${
|
`Created ${filesCreated.length} ${
|
||||||
@@ -210,7 +254,7 @@ export async function updateFiles(
|
|||||||
if (filesSkipped.length) {
|
if (filesSkipped.length) {
|
||||||
spinner(
|
spinner(
|
||||||
`Skipped ${filesSkipped.length} ${
|
`Skipped ${filesSkipped.length} ${
|
||||||
filesUpdated.length === 1 ? "file" : "files"
|
filesSkipped.length === 1 ? "file" : "files"
|
||||||
}: (files might be identical, use --overwrite to overwrite)`,
|
}: (files might be identical, use --overwrite to overwrite)`,
|
||||||
{
|
{
|
||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
@@ -350,7 +394,10 @@ export function resolveNestedFilePath(
|
|||||||
return fileSegments.slice(commonDirIndex + 1).join("/")
|
return fileSegments.slice(commonDirIndex + 1).join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNormalizedFileContent(content: string) {
|
export async function getNormalizedFileContent(content: string | undefined) {
|
||||||
|
if (!content) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return content.replace(/\r\n/g, "\n").trim()
|
return content.replace(/\r\n/g, "\n").trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,227 +436,3 @@ export function resolvePageTarget(
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveImports(filePaths: string[], config: Config) {
|
|
||||||
const project = new Project({
|
|
||||||
compilerOptions: {},
|
|
||||||
})
|
|
||||||
const projectInfo = await getProjectInfo(config.resolvedPaths.cwd)
|
|
||||||
const tsConfig = await loadConfig(config.resolvedPaths.cwd)
|
|
||||||
const updatedFiles = []
|
|
||||||
|
|
||||||
if (!projectInfo || tsConfig.resultType === "failed") {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filepath of filePaths) {
|
|
||||||
const resolvedPath = path.resolve(config.resolvedPaths.cwd, filepath)
|
|
||||||
|
|
||||||
// Check if the file exists.
|
|
||||||
if (!existsSync(resolvedPath)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(resolvedPath, "utf-8")
|
|
||||||
|
|
||||||
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
|
|
||||||
const sourceFile = project.createSourceFile(
|
|
||||||
path.join(dir, basename(resolvedPath)),
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
scriptKind: ScriptKind.TSX,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const importDeclarations = sourceFile.getImportDeclarations()
|
|
||||||
for (const importDeclaration of importDeclarations) {
|
|
||||||
const moduleSpecifier = importDeclaration.getModuleSpecifierValue()
|
|
||||||
|
|
||||||
// Filter out non-local imports.
|
|
||||||
if (
|
|
||||||
projectInfo?.aliasPrefix &&
|
|
||||||
!moduleSpecifier.startsWith(`${projectInfo.aliasPrefix}/`)
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the probable import file path.
|
|
||||||
// This is where we expect to find the file on disk.
|
|
||||||
const probableImportFilePath = await resolveImport(
|
|
||||||
moduleSpecifier,
|
|
||||||
tsConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!probableImportFilePath) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the actual import file path.
|
|
||||||
// This is the path where the file has been installed.
|
|
||||||
const resolvedImportFilePath = resolveModuleByProbablePath(
|
|
||||||
probableImportFilePath,
|
|
||||||
filePaths,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!resolvedImportFilePath) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the resolved import file path to an aliased import.
|
|
||||||
const newImport = toAliasedImport(
|
|
||||||
resolvedImportFilePath,
|
|
||||||
config,
|
|
||||||
projectInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!newImport || newImport === moduleSpecifier) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
importDeclaration.setModuleSpecifier(newImport)
|
|
||||||
|
|
||||||
// Write the updated content to the file.
|
|
||||||
await fs.writeFile(resolvedPath, sourceFile.getFullText(), "utf-8")
|
|
||||||
|
|
||||||
// Track the updated file.
|
|
||||||
updatedFiles.push(filepath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an absolute "probable" import path (no ext),
|
|
||||||
* plus an array of absolute file paths you already know about,
|
|
||||||
* return 0–N matches (best match first), and also check disk for any missing ones.
|
|
||||||
*/
|
|
||||||
export function resolveModuleByProbablePath(
|
|
||||||
probableImportFilePath: string,
|
|
||||||
files: string[],
|
|
||||||
config: Config,
|
|
||||||
extensions: string[] = [".tsx", ".ts", ".js", ".jsx", ".css"]
|
|
||||||
) {
|
|
||||||
const cwd = path.normalize(config.resolvedPaths.cwd)
|
|
||||||
|
|
||||||
// 1) Build a set of POSIX-normalized, project-relative files
|
|
||||||
const relativeFiles = files.map((f) => f.split(path.sep).join(path.posix.sep))
|
|
||||||
const fileSet = new Set(relativeFiles)
|
|
||||||
|
|
||||||
// 2) Strip any existing extension off the absolute base path
|
|
||||||
const extInPath = path.extname(probableImportFilePath)
|
|
||||||
const hasExt = extInPath !== ""
|
|
||||||
const absBase = hasExt
|
|
||||||
? probableImportFilePath.slice(0, -extInPath.length)
|
|
||||||
: probableImportFilePath
|
|
||||||
|
|
||||||
// 3) Compute the project-relative "base" directory for strong matching
|
|
||||||
const relBaseRaw = path.relative(cwd, absBase)
|
|
||||||
const relBase = relBaseRaw.split(path.sep).join(path.posix.sep)
|
|
||||||
|
|
||||||
// 4) Decide which extensions to try
|
|
||||||
const tryExts = hasExt ? [extInPath] : extensions
|
|
||||||
|
|
||||||
// 5) Collect candidates
|
|
||||||
const candidates = new Set<string>()
|
|
||||||
|
|
||||||
// 5a) Fast‑path: [base + ext] and [base/index + ext]
|
|
||||||
for (const e of tryExts) {
|
|
||||||
const absCand = absBase + e
|
|
||||||
const relCand = path.posix.normalize(path.relative(cwd, absCand))
|
|
||||||
if (fileSet.has(relCand) || existsSync(absCand)) {
|
|
||||||
candidates.add(relCand)
|
|
||||||
}
|
|
||||||
|
|
||||||
const absIdx = path.join(absBase, `index${e}`)
|
|
||||||
const relIdx = path.posix.normalize(path.relative(cwd, absIdx))
|
|
||||||
if (fileSet.has(relIdx) || existsSync(absIdx)) {
|
|
||||||
candidates.add(relIdx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5b) Fallback: scan known files by basename
|
|
||||||
const name = path.basename(absBase)
|
|
||||||
for (const f of relativeFiles) {
|
|
||||||
if (tryExts.some((e) => f.endsWith(`/${name}${e}`))) {
|
|
||||||
candidates.add(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6) If no matches, bail
|
|
||||||
if (candidates.size === 0) return null
|
|
||||||
|
|
||||||
// 7) Sort by (1) extension priority, then (2) "strong" base match
|
|
||||||
const sorted = Array.from(candidates).sort((a, b) => {
|
|
||||||
// a) extension order
|
|
||||||
const aExt = path.posix.extname(a)
|
|
||||||
const bExt = path.posix.extname(b)
|
|
||||||
const ord = tryExts.indexOf(aExt) - tryExts.indexOf(bExt)
|
|
||||||
if (ord !== 0) return ord
|
|
||||||
// b) strong match if path starts with relBase
|
|
||||||
const aStrong = relBase && a.startsWith(relBase) ? -1 : 1
|
|
||||||
const bStrong = relBase && b.startsWith(relBase) ? -1 : 1
|
|
||||||
return aStrong - bStrong
|
|
||||||
})
|
|
||||||
|
|
||||||
// 8) Return the first (best) candidate
|
|
||||||
return sorted[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toAliasedImport(
|
|
||||||
filePath: string,
|
|
||||||
config: Config,
|
|
||||||
projectInfo: ProjectInfo
|
|
||||||
): string | null {
|
|
||||||
const abs = path.normalize(path.join(config.resolvedPaths.cwd, filePath))
|
|
||||||
|
|
||||||
// 1️⃣ Find the longest matching alias root in resolvedPaths
|
|
||||||
// e.g. key="ui", root="/…/components/ui" beats key="components"
|
|
||||||
const matches = Object.entries(config.resolvedPaths)
|
|
||||||
.filter(
|
|
||||||
([, root]) => root && abs.startsWith(path.normalize(root + path.sep))
|
|
||||||
)
|
|
||||||
.sort((a, b) => b[1].length - a[1].length)
|
|
||||||
|
|
||||||
if (matches.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const [aliasKey, rootDir] = matches[0]
|
|
||||||
|
|
||||||
// 2️⃣ Compute the path UNDER that root
|
|
||||||
let rel = path.relative(rootDir, abs)
|
|
||||||
// force POSIX-style separators
|
|
||||||
rel = rel.split(path.sep).join("/") // e.g. "button/index.tsx"
|
|
||||||
|
|
||||||
// 3️⃣ Strip code-file extensions, keep others (css, json, etc.)
|
|
||||||
const ext = path.posix.extname(rel)
|
|
||||||
const codeExts = [".ts", ".tsx", ".js", ".jsx"]
|
|
||||||
const keepExt = codeExts.includes(ext) ? "" : ext
|
|
||||||
let noExt = rel.slice(0, rel.length - ext.length)
|
|
||||||
|
|
||||||
// 4️⃣ Collapse "/index" to its directory
|
|
||||||
if (noExt.endsWith("/index")) {
|
|
||||||
noExt = noExt.slice(0, -"/index".length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5️⃣ Build the aliased path
|
|
||||||
// config.aliases[aliasKey] is e.g. "@/components/ui"
|
|
||||||
const aliasBase =
|
|
||||||
aliasKey === "cwd"
|
|
||||||
? projectInfo.aliasPrefix
|
|
||||||
: config.aliases[aliasKey as keyof typeof config.aliases]
|
|
||||||
if (!aliasBase) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// if noExt is empty (i.e. file was exactly at the root), we import the root
|
|
||||||
let suffix = noExt === "" ? "" : `/${noExt}`
|
|
||||||
|
|
||||||
// Rremove /src from suffix.
|
|
||||||
// Alias will handle this.
|
|
||||||
suffix = suffix.replace("/src", "")
|
|
||||||
|
|
||||||
// 6️⃣ Prepend the prefix from projectInfo (e.g. "@") if needed
|
|
||||||
// but usually config.aliases already include it.
|
|
||||||
return `${aliasBase}${suffix}${keepExt}`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
0
packages/shadcn/test/fixtures/project-deno/deno.lock
generated
vendored
0
packages/shadcn/test/fixtures/project-deno/deno.lock
generated
vendored
13
packages/shadcn/test/fixtures/project-npm-react19/package-lock.json
generated
vendored
13
packages/shadcn/test/fixtures/project-npm-react19/package-lock.json
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "test-cli-npm-project",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "npm-project",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "test-cli-project-npm",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"author": "shadcn",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"react": "19.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { vi, describe, afterEach, test, expect } from "vitest"
|
|
||||||
import { execa } from "execa"
|
|
||||||
import prompts from "prompts"
|
|
||||||
import { updateDependencies } from "../../../src/utils/updaters/update-dependencies"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
vi.mock("execa")
|
|
||||||
vi.mock("prompts")
|
|
||||||
|
|
||||||
describe("updateDependencies", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.each([
|
|
||||||
{
|
|
||||||
description: "npm without react 19 includes no additional flags",
|
|
||||||
options: { silent: true },
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-npm")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "npm",
|
|
||||||
expectedArgs: ["install", "first", "second", "third"],
|
|
||||||
expectedDevArgs: ["install", "-D", "fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "npm with react 19 applies force prompt when silent",
|
|
||||||
options: { silent: true },
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-npm-react19")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "npm",
|
|
||||||
expectedArgs: ["install", "--force", "first", "second", "third"],
|
|
||||||
expectedDevArgs: ["install", "--force", "-D", "fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "npm with react 19 prompts for flag when not silent",
|
|
||||||
flagPrompt: "legacy-peer-deps",
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-npm-react19")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "npm",
|
|
||||||
expectedArgs: ["install", "--legacy-peer-deps", "first", "second", "third"],
|
|
||||||
expectedDevArgs: ["install", "--legacy-peer-deps", "-D", "fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "deno uses npm: package prefix",
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-deno")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "deno",
|
|
||||||
expectedArgs: ["add", "npm:first", "npm:second", "npm:third"],
|
|
||||||
expectedDevArgs: ["add", "-D", "npm:fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "bun uses bun",
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-bun")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "bun",
|
|
||||||
expectedArgs: ["add", "first", "second", "third"],
|
|
||||||
expectedDevArgs: ["add", "-D", "fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "pnpm uses pnpm",
|
|
||||||
dependencies: ["first", "second", "third"],
|
|
||||||
devDependencies: ["fourth"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-pnpm")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "pnpm",
|
|
||||||
expectedArgs: ["add", "first", "second", "third"],
|
|
||||||
expectedDevArgs: ["add", "-D", "fourth"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "deduplicates input dependencies",
|
|
||||||
options: { silent: true },
|
|
||||||
dependencies: ["first", "first"],
|
|
||||||
devDependencies: ["second", "second"],
|
|
||||||
config: {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: path.resolve(__dirname, "../../fixtures/project-npm")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedPackageManager: "npm",
|
|
||||||
expectedArgs: ["install", "first"],
|
|
||||||
expectedDevArgs: ["install", "-D", "second"]
|
|
||||||
}
|
|
||||||
])("$description", async ({ options, flagPrompt, config, dependencies, devDependencies, expectedPackageManager, expectedArgs, expectedDevArgs }) => {
|
|
||||||
|
|
||||||
vi.mocked(prompts).mockResolvedValue({ flag: flagPrompt })
|
|
||||||
|
|
||||||
await updateDependencies(
|
|
||||||
dependencies,
|
|
||||||
devDependencies,
|
|
||||||
config,
|
|
||||||
options ?? {}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (flagPrompt) {
|
|
||||||
expect(prompts).toHaveBeenCalled()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
expect(execa).toHaveBeenCalledWith(
|
|
||||||
expectedPackageManager,
|
|
||||||
expectedArgs,
|
|
||||||
{ cwd: config?.resolvedPaths.cwd }
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(execa).toHaveBeenCalledWith(
|
|
||||||
expectedPackageManager,
|
|
||||||
expectedDevArgs,
|
|
||||||
{ cwd: config?.resolvedPaths.cwd }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,23 +1,35 @@
|
|||||||
import { existsSync } from "fs"
|
import { existsSync, promises as fs } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { afterAll, afterEach, describe, expect, test, vi } from "vitest"
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
test,
|
||||||
|
vi,
|
||||||
|
} from "vitest"
|
||||||
|
|
||||||
import { getConfig } from "../../../src/utils/get-config"
|
import { getConfig } from "../../../src/utils/get-config"
|
||||||
|
import * as transformers from "../../../src/utils/transformers"
|
||||||
import {
|
import {
|
||||||
findCommonRoot,
|
findCommonRoot,
|
||||||
resolveFilePath,
|
resolveFilePath,
|
||||||
resolveModuleByProbablePath,
|
|
||||||
resolveNestedFilePath,
|
resolveNestedFilePath,
|
||||||
toAliasedImport,
|
|
||||||
updateFiles,
|
updateFiles,
|
||||||
} from "../../../src/utils/updaters/update-files"
|
} from "../../../src/utils/updaters/update-files"
|
||||||
|
|
||||||
|
vi.mock("../../../src/utils/transformers", () => ({
|
||||||
|
transform: vi.fn().mockImplementation((opts) => Promise.resolve(opts.raw)),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock("fs/promises", async () => {
|
vi.mock("fs/promises", async () => {
|
||||||
const actual = (await vi.importActual(
|
const actual = (await vi.importActual(
|
||||||
"fs/promises"
|
"fs/promises"
|
||||||
)) as typeof import("fs/promises")
|
)) as typeof import("fs/promises")
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
readFile: vi.fn(),
|
||||||
writeFile: vi.fn(),
|
writeFile: vi.fn(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -26,8 +38,10 @@ vi.mock("fs", async () => {
|
|||||||
const actual = (await vi.importActual("fs")) as typeof import("fs")
|
const actual = (await vi.importActual("fs")) as typeof import("fs")
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
existsSync: vi.fn(),
|
||||||
promises: {
|
promises: {
|
||||||
...actual.promises,
|
...actual.promises,
|
||||||
|
readFile: vi.fn(),
|
||||||
writeFile: vi.fn(),
|
writeFile: vi.fn(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -734,6 +748,30 @@ describe("updateFiles", () => {
|
|||||||
const config = await getConfig(
|
const config = await getConfig(
|
||||||
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set up mocks for transform
|
||||||
|
vi.mocked(transformers.transform).mockImplementation((opts) => {
|
||||||
|
if (opts.raw.includes("Button")) {
|
||||||
|
return Promise.resolve(opts.raw)
|
||||||
|
}
|
||||||
|
return Promise.resolve(opts.raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock existsSync to check for existing files
|
||||||
|
vi.mocked(existsSync).mockImplementation((path) => {
|
||||||
|
return path.toString().includes("button.tsx")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock readFile to return content for comparison
|
||||||
|
vi.mocked(fs.readFile).mockImplementation((path) => {
|
||||||
|
if (path.toString().includes("button.tsx")) {
|
||||||
|
return Promise.resolve(`export function Button() {
|
||||||
|
return <button>Click me</button>
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
return Promise.resolve("")
|
||||||
|
})
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await updateFiles(
|
await updateFiles(
|
||||||
[
|
[
|
||||||
@@ -775,6 +813,27 @@ return <div>Hello World</div>
|
|||||||
const config = await getConfig(
|
const config = await getConfig(
|
||||||
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set up mocks for transform
|
||||||
|
vi.mocked(transformers.transform).mockImplementation((opts) => {
|
||||||
|
return Promise.resolve(opts.raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock existsSync to check for existing files
|
||||||
|
vi.mocked(existsSync).mockImplementation((path) => {
|
||||||
|
return path.toString().includes("button.tsx")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock readFile to return content for comparison
|
||||||
|
vi.mocked(fs.readFile).mockImplementation((path) => {
|
||||||
|
if (path.toString().includes("button.tsx")) {
|
||||||
|
return Promise.resolve(`export function Button() {
|
||||||
|
return <button>I'm different</button>
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
return Promise.resolve("")
|
||||||
|
})
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await updateFiles(
|
await updateFiles(
|
||||||
[
|
[
|
||||||
@@ -813,339 +872,247 @@ return <div>Hello World</div>
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("resolveModuleByProbablePath", () => {
|
describe("file actions", () => {
|
||||||
test("should resolve exact file match in provided files list", () => {
|
test("should append content to existing file", async () => {
|
||||||
const files = [
|
// Set up mocks
|
||||||
"components/button.tsx",
|
vi.mocked(transformers.transform).mockResolvedValue("new-content")
|
||||||
"components/card.tsx",
|
vi.mocked(existsSync)
|
||||||
"lib/utils.ts",
|
.mockReturnValueOnce(true) // First check for file existence
|
||||||
]
|
.mockReturnValueOnce(true) // Directory exists check
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
const existingContent = "existing-content"
|
||||||
cwd: "/foo/bar",
|
vi.mocked(fs.readFile)
|
||||||
},
|
.mockResolvedValueOnce(existingContent) // For content comparison check
|
||||||
}
|
.mockResolvedValueOnce(existingContent) // For append operation
|
||||||
expect(
|
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
|
const config = await getConfig(
|
||||||
).toBe("components/button.tsx")
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await updateFiles(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "components/test.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
content: "original-content", // This will be transformed to "new-content"
|
||||||
|
action: "append",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
overwrite: true,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the file was updated not created
|
||||||
|
expect(result.filesUpdated).toHaveLength(1)
|
||||||
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
|
expect(result.filesSkipped).toHaveLength(0)
|
||||||
|
|
||||||
|
// Check writeFile was called correctly for append
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
`${existingContent}\nnew-content`,
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should resolve index file", () => {
|
test("should prepend content to existing file", async () => {
|
||||||
const files = ["components/button/index.tsx", "components/card.tsx"]
|
// Set up mocks
|
||||||
const config = {
|
vi.mocked(transformers.transform).mockResolvedValue("new-content")
|
||||||
resolvedPaths: {
|
vi.mocked(existsSync)
|
||||||
cwd: "/foo/bar",
|
.mockReturnValueOnce(true) // First check for file existence
|
||||||
},
|
.mockReturnValueOnce(true) // Directory exists check
|
||||||
}
|
|
||||||
expect(
|
const existingContent = "existing-content"
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
|
vi.mocked(fs.readFile)
|
||||||
).toBe("components/button/index.tsx")
|
.mockResolvedValueOnce(existingContent) // For content comparison check
|
||||||
|
.mockResolvedValueOnce(existingContent) // For prepend operation
|
||||||
|
|
||||||
|
const config = await getConfig(
|
||||||
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await updateFiles(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "components/test.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
content: "original-content", // This will be transformed to "new-content"
|
||||||
|
action: "prepend",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
overwrite: true,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the file was updated not created
|
||||||
|
expect(result.filesUpdated).toHaveLength(1)
|
||||||
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
|
expect(result.filesSkipped).toHaveLength(0)
|
||||||
|
|
||||||
|
// Check writeFile was called correctly for prepend
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
`new-content\n${existingContent}`,
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should try different extensions", () => {
|
test("should update file when content is different", async () => {
|
||||||
const files = ["components/button.jsx", "components/card.tsx"]
|
// Set up mocks
|
||||||
const config = {
|
vi.mocked(transformers.transform).mockResolvedValue("new-content")
|
||||||
resolvedPaths: {
|
vi.mocked(existsSync)
|
||||||
cwd: "/foo/bar",
|
.mockReturnValueOnce(true) // First check for file existence
|
||||||
},
|
.mockReturnValueOnce(true) // Directory exists check
|
||||||
}
|
|
||||||
expect(
|
const existingContent = "existing-content"
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
|
vi.mocked(fs.readFile).mockResolvedValueOnce(existingContent) // For content comparison
|
||||||
).toBe("components/button.jsx")
|
|
||||||
|
const config = await getConfig(
|
||||||
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await updateFiles(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "components/test.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
content: "original-content", // This will be transformed to "new-content"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
overwrite: true,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the file was updated not created
|
||||||
|
expect(result.filesUpdated).toHaveLength(1)
|
||||||
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
|
expect(result.filesSkipped).toHaveLength(0)
|
||||||
|
|
||||||
|
// Check writeFile was called with new content
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
"new-content",
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should fallback to basename matching", () => {
|
test("should skip file when content is the same", async () => {
|
||||||
const files = ["components/ui/button.tsx", "components/card.tsx"]
|
// Set up mocks
|
||||||
const config = {
|
const content = "same-content"
|
||||||
resolvedPaths: {
|
vi.mocked(transformers.transform).mockResolvedValue(content)
|
||||||
cwd: "/foo/bar",
|
vi.mocked(existsSync).mockReturnValue(true)
|
||||||
},
|
vi.mocked(fs.readFile).mockResolvedValue(content)
|
||||||
}
|
|
||||||
expect(
|
const config = await getConfig(
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
).toBe("components/ui/button.tsx")
|
)
|
||||||
|
|
||||||
|
const result = await updateFiles(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "components/test.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
overwrite: true,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the file was skipped
|
||||||
|
expect(result.filesSkipped).toHaveLength(1)
|
||||||
|
expect(result.filesUpdated).toHaveLength(0)
|
||||||
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
|
|
||||||
|
// Verify writeFile was not called
|
||||||
|
expect(fs.writeFile).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should return null when file not found", () => {
|
test("should skip file when content is already appended", async () => {
|
||||||
const files = ["components/card.tsx", "lib/utils.ts"]
|
// Set up mocks
|
||||||
const config = {
|
const newContent = "new-content"
|
||||||
resolvedPaths: {
|
const existingContent = "existing-content\nnew-content" // Already has the content appended
|
||||||
cwd: "/foo/bar",
|
|
||||||
},
|
vi.mocked(transformers.transform).mockResolvedValue(newContent)
|
||||||
}
|
vi.mocked(existsSync).mockReturnValue(true)
|
||||||
expect(
|
vi.mocked(fs.readFile).mockResolvedValue(existingContent)
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
|
|
||||||
).toBeNull()
|
const config = await getConfig(
|
||||||
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await updateFiles(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "components/test.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
content: newContent,
|
||||||
|
action: "append",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
overwrite: true,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// The file should be skipped because the content is already appended
|
||||||
|
expect(result.filesSkipped).toHaveLength(1)
|
||||||
|
expect(result.filesUpdated).toHaveLength(0)
|
||||||
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
|
|
||||||
|
// Verify writeFile was not called
|
||||||
|
expect(fs.writeFile).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should sort by extension priority", () => {
|
test("should skip file when content is already prepended", async () => {
|
||||||
const files = [
|
// Set up mocks
|
||||||
"components/button.jsx",
|
const newContent = "new-content"
|
||||||
"components/button.tsx",
|
const existingContent = "new-content\nexisting-content" // Already has the content prepended
|
||||||
"components/button.js",
|
|
||||||
]
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(
|
|
||||||
resolveModuleByProbablePath("/foo/bar/components/button", files, config, [
|
|
||||||
".tsx",
|
|
||||||
".jsx",
|
|
||||||
".js",
|
|
||||||
])
|
|
||||||
).toBe("components/button.tsx")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should preserve extension if specified in path", () => {
|
vi.mocked(transformers.transform).mockResolvedValue(newContent)
|
||||||
const files = ["components/button.tsx", "components/button.css"]
|
vi.mocked(existsSync).mockReturnValue(true)
|
||||||
const config = {
|
vi.mocked(fs.readFile).mockResolvedValue(existingContent)
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
const config = await getConfig(
|
||||||
},
|
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
|
||||||
}
|
)
|
||||||
expect(
|
|
||||||
resolveModuleByProbablePath(
|
const result = await updateFiles(
|
||||||
"/foo/bar/components/button.css",
|
[
|
||||||
files,
|
{
|
||||||
config
|
path: "components/test.tsx",
|
||||||
)
|
type: "registry:component",
|
||||||
).toBe("components/button.css")
|
content: newContent,
|
||||||
})
|
action: "prepend",
|
||||||
})
|
},
|
||||||
|
],
|
||||||
describe("toAliasedImport", () => {
|
config,
|
||||||
test("should convert components path to aliased import", () => {
|
{
|
||||||
const filePath = "components/button.tsx"
|
overwrite: true,
|
||||||
const config = {
|
silent: true,
|
||||||
resolvedPaths: {
|
}
|
||||||
cwd: "/foo/bar",
|
)
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
// The file should be skipped because the content is already prepended
|
||||||
lib: "/foo/bar/lib",
|
expect(result.filesSkipped).toHaveLength(1)
|
||||||
},
|
expect(result.filesUpdated).toHaveLength(0)
|
||||||
aliases: {
|
expect(result.filesCreated).toHaveLength(0)
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
// Verify writeFile was not called
|
||||||
lib: "@/lib",
|
expect(fs.writeFile).not.toHaveBeenCalled()
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@/components/button"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should convert ui path to aliased import", () => {
|
|
||||||
const filePath = "components/ui/button.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@/components/ui/button"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should collapse index files", () => {
|
|
||||||
const filePath = "components/ui/button/index.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@/components/ui/button"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should return null when no matching alias found", () => {
|
|
||||||
const filePath = "src/pages/index.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle nested directories", () => {
|
|
||||||
const filePath = "components/forms/inputs/text-input.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@/components/forms/inputs/text-input"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should keep non-code file extensions", () => {
|
|
||||||
const filePath = "components/styles/theme.css"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@/components/styles/theme.css"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should prefer longer matching paths", () => {
|
|
||||||
const filePath = "components/ui/button.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/ui",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/ui/button")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should support tilde (~) alias prefix", () => {
|
|
||||||
const filePath = "components/button.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "~components",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "~",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"~components/button"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should support @shadcn alias prefix", () => {
|
|
||||||
const filePath = "components/ui/button.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@shadcn/components",
|
|
||||||
ui: "@shadcn/ui",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@shadcn",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
|
|
||||||
"@shadcn/ui/button"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should support ~cn alias prefix", () => {
|
|
||||||
const filePath = "lib/utils/index.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
lib: "~cn/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "~cn",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe("~cn/lib/utils")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should use project alias prefix when aliasKey is cwd", () => {
|
|
||||||
const filePath = "src/pages/home.tsx"
|
|
||||||
const config = {
|
|
||||||
resolvedPaths: {
|
|
||||||
cwd: "/foo/bar",
|
|
||||||
components: "/foo/bar/components",
|
|
||||||
ui: "/foo/bar/components/ui",
|
|
||||||
lib: "/foo/bar/lib",
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
components: "@/components",
|
|
||||||
ui: "@/components/ui",
|
|
||||||
lib: "@/lib",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const projectInfo = {
|
|
||||||
aliasPrefix: "@",
|
|
||||||
}
|
|
||||||
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages/home")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -283,7 +283,7 @@ importers:
|
|||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 2.5.0
|
specifier: 2.4.1
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
@@ -542,7 +542,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.5.0
|
specifier: 2.4.1
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.32.6
|
specifier: ^0.32.6
|
||||||
|
|||||||
Reference in New Issue
Block a user