fix(shadcn): fix async imports not being transformed (#8036)

* fix(shadcn): fix async imports not being transformed when installing components

* fix(shadcn): improve performance

* test(shadcn): add tests for transform import

* test: update timeout

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
Fuma Nama
2025-09-15 18:55:18 +08:00
committed by GitHub
parent fc6d909ba2
commit fae1a81add
5 changed files with 208 additions and 18 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
fix async imports not being transformed when installing components

View File

@@ -1,5 +1,6 @@
import { Config } from "@/src/utils/get-config"
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"
export const transformImport: Transformer = async ({
sourceFile,
@@ -9,32 +10,34 @@ export const transformImport: Transformer = async ({
const workspaceAlias = config.aliases?.utils?.split("/")[0]?.slice(1)
const utilsImport = `@${workspaceAlias}/lib/utils`
const importDeclarations = sourceFile.getImportDeclarations()
if (![".tsx", ".ts", ".jsx", ".js"].includes(sourceFile.getExtension())) {
return sourceFile
}
for (const importDeclaration of importDeclarations) {
const moduleSpecifier = updateImportAliases(
importDeclaration.getModuleSpecifierValue(),
for (const specifier of sourceFile.getImportStringLiterals()) {
const updated = updateImportAliases(
specifier.getLiteralValue(),
config,
isRemote
)
importDeclaration.setModuleSpecifier(moduleSpecifier)
specifier.setLiteralValue(updated)
// Replace `import { cn } from "@/lib/utils"`
if (utilsImport === moduleSpecifier || moduleSpecifier === "@/lib/utils") {
const namedImports = importDeclaration.getNamedImports()
const cnImport = namedImports.find((i) => i.getName() === "cn")
if (cnImport) {
importDeclaration.setModuleSpecifier(
utilsImport === moduleSpecifier
? moduleSpecifier.replace(utilsImport, config.aliases.utils)
: config.aliases.utils
)
}
if (utilsImport === updated || updated === "@/lib/utils") {
const importDeclaration = specifier.getFirstAncestorByKind(
SyntaxKind.ImportDeclaration
)
const isCnImport = importDeclaration
?.getNamedImports()
.some((namedImport) => namedImport.getName() === "cn")
if (!isCnImport) continue
specifier.setLiteralValue(
utilsImport === updated
? updated.replace(utilsImport, config.aliases.utils)
: config.aliases.utils
)
}
}

View File

@@ -1,5 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`transform async/dynamic imports 1`] = `
"import * as React from "react"
import { Button } from "@/components/ui/button"
async function loadComponent() {
const { cn } = await import("@/lib/utils")
const module = await import("@/components/ui/card")
return module
}
function lazyLoad() {
return import("@/components/ui/dialog").then(module => module)
}
"
`;
exports[`transform async/dynamic imports 2`] = `
"import { Button } from "~/components/ui/button"
async function loadUtils() {
const utils = await import("~/lib/utils")
const { cn } = await import("~/lib/utils")
return { utils, cn }
}
const dialogPromise = import("~/components/ui/dialog")
const cardModule = import("~/components/ui/card")
"
`;
exports[`transform dynamic imports with cn utility 1`] = `
"async function loadCn() {
const { cn } = await import("@/lib/utils")
return cn
}
async function loadMultiple() {
const utils1 = await import("@/lib/utils")
const { cn, twMerge } = await import("@/lib/utils")
const other = await import("@/lib/other")
}
"
`;
exports[`transform dynamic imports with cn utility 2`] = `
"async function loadWorkspaceCn() {
const { cn } = await import("@workspace/lib/utils")
return cn
}
"
`;
exports[`transform import 1`] = `
"import * as React from "react"
import { Foo } from "bar"
@@ -91,3 +143,14 @@ import { Foo } from "bar"
import { cn } from "@repo/ui/lib/utils"
"
`;
exports[`transform re-exports with dynamic imports 1`] = `
"export { cn } from "@/lib/utils"
export { Button } from "@/components/ui/button"
async function load() {
const module = await import("@/components/ui/card")
return module
}
"
`;

View File

@@ -144,7 +144,6 @@ import { Foo } from "bar"
).toMatchSnapshot()
})
test("transform import for monorepo", async () => {
expect(
await transform({
@@ -196,3 +195,122 @@ import { Foo } from "bar"
})
).toMatchSnapshot()
})
test("transform async/dynamic imports", async () => {
expect(
await transform({
filename: "test.ts",
raw: `import * as React from "react"
import { Button } from "@/registry/new-york/ui/button"
async function loadComponent() {
const { cn } = await import("@/lib/utils")
const module = await import("@/registry/new-york/ui/card")
return module
}
function lazyLoad() {
return import("@/registry/new-york/ui/dialog").then(module => module)
}
`,
config: {
tsx: true,
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
})
).toMatchSnapshot()
expect(
await transform({
filename: "test.ts",
raw: `import { Button } from "@/registry/new-york/ui/button"
async function loadUtils() {
const utils = await import("@/lib/utils")
const { cn } = await import("@/lib/utils")
return { utils, cn }
}
const dialogPromise = import("@/registry/new-york/ui/dialog")
const cardModule = import("@/registry/new-york/ui/card")
`,
config: {
tsx: true,
aliases: {
components: "~/components",
utils: "~/lib/utils",
},
},
})
).toMatchSnapshot()
})
test("transform dynamic imports with cn utility", async () => {
expect(
await transform({
filename: "test.ts",
raw: `async function loadCn() {
const { cn } = await import("@/lib/utils")
return cn
}
async function loadMultiple() {
const utils1 = await import("@/lib/utils")
const { cn, twMerge } = await import("@/lib/utils")
const other = await import("@/lib/other")
}
`,
config: {
tsx: true,
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
})
).toMatchSnapshot()
expect(
await transform({
filename: "test.ts",
raw: `async function loadWorkspaceCn() {
const { cn } = await import("@/lib/utils")
return cn
}
`,
config: {
tsx: true,
aliases: {
components: "@workspace/ui/components",
utils: "@workspace/ui/lib/utils",
},
},
})
).toMatchSnapshot()
})
test("transform re-exports with dynamic imports", async () => {
expect(
await transform({
filename: "test.ts",
raw: `export { cn } from "@/lib/utils"
export { Button } from "@/registry/new-york/ui/button"
async function load() {
const module = await import("@/registry/new-york/ui/card")
return module
}
`,
config: {
tsx: true,
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
})
).toMatchSnapshot()
})

View File

@@ -9,6 +9,7 @@ export default defineConfig({
"**/fixtures/**",
"**/templates/**",
],
testTimeout: 8000,
},
plugins: [
tsconfigPaths({