Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/apply-monorepo-packages-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn": patch
---

fix `apply` failing with "could not detect a supported framework" when targeting a monorepo workspace (e.g. `packages/ui`) that has a valid `components.json` but no framework config
127 changes: 127 additions & 0 deletions packages/shadcn/src/preflights/preflight-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import path from "path"
import fs from "fs-extra"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

import { preFlightInit } from "./preflight-init"

let tmpDir: string

beforeEach(async () => {
tmpDir = path.join(
await fs.realpath(require("os").tmpdir()),
`shadcn-preflight-init-test-${Date.now()}-${Math.random()
.toString(36)
.slice(2)}`
)
await fs.ensureDir(tmpDir)
})

afterEach(async () => {
await fs.remove(tmpDir)
})

describe("preFlightInit", () => {
it("should not error on missing framework when existingConfig is provided", async () => {
// Simulate a shadcn monorepo UI workspace: a package.json with
// tailwind and an import alias but no framework config. This mirrors
// `packages/ui` in a shadcn-generated monorepo where the UI library
// is a sibling of the app workspace (e.g. `apps/web`).
await fs.writeJson(path.join(tmpDir, "package.json"), {
name: "@acme/ui",
dependencies: {
tailwindcss: "^4.0.0",
},
})
await fs.writeFile(
path.join(tmpDir, "styles.css"),
`@import "tailwindcss";\n`
)
await fs.writeJson(path.join(tmpDir, "tsconfig.json"), {
compilerOptions: {
paths: {
"@acme/ui/*": ["./src/*"],
},
},
})

const exitSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never)

const result = await preFlightInit({
cwd: tmpDir,
yes: true,
defaults: false,
// Force bypasses the components.json-exists check — apply uses
// a backup file to side-step that, but for the test this is equivalent.
force: true,
silent: true,
isNewProject: false,
cssVariables: true,
installStyleIndex: true,
existingConfig: {
$schema: "https://ui.shadcn.com/schema.json",
style: "new-york",
tailwind: {
config: "",
css: "styles.css",
baseColor: "neutral",
cssVariables: true,
prefix: "",
},
aliases: {
components: "@acme/ui/components",
ui: "@acme/ui/components/ui",
hooks: "@acme/ui/hooks",
lib: "@acme/ui/lib",
utils: "@acme/ui/lib/utils",
},
},
})

expect(exitSpy).not.toHaveBeenCalled()
expect(result.errors).toEqual({})
expect(result.projectInfo).toBeTruthy()

exitSpy.mockRestore()
})

it("should error on missing framework when no existingConfig is provided", async () => {
await fs.writeJson(path.join(tmpDir, "package.json"), {
name: "@acme/ui",
dependencies: {
tailwindcss: "^4.0.0",
},
})
await fs.writeFile(
path.join(tmpDir, "styles.css"),
`@import "tailwindcss";\n`
)
await fs.writeJson(path.join(tmpDir, "tsconfig.json"), {
compilerOptions: {
paths: {
"@acme/ui/*": ["./src/*"],
},
},
})

const exitSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never)

await preFlightInit({
cwd: tmpDir,
yes: true,
defaults: false,
force: true,
silent: true,
isNewProject: false,
cssVariables: true,
installStyleIndex: true,
})

expect(exitSpy).toHaveBeenCalledWith(1)

exitSpy.mockRestore()
})
})
34 changes: 24 additions & 10 deletions packages/shadcn/src/preflights/preflight-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,17 @@ export async function preFlightInit(
const projectInfo = await getProjectInfo(options.cwd, {
configCssFile: typeof tailwind?.css === "string" ? tailwind.css : undefined,
})
if (!projectInfo || projectInfo?.framework.name === "manual") {
// When an existing components.json is already present (re-init or the
// `apply` command), the project has been configured for shadcn. This is
// common for UI-library workspaces in monorepos (e.g. `packages/ui`) which
// don't ship their own framework config. Trust the existing config and
// skip the framework check rather than failing with
// UNSUPPORTED_FRAMEWORK.
const hasExistingConfig = Boolean(options.existingConfig)
if (
!hasExistingConfig &&
(!projectInfo || projectInfo?.framework.name === "manual")
) {
errors[ERRORS.UNSUPPORTED_FRAMEWORK] = true
frameworkSpinner?.fail()

Expand Down Expand Up @@ -93,15 +103,19 @@ export async function preFlightInit(
logger.break()
process.exit(1)
}
frameworkSpinner?.succeed(
`Verifying framework. Found ${highlighter.info(
projectInfo.framework.label
)}.`
)
if (projectInfo && projectInfo.framework.name !== "manual") {
frameworkSpinner?.succeed(
`Verifying framework. Found ${highlighter.info(
projectInfo.framework.label
)}.`
)
} else {
frameworkSpinner?.succeed(`Verifying framework.`)
}

let tailwindSpinnerMessage = "Validating Tailwind CSS."

if (projectInfo.tailwindVersion === "v4") {
if (projectInfo?.tailwindVersion === "v4") {
tailwindSpinnerMessage = `Validating Tailwind CSS. Found ${highlighter.info(
"v4"
)}.`
Expand All @@ -111,18 +125,18 @@ export async function preFlightInit(
silent: options.silent,
}).start()
if (
projectInfo.tailwindVersion === "v3" &&
projectInfo?.tailwindVersion === "v3" &&
(!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile)
) {
errors[ERRORS.TAILWIND_NOT_CONFIGURED] = true
tailwindSpinner?.fail()
} else if (
projectInfo.tailwindVersion === "v4" &&
projectInfo?.tailwindVersion === "v4" &&
!projectInfo?.tailwindCssFile
) {
errors[ERRORS.TAILWIND_NOT_CONFIGURED] = true
tailwindSpinner?.fail()
} else if (!projectInfo.tailwindVersion) {
} else if (!projectInfo?.tailwindVersion) {
errors[ERRORS.TAILWIND_NOT_CONFIGURED] = true
tailwindSpinner?.fail()
} else {
Expand Down