Skip to content

Commit 026f2cc

Browse files
committed
fix(shadcn): apply for monorepo
1 parent ba10089 commit 026f2cc

6 files changed

Lines changed: 291 additions & 32 deletions

File tree

.changeset/thirty-aliens-live.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 apply in monorepo

packages/shadcn/src/commands/apply.ts

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ import { isUrl } from "@/src/registry/utils"
1616
import { getTemplateForFramework } from "@/src/templates/index"
1717
import { loadEnvFiles } from "@/src/utils/env-loader"
1818
import * as ERRORS from "@/src/utils/errors"
19-
import { withFileBackup } from "@/src/utils/file-helper"
20-
import { getBase } from "@/src/utils/get-config"
19+
import { FileBackupError, withFileBackup } from "@/src/utils/file-helper"
20+
import {
21+
getBase,
22+
getWorkspaceConfig,
23+
type Config,
24+
} from "@/src/utils/get-config"
2125
import {
2226
getProjectComponents,
2327
getProjectInfo,
@@ -26,6 +30,7 @@ import { handleError } from "@/src/utils/handle-error"
2630
import { highlighter } from "@/src/utils/highlighter"
2731
import { logger } from "@/src/utils/logger"
2832
import { Command } from "commander"
33+
import fs from "fs-extra"
2934
import prompts from "prompts"
3035
import { z } from "zod"
3136

@@ -209,7 +214,7 @@ export const apply = new Command()
209214
only,
210215
})
211216

212-
await runInit({
217+
const config = await runInit({
213218
cwd: options.cwd,
214219
yes: true,
215220
force: false,
@@ -223,15 +228,8 @@ export const apply = new Command()
223228
existingConfig,
224229
components: [cleanUrl, ...reinstallComponents],
225230
})
226-
},
227-
{
228-
onBackupFailure: () => {
229-
logger.error(
230-
`Could not back up ${highlighter.info(
231-
"components.json"
232-
)}. Aborting.`
233-
)
234-
},
231+
232+
await syncApplyWorkspaceConfigs(config, { only })
235233
}
236234
)
237235

@@ -247,6 +245,14 @@ export const apply = new Command()
247245
process.exit(1)
248246
}
249247

248+
if (error instanceof FileBackupError) {
249+
logger.error(
250+
`Could not back up ${highlighter.info("components.json")}. Aborting.`
251+
)
252+
logger.break()
253+
process.exit(1)
254+
}
255+
250256
logger.break()
251257
handleError(error)
252258
} finally {
@@ -406,6 +412,79 @@ async function resolveApplyTemplate(cwd: string) {
406412
return getTemplateForFramework(projectInfo?.framework.name)
407413
}
408414

415+
async function syncApplyWorkspaceConfigs(
416+
config: Config,
417+
options?: {
418+
only?: string[]
419+
}
420+
) {
421+
if (options?.only && !options.only.includes("theme")) {
422+
return
423+
}
424+
425+
const linkedConfigs = await getApplyWorkspaceConfigs(config)
426+
if (!linkedConfigs.length) {
427+
return
428+
}
429+
430+
const patch = {
431+
style: config.style,
432+
tailwind: {
433+
baseColor: config.tailwind.baseColor,
434+
cssVariables: config.tailwind.cssVariables,
435+
},
436+
...(config.iconLibrary ? { iconLibrary: config.iconLibrary } : {}),
437+
...(config.rtl !== undefined ? { rtl: config.rtl } : {}),
438+
...(config.menuColor ? { menuColor: config.menuColor } : {}),
439+
...(config.menuAccent ? { menuAccent: config.menuAccent } : {}),
440+
}
441+
442+
for (const linkedConfig of linkedConfigs) {
443+
const configPath = path.resolve(
444+
linkedConfig.resolvedPaths.cwd,
445+
"components.json"
446+
)
447+
if (!(await fs.pathExists(configPath))) {
448+
continue
449+
}
450+
451+
const existingConfig = await fs.readJson(configPath)
452+
await fs.writeJson(
453+
configPath,
454+
{
455+
...existingConfig,
456+
...patch,
457+
tailwind: {
458+
...existingConfig.tailwind,
459+
...patch.tailwind,
460+
},
461+
},
462+
{ spaces: 2 }
463+
)
464+
}
465+
}
466+
467+
async function getApplyWorkspaceConfigs(config: Config) {
468+
const workspaceConfig = await getWorkspaceConfig(config)
469+
if (!workspaceConfig) {
470+
return []
471+
}
472+
473+
const linkedConfigs = new Map<string, Config>()
474+
475+
for (const linkedConfig of Object.values(workspaceConfig)) {
476+
if (linkedConfig.resolvedPaths.cwd === config.resolvedPaths.cwd) {
477+
continue
478+
}
479+
480+
linkedConfigs.set(linkedConfig.resolvedPaths.cwd, linkedConfig)
481+
}
482+
483+
return Array.from(linkedConfigs.values()).sort((a, b) =>
484+
a.resolvedPaths.cwd.localeCompare(b.resolvedPaths.cwd)
485+
)
486+
}
487+
409488
export function resolveApplyInitUrl(
410489
preset: string,
411490
currentBase: "radix" | "base",

packages/shadcn/src/preflights/preflight-apply.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getMonorepoTargets,
88
isMonorepoRoot,
99
} from "@/src/utils/get-monorepo-info"
10+
import { getProjectInfo } from "@/src/utils/get-project-info"
1011
import { highlighter } from "@/src/utils/highlighter"
1112
import { logger } from "@/src/utils/logger"
1213
import fs from "fs-extra"
@@ -28,8 +29,19 @@ export async function preFlightApply(options: { cwd: string }) {
2829
if (!fs.existsSync(path.resolve(options.cwd, "components.json"))) {
2930
if (await isMonorepoRoot(options.cwd)) {
3031
const targets = await getMonorepoTargets(options.cwd)
31-
if (targets.length > 0) {
32-
formatMonorepoMessage("apply --preset <preset>", targets, {
32+
const applyTargets = []
33+
34+
for (const target of targets) {
35+
const projectInfo = await getProjectInfo(
36+
path.resolve(options.cwd, target.name)
37+
)
38+
if (projectInfo?.framework && projectInfo.framework.name !== "manual") {
39+
applyTargets.push(target)
40+
}
41+
}
42+
43+
if (applyTargets.length > 0) {
44+
formatMonorepoMessage("apply --preset <preset>", applyTargets, {
3345
cwdFlag: "-c",
3446
})
3547
process.exit(1)

packages/shadcn/src/utils/file-helper.test.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import path from "path"
33
import fs from "fs-extra"
44
import { afterEach, describe, expect, it, vi } from "vitest"
55

6-
import { FILE_BACKUP_SUFFIX, withFileBackup } from "./file-helper"
6+
import {
7+
FILE_BACKUP_SUFFIX,
8+
FileBackupError,
9+
withFileBackup,
10+
} from "./file-helper"
711

812
const tempDirs: string[] = []
913

@@ -18,6 +22,7 @@ async function createTempFile() {
1822
}
1923

2024
afterEach(async () => {
25+
vi.restoreAllMocks()
2126
await Promise.all(tempDirs.splice(0).map((dir) => fs.remove(dir)))
2227
})
2328

@@ -49,22 +54,21 @@ describe("withFileBackup", () => {
4954

5055
it("should abort when backup creation fails", async () => {
5156
const filePath = await createTempFile()
52-
const consoleErrorSpy = vi
53-
.spyOn(console, "error")
54-
.mockImplementation(() => {})
57+
const task = vi.fn(async () => {
58+
await fs.writeFile(filePath, '{"style":"after"}\n', "utf8")
59+
})
5560
const renameSyncSpy = vi.spyOn(fs, "renameSync").mockImplementation(() => {
5661
throw new Error("boom")
5762
})
5863

59-
await expect(
60-
withFileBackup(filePath, async () => {
61-
await fs.writeFile(filePath, '{"style":"after"}\n', "utf8")
62-
})
63-
).rejects.toThrow(`Could not back up ${filePath}.`)
64+
await expect(withFileBackup(filePath, task)).rejects.toThrow(
65+
FileBackupError
66+
)
6467

68+
expect(task).not.toHaveBeenCalled()
6569
expect(await fs.readFile(filePath, "utf8")).toBe('{"style":"before"}\n')
70+
expect(await fs.pathExists(`${filePath}${FILE_BACKUP_SUFFIX}`)).toBe(false)
6671

6772
renameSyncSpy.mockRestore()
68-
consoleErrorSpy.mockRestore()
6973
})
7074
})

packages/shadcn/src/utils/file-helper.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import fsExtra from "fs-extra"
22

33
export const FILE_BACKUP_SUFFIX = ".bak"
44

5-
type WithFileBackupOptions = {
6-
onBackupFailure?: (filePath: string) => void
5+
export class FileBackupError extends Error {
6+
filePath: string
7+
8+
constructor(filePath: string) {
9+
super(`Could not back up ${filePath}.`)
10+
this.name = "FileBackupError"
11+
this.filePath = filePath
12+
}
713
}
814

915
export function createFileBackup(filePath: string): string | null {
@@ -15,8 +21,7 @@ export function createFileBackup(filePath: string): string | null {
1521
try {
1622
fsExtra.renameSync(filePath, backupPath)
1723
return backupPath
18-
} catch (error) {
19-
console.error(`Failed to create backup of ${filePath}: ${error}`)
24+
} catch {
2025
return null
2126
}
2227
}
@@ -57,8 +62,7 @@ export function deleteFileBackup(filePath: string): boolean {
5762

5863
export async function withFileBackup<T>(
5964
filePath: string,
60-
task: () => Promise<T>,
61-
options: WithFileBackupOptions = {}
65+
task: () => Promise<T>
6266
) {
6367
if (!fsExtra.existsSync(filePath)) {
6468
return task()
@@ -67,8 +71,7 @@ export async function withFileBackup<T>(
6771
const backupPath = createFileBackup(filePath)
6872

6973
if (!backupPath) {
70-
options.onBackupFailure?.(filePath)
71-
throw new Error(`Could not back up ${filePath}.`)
74+
throw new FileBackupError(filePath)
7275
}
7376

7477
const restoreBackupOnExit = () => restoreFileBackup(filePath)

0 commit comments

Comments
 (0)