Compare commits

...

6 Commits

Author SHA1 Message Date
shadcn
4ef12b0e09 fix
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-02 09:32:24 +04:00
shadcn
801e7d6b3d style 2026-03-02 09:28:49 +04:00
shadcn
5fad0832c6 Merge branch 'main' into shadcn/fix-sidebar-demo 2026-03-02 09:20:47 +04:00
shadcn
1c73a831ae fix 2026-03-02 09:20:18 +04:00
shadcn
2b62c90dae Merge branch 'main' into gokhan/add-subpath-imports 2026-03-01 11:33:06 +04:00
Gokhan Kurt
c753e32471 feat: add registry support for subpath imports 2026-02-28 16:47:27 +03:00
16 changed files with 498 additions and 175 deletions

View File

@@ -157,7 +157,7 @@ Here are some guidelines to follow when building components for a registry.
- It is recommended to add a proper name and description to your registry item. This helps LLMs understand the component and its purpose. - It is recommended to add a proper name and description to your registry item. This helps LLMs understand the component and its purpose.
- Make sure to list all registry dependencies in `registryDependencies`. A registry dependency is the name of the component in the registry eg. `input`, `button`, `card`, etc or a URL to a registry item eg. `http://localhost:3000/r/editor.json`. - Make sure to list all registry dependencies in `registryDependencies`. A registry dependency is the name of the component in the registry eg. `input`, `button`, `card`, etc or a URL to a registry item eg. `http://localhost:3000/r/editor.json`.
- Make sure to list all dependencies in `dependencies`. A dependency is the name of the package in the registry eg. `zod`, `sonner`, etc. To set a version, you can use the `name@version` format eg. `zod@^3.20.0`. - Make sure to list all dependencies in `dependencies`. A dependency is the name of the package in the registry eg. `zod`, `sonner`, etc. To set a version, you can use the `name@version` format eg. `zod@^3.20.0`.
- **Imports should always use the `@/registry` path.** eg. `import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"` - **Imports should always use the `@/registry` or `#registry` path.** eg. `import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"` or `import { HelloWorld } from "#registry/new-york/hello-world/hello-world.ts"`
- Ideally, place your files within a registry item in `components`, `hooks`, `lib` directories. - Ideally, place your files within a registry item in `components`, `hooks`, `lib` directories.
## Install using the CLI ## Install using the CLI

View File

@@ -209,67 +209,65 @@ function TeamSwitcher({
} }
return ( return (
<SidebarGroup> <SidebarMenu>
<SidebarMenu> <SidebarMenuItem>
<SidebarMenuItem> <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger
<DropdownMenuTrigger render={
render={ <SidebarMenuButton
<SidebarMenuButton size="lg"
size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" />
/> }
} >
> <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"> <activeTeam.logo className="size-4" />
<activeTeam.logo className="size-4" /> </div>
</div> <div className="grid flex-1 text-left text-sm leading-tight">
<div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate font-medium">{activeTeam.name}</span> <span className="truncate text-xs">{activeTeam.plan}</span>
<span className="truncate text-xs">{activeTeam.plan}</span> </div>
</div> <ChevronsUpDown className="ml-auto" />
<ChevronsUpDown className="ml-auto" /> </DropdownMenuTrigger>
</DropdownMenuTrigger> <DropdownMenuContent
<DropdownMenuContent className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" align="start"
align="start" side={isMobile ? "bottom" : "right"}
side={isMobile ? "bottom" : "right"} sideOffset={4}
sideOffset={4} >
> <DropdownMenuGroup>
<DropdownMenuGroup> <DropdownMenuLabel className="text-muted-foreground text-xs">
<DropdownMenuLabel className="text-muted-foreground text-xs"> Teams
Teams </DropdownMenuLabel>
</DropdownMenuLabel> {teams.map((team, index) => (
{teams.map((team, index) => ( <DropdownMenuItem
<DropdownMenuItem key={team.name}
key={team.name} onClick={() => setActiveTeam(team)}
onClick={() => setActiveTeam(team)} className="gap-2 p-2"
className="gap-2 p-2" >
> <div className="flex size-6 items-center justify-center rounded-md border">
<div className="flex size-6 items-center justify-center rounded-md border"> <team.logo className="size-3.5 shrink-0" />
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">
Add team
</div> </div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> ))}
</DropdownMenuContent> </DropdownMenuGroup>
</DropdownMenu> <DropdownMenuSeparator />
</SidebarMenuItem> <DropdownMenuGroup>
</SidebarMenu> <DropdownMenuItem className="gap-2 p-2">
</SidebarGroup> <div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
) )
} }
@@ -395,82 +393,80 @@ function NavUser({
const { isMobile } = useSidebar() const { isMobile } = useSidebar()
return ( return (
<SidebarGroup> <SidebarMenu>
<SidebarMenu> <SidebarMenuItem>
<SidebarMenuItem> <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger
<DropdownMenuTrigger render={
render={ <SidebarMenuButton
<SidebarMenuButton size="lg"
size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" />
/> }
} >
> <Avatar className="h-8 w-8 rounded-lg">
<Avatar className="h-8 w-8 rounded-lg"> <AvatarImage src={user.avatar} alt={user.name} />
<AvatarImage src={user.avatar} alt={user.name} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar>
</Avatar> <div className="grid flex-1 text-left text-sm leading-tight">
<div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium">{user.name}</span>
<span className="truncate font-medium">{user.name}</span> <span className="truncate text-xs">{user.email}</span>
<span className="truncate text-xs">{user.email}</span> </div>
</div> <ChevronsUpDown className="ml-auto size-4" />
<ChevronsUpDown className="ml-auto size-4" /> </DropdownMenuTrigger>
</DropdownMenuTrigger> <DropdownMenuContent
<DropdownMenuContent className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" side={isMobile ? "bottom" : "right"}
side={isMobile ? "bottom" : "right"} align="end"
align="end" sideOffset={4}
sideOffset={4} >
> <DropdownMenuGroup>
<DropdownMenuGroup> <DropdownMenuLabel className="p-0 font-normal">
<DropdownMenuLabel className="p-0 font-normal"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar className="h-8 w-8 rounded-lg">
<Avatar className="h-8 w-8 rounded-lg"> <AvatarImage src={user.avatar} alt={user.name} />
<AvatarImage src={user.avatar} alt={user.name} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar>
</Avatar> <div className="grid flex-1 text-left text-sm leading-tight">
<div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-medium">{user.name}</span>
<span className="truncate font-medium">{user.name}</span> <span className="truncate text-xs">{user.email}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div> </div>
</DropdownMenuLabel> </div>
</DropdownMenuGroup> </DropdownMenuLabel>
<DropdownMenuSeparator /> </DropdownMenuGroup>
<DropdownMenuGroup> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuGroup>
<Sparkles /> <DropdownMenuItem>
Upgrade to Pro <Sparkles />
</DropdownMenuItem> Upgrade to Pro
</DropdownMenuGroup> </DropdownMenuItem>
<DropdownMenuSeparator /> </DropdownMenuGroup>
<DropdownMenuGroup> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuGroup>
<BadgeCheck /> <DropdownMenuItem>
Account <BadgeCheck />
</DropdownMenuItem> Account
<DropdownMenuItem> </DropdownMenuItem>
<CreditCard /> <DropdownMenuItem>
Billing <CreditCard />
</DropdownMenuItem> Billing
<DropdownMenuItem> </DropdownMenuItem>
<Bell /> <DropdownMenuItem>
Notifications <Bell />
</DropdownMenuItem> Notifications
</DropdownMenuGroup> </DropdownMenuItem>
<DropdownMenuSeparator /> </DropdownMenuGroup>
<DropdownMenuGroup> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuGroup>
<LogOut /> <DropdownMenuItem>
Log out <LogOut />
</DropdownMenuItem> Log out
</DropdownMenuGroup> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuGroup>
</DropdownMenu> </DropdownMenuContent>
</SidebarMenuItem> </DropdownMenu>
</SidebarMenu> </SidebarMenuItem>
</SidebarGroup> </SidebarMenu>
) )
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -326,36 +326,36 @@ export default function SidebarIconExample() {
key={item.title} key={item.title}
defaultOpen={item.isActive} defaultOpen={item.isActive}
className="group/collapsible" className="group/collapsible"
render={<SidebarMenuItem />}
> >
<SidebarMenuButton <SidebarMenuItem>
tooltip={item.title} <CollapsibleTrigger
render={<CollapsibleTrigger />} render={<SidebarMenuButton tooltip={item.title} />}
> >
{item.icon} {item.icon}
<span>{item.title}</span> <span>{item.title}</span>
<IconPlaceholder <IconPlaceholder
lucide="ChevronRightIcon" lucide="ChevronRightIcon"
tabler="IconChevronRight" tabler="IconChevronRight"
hugeicons="ArrowRight01Icon" hugeicons="ArrowRight01Icon"
phosphor="CaretRightIcon" phosphor="CaretRightIcon"
remixicon="RiArrowRightSLine" remixicon="RiArrowRightSLine"
className="ml-auto transition-transform duration-100 group-data-open/collapsible:rotate-90" className="ml-auto transition-transform duration-100 group-data-open/collapsible:rotate-90"
/> />
</SidebarMenuButton> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<SidebarMenuSub> <SidebarMenuSub>
{item.items?.map((subItem) => ( {item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}> <SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton <SidebarMenuSubButton
render={<a href={subItem.url} />} render={<a href={subItem.url} />}
> >
{subItem.title} {subItem.title}
</SidebarMenuSubButton> </SidebarMenuSubButton>
</SidebarMenuSubItem> </SidebarMenuSubItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
</CollapsibleContent> </CollapsibleContent>
</SidebarMenuItem>
</Collapsible> </Collapsible>
))} ))}
</SidebarMenu> </SidebarMenu>

View File

@@ -119,9 +119,9 @@ export async function recursivelyResolveFileImports(
const moduleSpecifier = importStatement.getModuleSpecifierValue() const moduleSpecifier = importStatement.getModuleSpecifierValue()
const isRelativeImport = moduleSpecifier.startsWith(".") const isRelativeImport = moduleSpecifier.startsWith(".")
const isAliasImport = moduleSpecifier.startsWith( const isAliasImport =
`${projectInfo.aliasPrefix}/` moduleSpecifier.startsWith(`${projectInfo.aliasPrefix}/`) ||
) moduleSpecifier.startsWith("#")
// If not a local import, add to the dependencies array. // If not a local import, add to the dependencies array.
if (!isAliasImport && !isRelativeImport) { if (!isAliasImport && !isRelativeImport) {

View File

@@ -1,9 +1,24 @@
import { readFileSync } from "fs"
import path from "path"
import { createMatchPath, type ConfigLoaderSuccessResult } from "tsconfig-paths" import { createMatchPath, type ConfigLoaderSuccessResult } from "tsconfig-paths"
export async function resolveImport( export async function resolveImport(
importPath: string, importPath: string,
config: Pick<ConfigLoaderSuccessResult, "absoluteBaseUrl" | "paths"> config: Pick<ConfigLoaderSuccessResult, "absoluteBaseUrl" | "paths">
) { ) {
// Handle subpath imports (#) by resolving from package.json imports field.
if (importPath.startsWith("#")) {
try {
// @ts-ignore
const resolved = import.meta.resolve(importPath, import.meta.url)
if (resolved) return resolved
} catch {
// If native resolution fails, fallback to manual resolution.
}
return resolveSubpathImport(importPath, config.absoluteBaseUrl)
}
return createMatchPath(config.absoluteBaseUrl, config.paths)( return createMatchPath(config.absoluteBaseUrl, config.paths)(
importPath, importPath,
undefined, undefined,
@@ -11,3 +26,80 @@ export async function resolveImport(
[".ts", ".tsx", ".jsx", ".js", ".css"] [".ts", ".tsx", ".jsx", ".js", ".css"]
) )
} }
function resolveSubpathImport(
importPath: string,
baseDir: string
): string | null {
let packageJson: { imports?: Record<string, unknown> }
try {
packageJson = JSON.parse(
readFileSync(path.resolve(baseDir, "package.json"), "utf-8")
)
} catch {
return null
}
const imports = packageJson.imports
if (!imports || typeof imports !== "object") {
return null
}
// Try exact match first.
if (importPath in imports) {
const resolved = resolveImportTarget(imports[importPath])
if (resolved) {
return path.resolve(baseDir, resolved)
}
return null
}
// Try wildcard patterns (most specific / longest pattern first).
const patterns = Object.keys(imports)
.filter((p) => p.includes("*"))
.sort((a, b) => b.length - a.length)
for (const pattern of patterns) {
const [prefix, suffix] = pattern.split("*")
if (
importPath.startsWith(prefix) &&
(suffix === "" || importPath.endsWith(suffix))
) {
const wildcard = suffix
? importPath.slice(prefix.length, -suffix.length)
: importPath.slice(prefix.length)
const target = resolveImportTarget(imports[pattern])
if (target) {
return path.resolve(baseDir, target.replace(/\*/g, wildcard))
}
}
}
return null
}
// Resolve a conditional import target to the first local path.
function resolveImportTarget(target: unknown): string | null {
if (typeof target === "string") {
return target.startsWith(".") ? target : null
}
if (Array.isArray(target)) {
for (const item of target) {
const resolved = resolveImportTarget(item)
if (resolved) return resolved
}
return null
}
if (target && typeof target === "object") {
// Iterate conditions in order, return the first local path.
for (const value of Object.values(target as Record<string, unknown>)) {
const resolved = resolveImportTarget(value)
if (resolved) return resolved
}
return null
}
return null
}

View File

@@ -0,0 +1,90 @@
import { describe, expect, test } from "vitest"
import { transform } from "."
import { createConfig } from "../get-config"
import { transformImport } from "./transform-import"
describe("transformImport", () => {
describe("subpath imports in source files", () => {
const testConfig = createConfig({
aliases: {
components: "@/components",
ui: "@/components/ui",
utils: "@/lib/utils",
lib: "@/lib",
hooks: "@/hooks",
},
})
test.each([
["#/registry/new-york/ui/button", "@/components/ui/button"],
["#registry/new-york/ui/button", "@/components/ui/button"],
["#/registry/new-york/hooks/use-hook", "@/hooks/use-hook"],
["#/registry/new-york/lib/helper", "@/lib/helper"],
["#/registry/new-york/components/card", "@/components/card"],
["#/lib/foo", "@/lib/foo"],
["#/lib/utils", "@/lib/utils"],
["#lib/utils", "@/lib/utils"],
["#lib/foo", "@/lib/foo"],
["#hooks/use-hook", "@/hooks/use-hook"],
["react", "react"],
])("%s → %s", async (input, expected) => {
const result = await transform(
{
filename: "test.tsx",
raw: `import x from "${input}"`,
config: testConfig,
},
[transformImport]
)
expect(result).toContain(`"${expected}"`)
})
})
describe("user config with # aliases", () => {
const hashConfig = createConfig({
aliases: {
components: "#/src/components",
ui: "#/src/components/ui",
utils: "#/src/utils",
lib: "#/src/lib",
hooks: "#/src/hooks",
},
})
test.each([
["@/registry/new-york/ui/button", "#/src/components/ui/button"],
["@/registry/new-york/hooks/use-hook", "#/src/hooks/use-hook"],
["@/registry/new-york/components/card", "#/src/components/card"],
["@/config/foo", "#/config/foo"],
["#/registry/new-york/ui/button", "#/src/components/ui/button"],
["#registry/new-york/ui/button", "#/src/components/ui/button"],
])("%s → %s", async (input, expected) => {
const result = await transform(
{
filename: "test.tsx",
raw: `import x from "${input}"`,
config: hashConfig,
},
[transformImport]
)
expect(result).toContain(`"${expected}"`)
})
// cn import requires named import to trigger utils alias rewrite.
test("@/lib/utils (cn) → #/src/utils", async () => {
const result = await transform(
{
filename: "test.tsx",
raw: `import { cn } from "@/lib/utils"`,
config: hashConfig,
},
[transformImport]
)
expect(result).toContain(`"#/src/utils"`)
})
})
})

View File

@@ -55,6 +55,13 @@ function updateImportAliases(
config: Config, config: Config,
isRemote: boolean = false isRemote: boolean = false
) { ) {
// Normalize subpath imports (#) to @/ prefix.
if (moduleSpecifier.startsWith("#/")) {
moduleSpecifier = moduleSpecifier.replace(/^#\//, "@/")
} else if (moduleSpecifier.startsWith("#")) {
moduleSpecifier = moduleSpecifier.replace(/^#/, "@/")
}
// Not a local import. // Not a local import.
if (!moduleSpecifier.startsWith("@/") && !isRemote) { if (!moduleSpecifier.startsWith("@/") && !isRemote) {
return moduleSpecifier return moduleSpecifier

View File

@@ -550,9 +550,11 @@ async function resolveImports(filePaths: string[], config: Config) {
const moduleSpecifier = importDeclaration.getModuleSpecifierValue() const moduleSpecifier = importDeclaration.getModuleSpecifierValue()
// Filter out non-local imports. // Filter out non-local imports.
// Also accept subpath imports (#) as local.
if ( if (
projectInfo?.aliasPrefix && projectInfo?.aliasPrefix &&
!moduleSpecifier.startsWith(`${projectInfo.aliasPrefix}/`) !moduleSpecifier.startsWith(`${projectInfo.aliasPrefix}/`) &&
!moduleSpecifier.startsWith("#")
) { ) {
continue continue
} }
@@ -707,9 +709,14 @@ export function toAliasedImport(
rel = rel.split(path.sep).join("/") // e.g. "button/index.tsx" rel = rel.split(path.sep).join("/") // e.g. "button/index.tsx"
// 3⃣ Strip code-file extensions, keep others (css, json, etc.) // 3⃣ Strip code-file extensions, keep others (css, json, etc.)
// Subpath imports (#) require explicit file extensions.
const ext = path.posix.extname(rel) const ext = path.posix.extname(rel)
const codeExts = [".ts", ".tsx", ".js", ".jsx"] const codeExts = [".ts", ".tsx", ".js", ".jsx"]
const keepExt = codeExts.includes(ext) ? "" : ext const isSubpathImport =
aliasKey === "cwd"
? projectInfo.aliasPrefix?.startsWith("#")
: config.aliases[aliasKey as keyof typeof config.aliases]?.startsWith("#")
const keepExt = codeExts.includes(ext) && !isSubpathImport ? "" : ext
let noExt = rel.slice(0, rel.length - ext.length) let noExt = rel.slice(0, rel.length - ext.length)
// 4⃣ Collapse "/index" to its directory // 4⃣ Collapse "/index" to its directory

View File

@@ -0,0 +1,13 @@
{
"name": "test-subpath-imports",
"type": "module",
"imports": {
"#src/*": "./src/*",
"#components/*": "./src/components/*",
"#hooks": "./src/hooks/index.ts",
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
}
}

View File

@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths" import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths"
import { expect, test } from "vitest" import { describe, expect, test } from "vitest"
import { resolveImport } from "../../src/utils/resolve-import" import { resolveImport } from "../../src/utils/resolve-import"
@@ -79,3 +79,41 @@ test("resolve import without base url", async () => {
path.resolve(cwd, "foo/bar") path.resolve(cwd, "foo/bar")
) )
}) })
describe("resolve subpath imports", () => {
const cwd = path.resolve(__dirname, "../fixtures/with-subpath-imports")
const config = {
absoluteBaseUrl: cwd,
paths: {},
}
test("should resolve wildcard subpath import", async () => {
expect(await resolveImport("#src/components/ui", config)).toEqual(
path.resolve(cwd, "src/components/ui")
)
})
test("should resolve more specific wildcard pattern", async () => {
expect(await resolveImport("#components/button", config)).toEqual(
path.resolve(cwd, "src/components/button")
)
})
test("should resolve exact match subpath import", async () => {
expect(await resolveImport("#hooks", config)).toEqual(
path.resolve(cwd, "src/hooks/index.ts")
)
})
test("should resolve conditional subpath import to first local path", async () => {
// #dep has { "node": "dep-node-native", "default": "./dep-polyfill.js" }
// Should skip "dep-node-native" (not local) and pick "./dep-polyfill.js"
expect(await resolveImport("#dep", config)).toEqual(
path.resolve(cwd, "dep-polyfill.js")
)
})
test("should return null for unmatched subpath import", async () => {
expect(await resolveImport("#nonexistent/foo", config)).toBeNull()
})
})

View File

@@ -1968,4 +1968,84 @@ describe("toAliasedImport", () => {
} }
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages/home") expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages/home")
}) })
test("should keep code extension for # subpath import alias", () => {
const filePath = "components/ui/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
},
aliases: {
components: "#/src/components",
ui: "#/src/components/ui",
},
}
const projectInfo = {
aliasPrefix: "#",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"#/src/components/ui/button.tsx"
)
})
test("should keep .ts extension for # subpath import alias", () => {
const filePath = "lib/utils.ts"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
lib: "/foo/bar/lib",
},
aliases: {
lib: "#/src/lib",
},
}
const projectInfo = {
aliasPrefix: "#",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"#/src/lib/utils.ts"
)
})
test("should still strip extension for @ alias (existing behavior)", () => {
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: "@/components/ui",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/ui/button"
)
})
test("should keep css extension for # alias (non-code ext always kept)", () => {
const filePath = "components/styles/theme.css"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
},
aliases: {
components: "#/src/components",
},
}
const projectInfo = {
aliasPrefix: "#",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"#/src/components/styles/theme.css"
)
})
}) })