From 829709751209dc1dc7909fedde596d29cd105ff0 Mon Sep 17 00:00:00 2001 From: shadcn Date: Mon, 16 Mar 2026 19:21:49 +0400 Subject: [PATCH] fix: refactor --- packages/shadcn/src/utils/get-config.ts | 108 +++++++----------- packages/shadcn/src/utils/resolve-import.ts | 18 +-- .../shadcn/src/utils/updaters/update-files.ts | 12 +- 3 files changed, 48 insertions(+), 90 deletions(-) diff --git a/packages/shadcn/src/utils/get-config.ts b/packages/shadcn/src/utils/get-config.ts index f59d5f33fb..4b85c8741e 100644 --- a/packages/shadcn/src/utils/get-config.ts +++ b/packages/shadcn/src/utils/get-config.ts @@ -7,10 +7,7 @@ import { } from "@/src/schema" import { getProjectInfo } from "@/src/utils/get-project-info" import { highlighter } from "@/src/utils/highlighter" -import { - resolveImport, - resolveImportWithMetadata, -} from "@/src/utils/resolve-import" +import { resolveImportWithMetadata } from "@/src/utils/resolve-import" import { cosmiconfig } from "cosmiconfig" import fg from "fast-glob" import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths" @@ -67,6 +64,20 @@ export async function resolveConfigPaths( ) } + // Resolve the primary aliases first so fallbacks can reuse their results. + const resolvedUtils = await resolveAliasPath( + "utils", + config.aliases["utils"], + cwd, + tsConfig + ) + const resolvedComponents = await resolveAliasPath( + "components", + config.aliases["components"], + cwd, + tsConfig + ) + return configSchema.parse({ ...config, resolvedPaths: { @@ -75,42 +86,16 @@ export async function resolveConfigPaths( ? path.resolve(cwd, config.tailwind.config) : "", tailwindCss: path.resolve(cwd, config.tailwind.css), - utils: await resolveAliasPath( - "utils", - config.aliases["utils"], - cwd, - tsConfig - ), - components: await resolveAliasPath( - "components", - config.aliases["components"], - cwd, - tsConfig - ), + utils: resolvedUtils, + components: resolvedComponents, ui: config.aliases["ui"] ? await resolveAliasPath("ui", config.aliases["ui"], cwd, tsConfig) - : path.resolve( - (await resolveAliasPath( - "components", - config.aliases["components"], - cwd, - tsConfig - )) ?? cwd, - "ui" - ), + : path.resolve(resolvedComponents ?? cwd, "ui"), // TODO: Make this configurable. // For now, we assume the lib and hooks directories are one level up from the components directory. lib: config.aliases["lib"] ? await resolveAliasPath("lib", config.aliases["lib"], cwd, tsConfig) - : path.resolve( - (await resolveAliasPath( - "utils", - config.aliases["utils"], - cwd, - tsConfig - )) ?? cwd, - ".." - ), + : path.resolve(resolvedUtils ?? cwd, ".."), hooks: config.aliases["hooks"] ? await resolveAliasPath( "hooks", @@ -118,16 +103,7 @@ export async function resolveConfigPaths( cwd, tsConfig ) - : path.resolve( - (await resolveAliasPath( - "components", - config.aliases["components"], - cwd, - tsConfig - )) ?? cwd, - "..", - "hooks" - ), + : path.resolve(resolvedComponents ?? cwd, "..", "hooks"), }, }) } @@ -144,36 +120,32 @@ async function resolveAliasPath( }) if (!resolved?.path) { - return await resolveImport(alias, { ...tsConfig, cwd }) + return null } + // For non-utils alias keys backed by package imports or workspace exports, + // strip directory-level artifacts so the resolved path points at the + // directory root rather than a specific file. if ( aliasKey !== "utils" && - ["package_imports", "workspace_package_exports"].includes( - resolved.source - ) && - !resolved.matchedAlias.includes("*") && - /\/index\.[^/]+$/.test(resolved.path) + (resolved.source === "package_imports" || + resolved.source === "workspace_package_exports") ) { - // Exact package-import aliases like `#hooks`, and exact workspace package - // exports that point at index files, should resolve to directory roots for - // components/hooks/ui/lib. `utils` stays file-based by design. - return path.dirname(resolved.path) - } + // Exact aliases (e.g. `#hooks` → `./src/hooks/index.ts`) should resolve + // to the directory root. + if ( + !resolved.matchedAlias.includes("*") && + /\/index\.[^/]+$/.test(resolved.path) + ) { + return path.dirname(resolved.path) + } - if ( - aliasKey !== "utils" && - ["package_imports", "workspace_package_exports"].includes( - resolved.source - ) && - resolved.matchedAlias.includes("*") && - /\.[^/]+$/.test(resolved.path) - ) { - // Wildcard package-import aliases and workspace package exports stored in - // components.json represent directory roots, even when the matched target - // is extension-based (`./src/components/*.tsx`). Strip the source extension - // so `ui` resolves to `/src/components/ui` instead of `/src/components/ui.tsx`. - return resolved.path.replace(/\.[^/]+$/, "") + // Wildcard aliases with explicit extensions (e.g. `#components/*` → + // `./src/components/*.tsx`) should strip the source extension so `ui` + // resolves to `/src/components/ui` instead of `/src/components/ui.tsx`. + if (resolved.matchedAlias.includes("*") && /\.[^/]+$/.test(resolved.path)) { + return resolved.path.replace(/\.[^/]+$/, "") + } } return resolved.path diff --git a/packages/shadcn/src/utils/resolve-import.ts b/packages/shadcn/src/utils/resolve-import.ts index e5f663c5ef..9aa3e20f5e 100644 --- a/packages/shadcn/src/utils/resolve-import.ts +++ b/packages/shadcn/src/utils/resolve-import.ts @@ -1,3 +1,4 @@ +import { getPatternWildcardValue } from "@/src/utils/import-matcher" import { resolvePackageImport, type ImportEmitMode, @@ -119,7 +120,7 @@ function findMatchingTsPathPattern( ) { for (const [key, targets] of Object.entries(paths)) { const targetList = Array.isArray(targets) ? targets : [targets] - const wildcardValue = getWildcardValue(importPath, key) + const wildcardValue = getPatternWildcardValue(importPath, key) if (wildcardValue === null) { continue @@ -136,18 +137,3 @@ function findMatchingTsPathPattern( return null } - -function getWildcardValue(importPath: string, pattern: string) { - if (!pattern.includes("*")) { - return importPath === pattern ? "" : null - } - - const [prefix, suffix = ""] = pattern.split("*") - if (!importPath.startsWith(prefix) || !importPath.endsWith(suffix)) { - return null - } - - return suffix - ? importPath.slice(prefix.length, -suffix.length) - : importPath.slice(prefix.length) -} diff --git a/packages/shadcn/src/utils/updaters/update-files.ts b/packages/shadcn/src/utils/updaters/update-files.ts index 7ca9771457..cf24345f55 100644 --- a/packages/shadcn/src/utils/updaters/update-files.ts +++ b/packages/shadcn/src/utils/updaters/update-files.ts @@ -37,6 +37,8 @@ import { Project, ScriptKind } from "ts-morph" import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths" import { z } from "zod" +const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"] + export async function updateFiles( files: RegistryItem["files"], config: Config, @@ -868,8 +870,7 @@ export function toAliasedImport( // 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 + const keepExt = CODE_EXTENSIONS.includes(ext) ? "" : ext let noExt = rel.slice(0, rel.length - ext.length) // 4️⃣ Collapse "/index" to its directory @@ -895,9 +896,9 @@ function toPackageImport( : never ) { const ext = path.posix.extname(relativePath) - const codeExts = [".ts", ".tsx", ".js", ".jsx"] const keepExt = - codeExts.includes(ext) && packageImport.emitMode === "strip_extension" + CODE_EXTENSIONS.includes(ext) && + packageImport.emitMode === "strip_extension" ? "" : ext const normalizedRelativePath = relativePath @@ -996,8 +997,7 @@ function toRelativeImport(fromFilePath: string, targetFilePath: string) { rel = rel.split(path.sep).join("/") const ext = path.posix.extname(rel) - const codeExts = [".ts", ".tsx", ".js", ".jsx"] - const keepExt = codeExts.includes(ext) ? "" : ext + const keepExt = CODE_EXTENSIONS.includes(ext) ? "" : ext let noExt = rel.slice(0, rel.length - ext.length) if (noExt.endsWith("/index")) {