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