Skip to content

Commit fae1a81

Browse files
fuma-namashadcn
andauthored
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 <[email protected]>
1 parent fc6d909 commit fae1a81

File tree

5 files changed

+208
-18
lines changed

5 files changed

+208
-18
lines changed

.changeset/tame-schools-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": patch
3+
---
4+
5+
fix async imports not being transformed when installing components

packages/shadcn/src/utils/transformers/transform-import.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Config } from "@/src/utils/get-config"
22
import { Transformer } from "@/src/utils/transformers"
3+
import { SyntaxKind } from "ts-morph"
34

45
export const transformImport: Transformer = async ({
56
sourceFile,
@@ -9,32 +10,34 @@ export const transformImport: Transformer = async ({
910
const workspaceAlias = config.aliases?.utils?.split("/")[0]?.slice(1)
1011
const utilsImport = `@${workspaceAlias}/lib/utils`
1112

12-
const importDeclarations = sourceFile.getImportDeclarations()
13-
1413
if (![".tsx", ".ts", ".jsx", ".js"].includes(sourceFile.getExtension())) {
1514
return sourceFile
1615
}
1716

18-
for (const importDeclaration of importDeclarations) {
19-
const moduleSpecifier = updateImportAliases(
20-
importDeclaration.getModuleSpecifierValue(),
17+
for (const specifier of sourceFile.getImportStringLiterals()) {
18+
const updated = updateImportAliases(
19+
specifier.getLiteralValue(),
2120
config,
2221
isRemote
2322
)
24-
25-
importDeclaration.setModuleSpecifier(moduleSpecifier)
23+
specifier.setLiteralValue(updated)
2624

2725
// Replace `import { cn } from "@/lib/utils"`
28-
if (utilsImport === moduleSpecifier || moduleSpecifier === "@/lib/utils") {
29-
const namedImports = importDeclaration.getNamedImports()
30-
const cnImport = namedImports.find((i) => i.getName() === "cn")
31-
if (cnImport) {
32-
importDeclaration.setModuleSpecifier(
33-
utilsImport === moduleSpecifier
34-
? moduleSpecifier.replace(utilsImport, config.aliases.utils)
35-
: config.aliases.utils
36-
)
37-
}
26+
if (utilsImport === updated || updated === "@/lib/utils") {
27+
const importDeclaration = specifier.getFirstAncestorByKind(
28+
SyntaxKind.ImportDeclaration
29+
)
30+
const isCnImport = importDeclaration
31+
?.getNamedImports()
32+
.some((namedImport) => namedImport.getName() === "cn")
33+
34+
if (!isCnImport) continue
35+
36+
specifier.setLiteralValue(
37+
utilsImport === updated
38+
? updated.replace(utilsImport, config.aliases.utils)
39+
: config.aliases.utils
40+
)
3841
}
3942
}
4043

packages/shadcn/test/utils/__snapshots__/transform-import.test.ts.snap

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,57 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`transform async/dynamic imports 1`] = `
4+
"import * as React from "react"
5+
import { Button } from "@/components/ui/button"
6+
7+
async function loadComponent() {
8+
const { cn } = await import("@/lib/utils")
9+
const module = await import("@/components/ui/card")
10+
return module
11+
}
12+
13+
function lazyLoad() {
14+
return import("@/components/ui/dialog").then(module => module)
15+
}
16+
"
17+
`;
18+
19+
exports[`transform async/dynamic imports 2`] = `
20+
"import { Button } from "~/components/ui/button"
21+
22+
async function loadUtils() {
23+
const utils = await import("~/lib/utils")
24+
const { cn } = await import("~/lib/utils")
25+
return { utils, cn }
26+
}
27+
28+
const dialogPromise = import("~/components/ui/dialog")
29+
const cardModule = import("~/components/ui/card")
30+
"
31+
`;
32+
33+
exports[`transform dynamic imports with cn utility 1`] = `
34+
"async function loadCn() {
35+
const { cn } = await import("@/lib/utils")
36+
return cn
37+
}
38+
39+
async function loadMultiple() {
40+
const utils1 = await import("@/lib/utils")
41+
const { cn, twMerge } = await import("@/lib/utils")
42+
const other = await import("@/lib/other")
43+
}
44+
"
45+
`;
46+
47+
exports[`transform dynamic imports with cn utility 2`] = `
48+
"async function loadWorkspaceCn() {
49+
const { cn } = await import("@workspace/lib/utils")
50+
return cn
51+
}
52+
"
53+
`;
54+
355
exports[`transform import 1`] = `
456
"import * as React from "react"
557
import { Foo } from "bar"
@@ -91,3 +143,14 @@ import { Foo } from "bar"
91143
import { cn } from "@repo/ui/lib/utils"
92144
"
93145
`;
146+
147+
exports[`transform re-exports with dynamic imports 1`] = `
148+
"export { cn } from "@/lib/utils"
149+
export { Button } from "@/components/ui/button"
150+
151+
async function load() {
152+
const module = await import("@/components/ui/card")
153+
return module
154+
}
155+
"
156+
`;

packages/shadcn/test/utils/transform-import.test.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ import { Foo } from "bar"
144144
).toMatchSnapshot()
145145
})
146146

147-
148147
test("transform import for monorepo", async () => {
149148
expect(
150149
await transform({
@@ -196,3 +195,122 @@ import { Foo } from "bar"
196195
})
197196
).toMatchSnapshot()
198197
})
198+
199+
test("transform async/dynamic imports", async () => {
200+
expect(
201+
await transform({
202+
filename: "test.ts",
203+
raw: `import * as React from "react"
204+
import { Button } from "@/registry/new-york/ui/button"
205+
206+
async function loadComponent() {
207+
const { cn } = await import("@/lib/utils")
208+
const module = await import("@/registry/new-york/ui/card")
209+
return module
210+
}
211+
212+
function lazyLoad() {
213+
return import("@/registry/new-york/ui/dialog").then(module => module)
214+
}
215+
`,
216+
config: {
217+
tsx: true,
218+
aliases: {
219+
components: "@/components",
220+
utils: "@/lib/utils",
221+
},
222+
},
223+
})
224+
).toMatchSnapshot()
225+
226+
expect(
227+
await transform({
228+
filename: "test.ts",
229+
raw: `import { Button } from "@/registry/new-york/ui/button"
230+
231+
async function loadUtils() {
232+
const utils = await import("@/lib/utils")
233+
const { cn } = await import("@/lib/utils")
234+
return { utils, cn }
235+
}
236+
237+
const dialogPromise = import("@/registry/new-york/ui/dialog")
238+
const cardModule = import("@/registry/new-york/ui/card")
239+
`,
240+
config: {
241+
tsx: true,
242+
aliases: {
243+
components: "~/components",
244+
utils: "~/lib/utils",
245+
},
246+
},
247+
})
248+
).toMatchSnapshot()
249+
})
250+
251+
test("transform dynamic imports with cn utility", async () => {
252+
expect(
253+
await transform({
254+
filename: "test.ts",
255+
raw: `async function loadCn() {
256+
const { cn } = await import("@/lib/utils")
257+
return cn
258+
}
259+
260+
async function loadMultiple() {
261+
const utils1 = await import("@/lib/utils")
262+
const { cn, twMerge } = await import("@/lib/utils")
263+
const other = await import("@/lib/other")
264+
}
265+
`,
266+
config: {
267+
tsx: true,
268+
aliases: {
269+
components: "@/components",
270+
utils: "@/lib/utils",
271+
},
272+
},
273+
})
274+
).toMatchSnapshot()
275+
276+
expect(
277+
await transform({
278+
filename: "test.ts",
279+
raw: `async function loadWorkspaceCn() {
280+
const { cn } = await import("@/lib/utils")
281+
return cn
282+
}
283+
`,
284+
config: {
285+
tsx: true,
286+
aliases: {
287+
components: "@workspace/ui/components",
288+
utils: "@workspace/ui/lib/utils",
289+
},
290+
},
291+
})
292+
).toMatchSnapshot()
293+
})
294+
295+
test("transform re-exports with dynamic imports", async () => {
296+
expect(
297+
await transform({
298+
filename: "test.ts",
299+
raw: `export { cn } from "@/lib/utils"
300+
export { Button } from "@/registry/new-york/ui/button"
301+
302+
async function load() {
303+
const module = await import("@/registry/new-york/ui/card")
304+
return module
305+
}
306+
`,
307+
config: {
308+
tsx: true,
309+
aliases: {
310+
components: "@/components",
311+
utils: "@/lib/utils",
312+
},
313+
},
314+
})
315+
).toMatchSnapshot()
316+
})

packages/shadcn/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default defineConfig({
99
"**/fixtures/**",
1010
"**/templates/**",
1111
],
12+
testTimeout: 8000,
1213
},
1314
plugins: [
1415
tsconfigPaths({

0 commit comments

Comments
 (0)