fix: refactor

This commit is contained in:
shadcn
2026-03-16 19:21:49 +04:00
parent a434fada95
commit 8297097512
3 changed files with 48 additions and 90 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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")) {