diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index 6eb2fda260..5a1a45a67a 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -21,6 +21,7 @@ import { templates, } from "@/src/templates/index" import { addComponents } from "@/src/utils/add-components" +import { getInitAliasDefaults } from "@/src/utils/alias" import { createProject } from "@/src/utils/create-project" import { loadEnvFiles } from "@/src/utils/env-loader" import * as ERRORS from "@/src/utils/errors" @@ -905,63 +906,6 @@ async function promptForConfig(defaultConfig: Config | null = null) { }) } -export function getInitAliasDefaults( - componentsAlias: string, - existingAliases?: Config["aliases"] -) { - const derivedLib = - existingAliases?.lib ?? deriveAliasFromComponents(componentsAlias, "lib") - - return { - ui: existingAliases?.ui ?? deriveAliasFromComponents(componentsAlias, "ui"), - lib: derivedLib, - hooks: - existingAliases?.hooks ?? - deriveAliasFromComponents(componentsAlias, "hooks"), - utils: - existingAliases?.utils ?? - deriveAliasFromComponents(componentsAlias, "utils", derivedLib), - } -} - -function deriveAliasFromComponents( - componentsAlias: string, - kind: "ui" | "lib" | "hooks" | "utils", - libAlias?: string -) { - const alias = componentsAlias || DEFAULT_COMPONENTS - - if (kind === "ui") { - return `${alias}/ui` - } - - if (kind === "utils") { - const resolvedLib = libAlias || replaceComponentsAliasTail(alias, "lib") - return resolvedLib ? `${resolvedLib}/utils` : DEFAULT_UTILS - } - - return replaceComponentsAliasTail(alias, kind) -} - -function replaceComponentsAliasTail( - alias: string, - kind: "lib" | "hooks" -) { - if (alias === "components") { - return kind - } - - if (alias.endsWith("/components")) { - return `${alias.slice(0, -"/components".length)}/${kind}` - } - - if (alias.endsWith("components") && !alias.includes("/")) { - return `${alias.slice(0, -"components".length)}${kind}` - } - - return "" -} - export function shouldRunTemplatePostInit( template: | { postInit?: (options: { projectPath: string }) => Promise } diff --git a/packages/shadcn/src/utils/alias.test.ts b/packages/shadcn/src/utils/alias.test.ts new file mode 100644 index 0000000000..7f4054dd03 --- /dev/null +++ b/packages/shadcn/src/utils/alias.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "vitest" + +import { + deriveAliasFromComponents, + getInitAliasDefaults, +} from "@/src/utils/alias" + +describe("deriveAliasFromComponents", () => { + test("derives ui aliases from components", () => { + expect(deriveAliasFromComponents("@/components", "ui")).toBe( + "@/components/ui" + ) + }) + + test("derives utils aliases from lib aliases", () => { + expect(deriveAliasFromComponents("#components", "utils")).toBe("#lib/utils") + expect( + deriveAliasFromComponents("#custom/components", "utils", "#custom/lib") + ).toBe("#custom/lib/utils") + }) + + test("derives sibling lib and hooks aliases from components", () => { + expect(deriveAliasFromComponents("@/components", "lib")).toBe("@/lib") + expect(deriveAliasFromComponents("#custom/components", "hooks")).toBe( + "#custom/hooks" + ) + }) + + test("returns an empty string when components alias has no sibling base", () => { + expect(deriveAliasFromComponents("#custom/ui", "lib")).toBe("") + }) +}) + +describe("getInitAliasDefaults", () => { + test("derives standard aliases from components", () => { + expect(getInitAliasDefaults("@/components")).toEqual({ + ui: "@/components/ui", + lib: "@/lib", + hooks: "@/hooks", + utils: "@/lib/utils", + }) + }) + + test("derives package import aliases from #components", () => { + expect(getInitAliasDefaults("#components")).toEqual({ + ui: "#components/ui", + lib: "#lib", + hooks: "#hooks", + utils: "#lib/utils", + }) + }) + + test("derives sibling aliases for nested custom aliases", () => { + expect(getInitAliasDefaults("#custom/components")).toEqual({ + ui: "#custom/components/ui", + lib: "#custom/lib", + hooks: "#custom/hooks", + utils: "#custom/lib/utils", + }) + }) + + test("preserves existing aliases when components alias is unchanged", () => { + expect( + getInitAliasDefaults("#components", { + components: "#components", + ui: "#components/ui", + lib: "#lib", + hooks: "#hooks", + utils: "#lib/utils", + }) + ).toEqual({ + ui: "#components/ui", + lib: "#lib", + hooks: "#hooks", + utils: "#lib/utils", + }) + }) +}) diff --git a/packages/shadcn/src/utils/alias.ts b/packages/shadcn/src/utils/alias.ts new file mode 100644 index 0000000000..02185ede94 --- /dev/null +++ b/packages/shadcn/src/utils/alias.ts @@ -0,0 +1,65 @@ +import type { Config } from "@/src/utils/get-config" +import { DEFAULT_COMPONENTS, DEFAULT_UTILS } from "@/src/utils/get-config" + +export function getInitAliasDefaults( + componentsAlias: string, + existingAliases?: Config["aliases"] +) { + // `lib` is the anchor for deriving `utils`, so reuse the existing value first + // when init is re-running against the same components alias. + const derivedLib = + existingAliases?.lib ?? deriveAliasFromComponents(componentsAlias, "lib") + + return { + ui: existingAliases?.ui ?? deriveAliasFromComponents(componentsAlias, "ui"), + lib: derivedLib, + hooks: + existingAliases?.hooks ?? + deriveAliasFromComponents(componentsAlias, "hooks"), + utils: + existingAliases?.utils ?? + deriveAliasFromComponents(componentsAlias, "utils", derivedLib), + } +} + +export function deriveAliasFromComponents( + componentsAlias: string, + kind: "ui" | "lib" | "hooks" | "utils", + libAlias?: string +) { + const alias = componentsAlias || DEFAULT_COMPONENTS + + if (kind === "ui") { + return `${alias}/ui` + } + + if (kind === "utils") { + // `utils` follows `lib`, not `components`, so derive or reuse the sibling + // lib alias before appending `/utils`. + const resolvedLib = libAlias || replaceComponentsAliasTail(alias, "lib") + return resolvedLib ? `${resolvedLib}/utils` : DEFAULT_UTILS + } + + return replaceComponentsAliasTail(alias, kind) +} + +function replaceComponentsAliasTail( + alias: string, + kind: "lib" | "hooks" +) { + // Handles the common `@/components` and `#custom/components` forms by + // swapping the trailing `components` segment for a sibling alias root. + if (alias === "components") { + return kind + } + + if (alias.endsWith("/components")) { + return `${alias.slice(0, -"/components".length)}/${kind}` + } + + if (alias.endsWith("components") && !alias.includes("/")) { + return `${alias.slice(0, -"components".length)}${kind}` + } + + return "" +} diff --git a/packages/shadcn/test/commands/init.test.ts b/packages/shadcn/test/commands/init.test.ts index 2b88536a4e..c330ce2118 100644 --- a/packages/shadcn/test/commands/init.test.ts +++ b/packages/shadcn/test/commands/init.test.ts @@ -1,55 +1,6 @@ import { describe, expect, test } from "vitest" -import { - getInitAliasDefaults, - shouldRunTemplatePostInit, -} from "../../src/commands/init" - -describe("getInitAliasDefaults", () => { - test("derives standard aliases from components", () => { - expect(getInitAliasDefaults("@/components")).toEqual({ - ui: "@/components/ui", - lib: "@/lib", - hooks: "@/hooks", - utils: "@/lib/utils", - }) - }) - - test("derives package import aliases from #components", () => { - expect(getInitAliasDefaults("#components")).toEqual({ - ui: "#components/ui", - lib: "#lib", - hooks: "#hooks", - utils: "#lib/utils", - }) - }) - - test("derives sibling aliases for nested custom aliases", () => { - expect(getInitAliasDefaults("#custom/components")).toEqual({ - ui: "#custom/components/ui", - lib: "#custom/lib", - hooks: "#custom/hooks", - utils: "#custom/lib/utils", - }) - }) - - test("preserves existing aliases when components alias is unchanged", () => { - expect( - getInitAliasDefaults("#components", { - components: "#components", - ui: "#components/ui", - lib: "#lib", - hooks: "#hooks", - utils: "#lib/utils", - }) - ).toEqual({ - ui: "#components/ui", - lib: "#lib", - hooks: "#hooks", - utils: "#lib/utils", - }) - }) -}) +import { shouldRunTemplatePostInit } from "../../src/commands/init" describe("shouldRunTemplatePostInit", () => { test("does not run post-init for existing projects with an explicit template", () => {