Skip to content

Commit 2c164b0

Browse files
authored
feat(shadcn): update registry dependencies resolution algorithm (#7948)
* feat(shadcn): update dependency resolution algorithm * feat(shadcn): rename style to base-style * feat(shadcn): init from namespaced * fix(shadcn): force validation early * chore: changeset * fix(shadcn): headers * fix: smh * fix(shadcn): restore backup on exit and error
1 parent 578f83c commit 2c164b0

16 files changed

Lines changed: 1511 additions & 198 deletions

File tree

.changeset/rare-sloths-run.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": minor
3+
---
4+
5+
update registry dependencies resolution algorithm

packages/shadcn/src/commands/add.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const add = new Command()
170170
isNewProject: false,
171171
srcDir: options.srcDir,
172172
cssVariables: options.cssVariables,
173-
style: "index",
173+
baseStyle: true,
174174
})
175175
}
176176

@@ -202,7 +202,7 @@ export const add = new Command()
202202
isNewProject: true,
203203
srcDir: options.srcDir,
204204
cssVariables: options.cssVariables,
205-
style: "index",
205+
baseStyle: true,
206206
})
207207

208208
shouldUpdateAppIndex =

packages/shadcn/src/commands/init.ts

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { promises as fs } from "fs"
22
import path from "path"
33
import { preFlightInit } from "@/src/preflights/preflight-init"
4+
import { buildUrlAndHeadersForRegistryItem } from "@/src/registry"
45
import {
56
BASE_COLORS,
67
getRegistryBaseColors,
@@ -9,11 +10,16 @@ import {
910
} from "@/src/registry/api"
1011
import { clearRegistryContext } from "@/src/registry/context"
1112
import { rawConfigSchema } from "@/src/registry/schema"
12-
import { isLocalFile, isUrl } from "@/src/registry/utils"
1313
import { addComponents } from "@/src/utils/add-components"
1414
import { TEMPLATES, createProject } from "@/src/utils/create-project"
1515
import { loadEnvFiles } from "@/src/utils/env-loader"
1616
import * as ERRORS from "@/src/utils/errors"
17+
import {
18+
FILE_BACKUP_SUFFIX,
19+
createFileBackup,
20+
deleteFileBackup,
21+
restoreFileBackup,
22+
} from "@/src/utils/file-helper"
1723
import {
1824
DEFAULT_COMPONENTS,
1925
DEFAULT_TAILWIND_CONFIG,
@@ -34,9 +40,23 @@ import { logger } from "@/src/utils/logger"
3440
import { spinner } from "@/src/utils/spinner"
3541
import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content"
3642
import { Command } from "commander"
43+
import deepmerge from "deepmerge"
44+
import fsExtra from "fs-extra"
3745
import prompts from "prompts"
3846
import { z } from "zod"
3947

48+
process.on("exit", (code) => {
49+
const filePath = path.resolve(process.cwd(), "components.json")
50+
51+
// Delete backup if successful.
52+
if (code === 0) {
53+
return deleteFileBackup(filePath)
54+
}
55+
56+
// Restore backup if error.
57+
return restoreFileBackup(filePath)
58+
})
59+
4060
export const initOptionsSchema = z.object({
4161
cwd: z.string(),
4262
components: z.array(z.string()).optional(),
@@ -78,7 +98,7 @@ export const initOptionsSchema = z.object({
7898
).join("', '")}'`,
7999
}
80100
),
81-
style: z.string(),
101+
baseStyle: z.boolean(),
82102
})
83103

84104
export const init = new Command()
@@ -114,34 +134,69 @@ export const init = new Command()
114134
)
115135
.option("--css-variables", "use css variables for theming.", true)
116136
.option("--no-css-variables", "do not use css variables for theming.")
137+
.option("--no-base-style", "do not install the base shadcn style.")
117138
.action(async (components, opts) => {
118139
try {
119140
const options = initOptionsSchema.parse({
120141
cwd: path.resolve(opts.cwd),
121142
isNewProject: false,
122143
components,
123-
style: "index",
124144
...opts,
125145
})
126146

127147
await loadEnvFiles(options.cwd)
128148

129-
const config = await getConfig(options.cwd)
130-
131149
// We need to check if we're initializing with a new style.
132-
// We fetch the payload of the first item.
133-
// This is okay since the request is cached and deduped.
150+
// This will allow us to determine if we need to install the base style.
151+
// And if we should prompt the user for a base color.
134152
if (components.length > 0) {
135-
const item = await getRegistryItem(components[0], config || undefined)
153+
// We don't know the full config at this point.
154+
// So we'll use a shadow config to fetch the first item.
155+
let shadowConfig: Parameters<typeof getRegistryItem>[1] = {
156+
style: "new-york",
157+
resolvedPaths: {
158+
cwd: "",
159+
},
160+
}
161+
162+
// Check if there's a components.json file.
163+
// If so, we'll merge with our shadow config.
164+
const componentsJsonPath = path.resolve(options.cwd, "components.json")
165+
if (fsExtra.existsSync(componentsJsonPath)) {
166+
const existingConfig = await fsExtra.readJson(componentsJsonPath)
167+
const config = rawConfigSchema.partial().parse(existingConfig)
168+
shadowConfig = {
169+
...shadowConfig,
170+
...config,
171+
}
172+
173+
// Since components.json might not be valid at this point.
174+
// Temporarily rename components.json to allow preflight to run.
175+
// We'll rename it back after preflight.
176+
createFileBackup(componentsJsonPath)
177+
}
178+
179+
// This forces a shadowConfig validation early in the process.
180+
buildUrlAndHeadersForRegistryItem(components[0], shadowConfig)
136181

137-
// Skip base color if style.
138-
// We set a default and let the style override it.
182+
const item = await getRegistryItem(components[0], shadowConfig)
139183
if (item?.type === "registry:style") {
184+
// Set a default base color so we're not prompted.
185+
// The style will extend or override it.
140186
options.baseColor = "neutral"
141-
options.style = item.extends ?? "index"
187+
188+
// If the style extends none, we don't want to install the base style.
189+
options.baseStyle =
190+
item.extends === "none" ? false : options.baseStyle
142191
}
143192
}
144193

194+
// If --no-base-style, we don't want to prompt for a base color either.
195+
// The style will extend or override it.
196+
if (!options.baseStyle) {
197+
options.baseColor = "neutral"
198+
}
199+
145200
await runInit(options)
146201

147202
logger.log(
@@ -187,7 +242,8 @@ export async function runInit(
187242
}
188243

189244
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
190-
const config = projectConfig
245+
246+
let config = projectConfig
191247
? await promptForMinimalConfig(projectConfig, options)
192248
: await promptForConfig(await getConfig(options.cwd))
193249

@@ -206,23 +262,38 @@ export async function runInit(
206262
}
207263
}
208264

209-
// Write components.json.
210265
const componentSpinner = spinner(`Writing components.json.`).start()
211266
const targetPath = path.resolve(options.cwd, "components.json")
212-
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8")
267+
const backupPath = `${targetPath}${FILE_BACKUP_SUFFIX}`
268+
269+
// Merge with backup config if it exists and not using --force
270+
if (!options.force && fsExtra.existsSync(backupPath)) {
271+
const existingConfig = await fsExtra.readJson(backupPath)
272+
273+
// Move registries at the end of the config.
274+
const { registries, ...merged } = deepmerge(existingConfig, config)
275+
config = { ...merged, registries }
276+
}
277+
278+
// Write components.json.
279+
await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
213280
componentSpinner.succeed()
214281

215282
// Add components.
216283
const fullConfig = await resolveConfigPaths(options.cwd, config)
217284
const components = [
218-
...(options.style === "none" ? [] : [options.style]),
285+
// "index" is the default shadcn style.
286+
// Why index? Because when style is true, we read style from components.json and fetch that.
287+
// i.e new-york from components.json then fetch /styles/new-york/index.
288+
// TODO: Fix this so that we can extend any style i.e --style=new-york.
289+
...(options.baseStyle ? ["index"] : []),
219290
...(options.components ?? []),
220291
]
221292
await addComponents(components, fullConfig, {
222293
// Init will always overwrite files.
223294
overwrite: true,
224295
silent: options.silent,
225-
style: options.style,
296+
baseStyle: options.baseStyle,
226297
isNewProject:
227298
options.isNewProject || projectInfo?.framework.name === "next-app",
228299
})

0 commit comments

Comments
 (0)