fix: refactor

This commit is contained in:
shadcn
2026-03-16 13:48:31 +04:00
parent ef35fd8f4c
commit 83f5d46b6e
6 changed files with 309 additions and 372 deletions

View File

@@ -22,6 +22,7 @@ import { transformCss } from "@/src/utils/updaters/update-css"
import { transformCssVars } from "@/src/utils/updaters/update-css-vars"
import {
findCommonRoot,
getPlannedFilePaths,
resolveFilePath,
rewriteResolvedImportsInContent,
} from "@/src/utils/updaters/update-files"
@@ -152,32 +153,10 @@ async function processFiles(
const project = new Project({
compilerOptions: {},
})
const plannedFilePaths = files
.filter((file): file is NonNullable<typeof file> => !!file?.content)
.map((file, index) => {
let plannedFilePath = resolveFilePath(file, config, {
isSrcDir: projectInfo?.isSrcDir,
framework: projectInfo?.framework.name,
commonRoot: findCommonRoot(
files.map((entry) => entry.path),
file.path
),
fileIndex: index,
})
if (!plannedFilePath) {
return null
}
if (!config.tsx) {
plannedFilePath = plannedFilePath.replace(/\.tsx?$/, (match) =>
match === ".tsx" ? ".jsx" : ".js"
)
}
return path.relative(config.resolvedPaths.cwd, plannedFilePath)
})
.filter((filePath): filePath is string => !!filePath)
const plannedFilePaths = getPlannedFilePaths(files, config, {
isSrcDir: projectInfo?.isSrcDir,
framework: projectInfo?.framework.name,
})
for (let index = 0; index < files.length; index++) {
const file = files[index]

View File

@@ -0,0 +1,152 @@
import path from "path"
export type ImportEmitMode = "strip_extension" | "preserve_extension"
export type ImportResolutionEntry = {
key: string
aliasBase: string
target: string
emitMode: ImportEmitMode
hasWildcard: boolean
rootDir: string
}
export type ImportResolutionMatch = {
path: string
matchedAlias: string
matchedTarget: string
emitMode: ImportEmitMode
}
export function resolveLocalPathTarget(target: unknown): string | null {
if (typeof target === "string") {
return target.startsWith(".") ? target : null
}
if (Array.isArray(target)) {
for (const value of target) {
const resolved = resolveLocalPathTarget(value)
if (resolved) {
return resolved
}
}
return null
}
if (target && typeof target === "object") {
for (const value of Object.values(target as Record<string, unknown>)) {
const resolved = resolveLocalPathTarget(value)
if (resolved) {
return resolved
}
}
}
return null
}
export function getImportTargetEmitMode(target: string): ImportEmitMode {
if (!target.includes("*")) {
return "strip_extension"
}
const suffix = target.slice(target.indexOf("*") + 1)
// A bare `*` target like `./src/components/*` expects the emitted specifier
// to include the source extension (`#components/button.tsx`).
if (!suffix) {
return "preserve_extension"
}
return /^\.[^/]+$/.test(suffix) ? "strip_extension" : "preserve_extension"
}
export function resolveImportEntryMatch(
importPath: string,
entries: ImportResolutionEntry[]
): ImportResolutionMatch | null {
const exactMatch = entries.find(
(entry) => !entry.hasWildcard && entry.key === importPath
)
if (exactMatch) {
return {
path: path.resolve(exactMatch.rootDir, exactMatch.target),
matchedAlias: exactMatch.key,
matchedTarget: exactMatch.target,
emitMode: exactMatch.emitMode,
}
}
const wildcardMatches = entries
.filter((entry) => entry.hasWildcard)
.sort((a, b) => b.key.length - a.key.length)
for (const entry of wildcardMatches) {
const wildcardValue = getPatternWildcardValue(importPath, entry.key, {
allowBareAliasBase: true,
})
if (wildcardValue === null) {
continue
}
return {
path: path.resolve(
entry.rootDir,
applyWildcardTarget(entry.target, wildcardValue)
),
matchedAlias: entry.key,
matchedTarget: entry.target,
emitMode: entry.emitMode,
}
}
return null
}
export function getPatternWildcardValue(
importPath: string,
pattern: string,
options: {
allowBareAliasBase?: boolean
} = {}
): string | null {
if (!pattern.includes("*")) {
return importPath === pattern ? "" : null
}
const [prefix, suffix = ""] = pattern.split("*")
if (importPath.startsWith(prefix) && importPath.endsWith(suffix)) {
return suffix
? importPath.slice(prefix.length, -suffix.length)
: importPath.slice(prefix.length)
}
if (
options.allowBareAliasBase &&
suffix === "" &&
prefix.endsWith("/") &&
importPath === prefix.slice(0, -1)
) {
return ""
}
return null
}
export function applyWildcardTarget(target: string, wildcardValue: string) {
if (!target.includes("*")) {
return target
}
const [prefix, suffix = ""] = target.split("*")
if (!wildcardValue) {
return prefix.replace(/\/$/, "")
}
return `${prefix}${wildcardValue}${suffix}`
}

View File

@@ -1,22 +1,17 @@
import path from "path"
import { getPackageInfo } from "@/src/utils/get-package-info"
import {
getImportTargetEmitMode,
resolveImportEntryMatch,
resolveLocalPathTarget,
type ImportEmitMode,
type ImportResolutionEntry,
type ImportResolutionMatch,
} from "@/src/utils/import-entries"
export type ImportEmitMode = "strip_extension" | "preserve_extension"
export type PackageImportEntry = {
key: string
aliasBase: string
target: string
emitMode: ImportEmitMode
hasWildcard: boolean
}
export type PackageImportMatch = {
path: string
matchedAlias: string
matchedTarget: string
emitMode: ImportEmitMode
}
export type { ImportEmitMode } from "@/src/utils/import-entries"
export type PackageImportEntry = ImportResolutionEntry
export type PackageImportMatch = ImportResolutionMatch
const packageImportEntriesCache = new Map<string, PackageImportEntry[]>()
@@ -43,7 +38,7 @@ export function getPackageImportEntries(cwd: string): PackageImportEntry[] {
continue
}
const target = resolveLocalImportTarget(value)
const target = resolveLocalPathTarget(value)
if (!target) {
continue
}
@@ -52,8 +47,9 @@ export function getPackageImportEntries(cwd: string): PackageImportEntry[] {
key,
aliasBase: key.endsWith("/*") ? key.slice(0, -2) : key,
target,
emitMode: getImportEmitMode(target),
emitMode: getImportTargetEmitMode(target),
hasWildcard: key.includes("*"),
rootDir: cacheKey,
})
}
@@ -75,41 +71,7 @@ export function resolvePackageImport(
importPath: string,
cwd: string
): PackageImportMatch | null {
const entries = getPackageImportEntries(cwd)
const exactMatch = entries.find(
(entry) => !entry.hasWildcard && entry.key === importPath
)
if (exactMatch) {
return {
path: path.resolve(cwd, exactMatch.target),
matchedAlias: exactMatch.key,
matchedTarget: exactMatch.target,
emitMode: exactMatch.emitMode,
}
}
const wildcardMatches = entries
.filter((entry) => entry.hasWildcard)
.sort((a, b) => b.key.length - a.key.length)
for (const entry of wildcardMatches) {
const [keyPrefix, keySuffix] = entry.key.split("*")
const wildcardValue = getWildcardValue(importPath, keyPrefix, keySuffix)
if (wildcardValue === null) {
continue
}
return {
path: path.resolve(cwd, applyWildcardTarget(entry.target, wildcardValue)),
matchedAlias: entry.key,
matchedTarget: entry.target,
emitMode: entry.emitMode,
}
}
return null
return resolveImportEntryMatch(importPath, getPackageImportEntries(cwd))
}
export function getPackageImportAliases(cwd: string) {
@@ -124,89 +86,6 @@ export function getPackageImportAliases(cwd: string) {
}
}
function resolveLocalImportTarget(target: unknown): string | null {
if (typeof target === "string") {
return target.startsWith(".") ? target : null
}
if (Array.isArray(target)) {
for (const value of target) {
const resolved = resolveLocalImportTarget(value)
if (resolved) {
return resolved
}
}
return null
}
if (target && typeof target === "object") {
for (const value of Object.values(target as Record<string, unknown>)) {
const resolved = resolveLocalImportTarget(value)
if (resolved) {
return resolved
}
}
}
return null
}
function getImportEmitMode(target: string): ImportEmitMode {
if (!target.includes("*")) {
return "strip_extension"
}
const suffix = target.slice(target.indexOf("*") + 1)
// A bare `*` target like `./src/components/*` expects the emitted specifier
// to include the source extension (`#components/button.tsx`).
if (!suffix) {
return "preserve_extension"
}
return /^\.[^/]+$/.test(suffix) ? "strip_extension" : "preserve_extension"
}
function getWildcardValue(
importPath: string,
prefix: string,
suffix: string
): string | null {
if (importPath.startsWith(prefix) && importPath.endsWith(suffix)) {
return suffix
? importPath.slice(prefix.length, -suffix.length)
: importPath.slice(prefix.length)
}
// `components.json` stores alias roots like `#components`, not raw
// `package.json#imports` patterns like `#components/*`, so we accept the
// bare alias base here to keep package-import support schema-compatible.
if (
suffix === "" &&
prefix.endsWith("/") &&
importPath === prefix.slice(0, -1)
) {
return ""
}
return null
}
function applyWildcardTarget(target: string, wildcardValue: string) {
if (!target.includes("*")) {
return target
}
const [prefix, suffix = ""] = target.split("*")
if (!wildcardValue) {
return prefix.replace(/\/$/, "")
}
return `${prefix}${wildcardValue}${suffix}`
}
function findBestAlias(
entries: PackageImportEntry[],
kind: "components" | "ui" | "lib" | "hooks" | "utils"

View File

@@ -26,59 +26,19 @@ export async function resolveImportWithMetadata(
): Promise<ResolvedImport | null> {
const cwd = config.cwd ?? config.absoluteBaseUrl
if (importPath.startsWith("#")) {
const packageImport = resolvePackageImport(importPath, cwd)
for (const resolver of [
() => resolveFromPackageImports(importPath, cwd),
() => resolveFromWorkspacePackageExports(importPath, cwd),
() => resolveFromTsconfigPaths(importPath, config),
]) {
const resolved = await resolver()
if (packageImport) {
return {
path: packageImport.path,
source: "package_imports",
matchedAlias: packageImport.matchedAlias,
matchedTarget: packageImport.matchedTarget,
emitMode: packageImport.emitMode,
}
if (resolved) {
return resolved
}
}
const workspacePackageExport = await resolveWorkspacePackageExport(
importPath,
cwd
)
if (workspacePackageExport) {
return {
path: workspacePackageExport.path,
source: "workspace_package_exports",
matchedAlias: workspacePackageExport.matchedAlias,
matchedTarget: workspacePackageExport.matchedTarget,
emitMode: workspacePackageExport.emitMode,
}
}
const matchedPath = createMatchPath(config.absoluteBaseUrl, config.paths)(
importPath,
undefined,
() => true,
[".ts", ".tsx", ".jsx", ".js", ".css"]
)
if (!matchedPath) {
return null
}
const matchedPattern = findMatchingTsPathPattern(importPath, config.paths)
if (!matchedPattern && isScopedPackageSpecifier(importPath)) {
return null
}
return {
path: matchedPath,
source: "tsconfig_paths",
matchedAlias: matchedPattern?.key ?? importPath,
matchedTarget: matchedPattern?.target ?? matchedPath,
emitMode: "strip_extension",
}
return null
}
export async function resolveImport(
@@ -109,6 +69,79 @@ function isScopedPackageSpecifier(importPath: string) {
return /^@[^/]+\/[^/]+(?:\/.*)?$/.test(importPath)
}
function toResolvedImport(
source: ResolvedImport["source"],
resolved: {
path: string
matchedAlias: string
matchedTarget: string
emitMode: ImportEmitMode
} | null
) {
if (!resolved) {
return null
}
return {
path: resolved.path,
source,
matchedAlias: resolved.matchedAlias,
matchedTarget: resolved.matchedTarget,
emitMode: resolved.emitMode,
} satisfies ResolvedImport
}
function resolveFromPackageImports(importPath: string, cwd: string) {
if (!importPath.startsWith("#")) {
return null
}
return toResolvedImport(
"package_imports",
resolvePackageImport(importPath, cwd)
)
}
async function resolveFromWorkspacePackageExports(
importPath: string,
cwd: string
) {
return toResolvedImport(
"workspace_package_exports",
await resolveWorkspacePackageExport(importPath, cwd)
)
}
function resolveFromTsconfigPaths(
importPath: string,
config: ResolveImportConfig
): ResolvedImport | null {
const matchedPath = createMatchPath(config.absoluteBaseUrl, config.paths)(
importPath,
undefined,
() => true,
[".ts", ".tsx", ".jsx", ".js", ".css"]
)
if (!matchedPath) {
return null
}
const matchedPattern = findMatchingTsPathPattern(importPath, config.paths)
if (!matchedPattern && isScopedPackageSpecifier(importPath)) {
return null
}
return {
path: matchedPath,
source: "tsconfig_paths",
matchedAlias: matchedPattern?.key ?? importPath,
matchedTarget: matchedPattern?.target ?? matchedPath,
emitMode: "strip_extension",
}
}
function findMatchingTsPathPattern(
importPath: string,
paths: ConfigLoaderSuccessResult["paths"]

View File

@@ -34,7 +34,7 @@ import { transformRtl } from "@/src/utils/transformers/transform-rtl"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
import prompts from "prompts"
import { Project, ScriptKind } from "ts-morph"
import { loadConfig } from "tsconfig-paths"
import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths"
import { z } from "zod"
export async function updateFiles(
@@ -594,7 +594,7 @@ async function resolveImports(filePaths: string[], config: Config) {
return updatedFiles
}
function getPlannedFilePaths(
export function getPlannedFilePaths(
files: RegistryItem["files"],
config: Config,
options: {
@@ -683,26 +683,11 @@ export async function rewriteResolvedImportsInContent({
continue
}
const probableImportFilePath = (
await resolveImportWithMetadata(moduleSpecifier, {
...tsConfig,
cwd: config.resolvedPaths.cwd,
})
)?.path
const fallbackImportFilePath =
!probableImportFilePath && !moduleSpecifier.startsWith(".")
? resolveImportFromConfiguredAliases(moduleSpecifier, config)
: null
if (!probableImportFilePath && !fallbackImportFilePath) {
continue
}
const resolvedImportFilePath = resolveModuleByProbablePath(
probableImportFilePath ?? fallbackImportFilePath!,
const resolvedImportFilePath = await resolveImportFilePathForRewrite(
moduleSpecifier,
filePaths,
config
config,
tsConfig
)
if (!resolvedImportFilePath) {
@@ -727,6 +712,35 @@ export async function rewriteResolvedImportsInContent({
return hasChanges ? workingSourceFile.getFullText() : content
}
async function resolveImportFilePathForRewrite(
moduleSpecifier: string,
filePaths: string[],
config: Config,
tsConfig: Pick<ConfigLoaderSuccessResult, "absoluteBaseUrl" | "paths">
) {
const probableImportFilePath = (
await resolveImportWithMetadata(moduleSpecifier, {
...tsConfig,
cwd: config.resolvedPaths.cwd,
})
)?.path
const fallbackImportFilePath =
!probableImportFilePath && !moduleSpecifier.startsWith(".")
? resolveImportFromConfiguredAliases(moduleSpecifier, config)
: null
if (!probableImportFilePath && !fallbackImportFilePath) {
return null
}
return resolveModuleByProbablePath(
probableImportFilePath ?? fallbackImportFilePath!,
filePaths,
config
)
}
/**
* Given an absolute "probable" import path (no ext),
* plus an array of absolute file paths you already know about,

View File

@@ -1,30 +1,24 @@
import path from "path"
import { getWorkspacePatterns } from "@/src/utils/get-monorepo-info"
import { getPackageInfo } from "@/src/utils/get-package-info"
import { type ImportEmitMode } from "@/src/utils/package-imports"
import fg from "fast-glob"
import fs from "fs-extra"
import {
getImportTargetEmitMode,
resolveImportEntryMatch,
resolveLocalPathTarget,
type ImportEmitMode,
type ImportResolutionEntry,
type ImportResolutionMatch,
} from "@/src/utils/import-entries"
type WorkspacePackageInfo = {
packageName: string
packageRoot: string
}
type WorkspacePackageExportEntry = {
key: string
aliasBase: string
target: string
emitMode: ImportEmitMode
hasWildcard: boolean
packageRoot: string
}
export type WorkspacePackageExportMatch = {
path: string
matchedAlias: string
matchedTarget: string
emitMode: ImportEmitMode
}
type WorkspacePackageExportEntry = ImportResolutionEntry
export type WorkspacePackageExportMatch = ImportResolutionMatch
const workspacePackageCache = new Map<
string,
@@ -55,46 +49,10 @@ export async function resolveWorkspacePackageExport(
return null
}
const entries = getWorkspacePackageExportEntries(workspacePackage)
const exactMatch = entries.find(
(entry) => !entry.hasWildcard && entry.aliasBase === importPath
return resolveImportEntryMatch(
importPath,
getWorkspacePackageExportEntries(workspacePackage)
)
if (exactMatch) {
return {
path: path.resolve(exactMatch.packageRoot, exactMatch.target),
matchedAlias: exactMatch.aliasBase,
matchedTarget: exactMatch.target,
emitMode: exactMatch.emitMode,
}
}
const wildcardMatches = entries
.filter((entry) => entry.hasWildcard)
.sort((a, b) => b.aliasBase.length - a.aliasBase.length)
for (const entry of wildcardMatches) {
const pattern = `${entry.aliasBase}/*`
const [prefix, suffix = ""] = pattern.split("*")
const wildcardValue = getWildcardValue(importPath, prefix, suffix)
if (wildcardValue === null) {
continue
}
return {
path: path.resolve(
entry.packageRoot,
applyWildcardTarget(entry.target, wildcardValue)
),
matchedAlias: pattern,
matchedTarget: entry.target,
emitMode: entry.emitMode,
}
}
return null
}
function getWorkspacePackageExportEntries(
@@ -126,19 +84,21 @@ function getWorkspacePackageExportEntries(
continue
}
const target = resolveLocalExportTarget(value)
const target = resolveLocalPathTarget(value)
if (!target) {
continue
}
const aliasBase = getAliasBase(workspacePackage.packageName, key)
entries.push({
key,
aliasBase: getAliasBase(workspacePackage.packageName, key),
key: key.includes("*") ? `${aliasBase}/*` : aliasBase,
aliasBase,
target,
emitMode: getExportEmitMode(target),
emitMode: getImportTargetEmitMode(target),
hasWildcard: key.includes("*"),
packageRoot: workspacePackage.packageRoot,
rootDir: workspacePackage.packageRoot,
})
}
@@ -292,83 +252,3 @@ function getAliasBase(packageName: string, exportKey: string) {
return normalizedKey ? `${packageName}/${normalizedKey}` : packageName
}
function resolveLocalExportTarget(target: unknown): string | null {
if (typeof target === "string") {
return target.startsWith(".") ? target : null
}
if (Array.isArray(target)) {
for (const value of target) {
const resolved = resolveLocalExportTarget(value)
if (resolved) {
return resolved
}
}
return null
}
if (target && typeof target === "object") {
for (const value of Object.values(target as Record<string, unknown>)) {
const resolved = resolveLocalExportTarget(value)
if (resolved) {
return resolved
}
}
}
return null
}
function getExportEmitMode(target: string): ImportEmitMode {
if (!target.includes("*")) {
return "strip_extension"
}
const suffix = target.slice(target.indexOf("*") + 1)
if (!suffix) {
return "preserve_extension"
}
return /^\.[^/]+$/.test(suffix) ? "strip_extension" : "preserve_extension"
}
function getWildcardValue(
importPath: string,
prefix: string,
suffix: string
): string | null {
if (importPath.startsWith(prefix) && importPath.endsWith(suffix)) {
return suffix
? importPath.slice(prefix.length, -suffix.length)
: importPath.slice(prefix.length)
}
if (
suffix === "" &&
prefix.endsWith("/") &&
importPath === prefix.slice(0, -1)
) {
return ""
}
return null
}
function applyWildcardTarget(target: string, wildcardValue: string) {
if (!target.includes("*")) {
return target
}
const [prefix, suffix = ""] = target.split("*")
if (!wildcardValue) {
return prefix.replace(/\/$/, "")
}
return `${prefix}${wildcardValue}${suffix}`
}