I wanted to find out how shadcn-ui CLI works. In this article, I discuss the code used to build the shadcn-ui/ui CLI.
In part 1.0 and part 1.1, I discussed the code written in packages/cli/src/index.ts. In part 2.0, I talked about how the commander.js is used along with zod to parse the CLI argument passed. In Part 2.1, looked at a function named preFlight and a package named fast-glob. In part 2.2, we will look at few more lines of code.
There’s few side effects in getProjectConfig
getProjectConfig
getProjectConfig is imported from utils/get-project-info.
export async function getProjectConfig(cwd: string): Promise<Config | null> {
// Check for existing component config.
const existingConfig = await getConfig(cwd)
if (existingConfig) {
return existingConfig
}
const projectType = await getProjectType(cwd)
const tailwindCssFile = await getTailwindCssFile(cwd)
const tsConfigAliasPrefix = await getTsConfigAliasPrefix(cwd)
if (!projectType || !tailwindCssFile || !tsConfigAliasPrefix) {
return null
}
const isTsx = await isTypeScriptProject(cwd)
const config: RawConfig = {
$schema: "https://ui.shadcn.com/schema.json",
rsc: \["next-app", "next-app-src"\].includes(projectType),
tsx: isTsx,
style: "new-york",
tailwind: {
config: isTsx ? "tailwind.config.ts" : "tailwind.config.js",
baseColor: "zinc",
css: tailwindCssFile,
cssVariables: true,
prefix: "",
},
aliases: {
utils: \`${tsConfigAliasPrefix}/lib/utils\`,
components: \`${tsConfigAliasPrefix}/components\`,
},
}
return await resolveConfigPaths(cwd, config)
}
let’s begin our analysis with getConfig.
getConfig
const existingConfig = await getConfig(cwd)
if (existingConfig) {
return existingConfig
}
getConfig is imported from a different file named get-config. Reason behind this could be that context matters when it comes where you place your function. For example, logically, a function named getConfig can never be placed in a file named get-project-info.
export async function getConfig(cwd: string) {
const config = await getRawConfig(cwd)
if (!config) {
return null
}
return await resolveConfigPaths(cwd, config)
}
This function calls another function named getRawConfig.
Let’s jump into analysing getRawConfig, we still have to come back to this function, we are just following along the functions in callstack.
getRawConfig
export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
try {
const configResult = await explorer.search(cwd)
if (!configResult) {
return null
}
return rawConfigSchema.parse(configResult.config)
} catch (error) {
throw new Error(\`Invalid configuration found in ${cwd}/components.json.\`)
}
getRawConfig makes another call to explorer.search(cwd). Let’s find out what’s explorer first.
explorer variable is initalised at Line 16 in utils/get-config.ts.
// https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L16
// TODO: Figure out if we want to support all cosmiconfig formats.
// A simple components.json file would be nice.
const explorer = cosmiconfig("components", {
searchPlaces: \["components.json"\],
})
cosmicconfig
explorer.search Searches for a configuration file. Returns a Promise that resolves with a result or with null, if no configuration file is found.
You can do the same thing synchronously with explorerSync.search().
Let’s say your module name is goldengrahams so you initialized with const explorer = cosmiconfig('goldengrahams');. Here's how your default search() will work:
- Starting from process.cwd() (or some other directory defined by the searchFrom argument to search()), look for configuration objects in the following places:
- A goldengrahams property in a package.json file.
- A .goldengrahamsrc file with JSON or YAML syntax.
- A .goldengrahamsrc.json, .goldengrahamsrc.yaml, .goldengrahamsrc.yml, .goldengrahamsrc.js, .goldengrahamsrc.ts, .goldengrahamsrc.mjs, or .goldengrahamsrc.cjs file. (To learn more about how JS files are loaded, see "Loading JS modules".)
- A goldengrahamsrc, goldengrahamsrc.json, goldengrahamsrc.yaml, goldengrahamsrc.yml, goldengrahamsrc.js, goldengrahamsrc.ts, goldengrahamsrc.mjs, or goldengrahamsrc.cjs file in the .config subdirectory.
- A goldengrahams.config.js, goldengrahams.config.ts, goldengrahams.config.mjs, or goldengrahams.config.cjs file. (To learn more about how JS files are loaded, see "Loading JS modules".)
Read more about explorer.search.
So what is it shadcn-ui/ui searching for? the answer lies in the below code:
const configResult = await explorer.search(cwd)
if (!configResult) {
return null
}
return rawConfigSchema.parse(configResult.config)
Turns out, explorer.search(cwd) is searching for components.json. Hang on, how exactly search function knows the module name?
const explorer = cosmiconfig("components", {
searchPlaces: \["components.json"\],
})
When we set the cosmicconfig with “components”, we are setting the moduleName to “components” which means explorer.search looks for a file named components.json in a given directory. Brilliant!
return rawConfigSchema.parse(configResult.config)
} catch (error) {
throw new Error(\`Invalid configuration found in ${cwd}/components.json.\`)
}
configResult from cosmic search is parsed against rawConfigSchema.
export const rawConfigSchema = z
.object({
$schema: z.string().optional(),
style: z.string(),
rsc: z.coerce.boolean().default(false),
tsx: z.coerce.boolean().default(true),
tailwind: z.object({
config: z.string(),
css: z.string(),
baseColor: z.string(),
cssVariables: z.boolean().default(true),
prefix: z.string().default("").optional(),
}),
aliases: z.object({
components: z.string(),
utils: z.string(),
ui: z.string().optional(),
}),
})
.strict()
and if there is an error, this means components.json is not configured correctly.
Conclusion:
In this article, I was following along the call stack when the function getProjectConfig is called as this function has a bunch of calls to other functions that are placed logically in files (contextually). What I found inspiring was the usage of cosmicconfig, I have never come across this package but, boi does it have 54M downloads per week. It now makes sense how shadcn-ui/ui gets the config information from components.json (you will know this if you have used shadcn-ui/ui CLI before).
const explorer = cosmiconfig("components", {
searchPlaces: \["components.json"\],
})
// somewhere in getRawConfig file in utils/get-config.ts
const configResult = await explorer.search(cwd)
cosmicconfig searches for specific config in a given directory. In this case, it searches for “components.json”. There was other package named fast-glob that I discussed in part 2.1, fast-glob is a package that provides methods for traversing the file system and returning pathnames that matched a defined set of a specified pattern but this only returns pathnames.
Right, so if you want to get pathnames based on a certain pattern from a file system, use fast-glob. If you want to access certain config in a dir use CosmicConfig because Cosmiconfig will check the current directory for the following:
- a package.json property
- a JSON or YAML, extensionless “rc file”
- an “rc file” with the extensions .json, .yaml, .yml, .js, .ts, .mjs, or .cjs
- any of the above two inside a .config subdirectory
- a .config.js, .config.ts, .config.mjs, or .config.cjs file
Get free courses inspired by the best practices used in open source.
About me:
Website: https://ramunarasinga.com/
Linkedin: https://www.linkedin.com/in/ramu-narasinga-189361128/
Github: https://github.com/Ramu-Narasinga
Email: ramu.narasinga@gmail.com
Learn the best practices used in open source.
References:
- https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/commands/init.ts#L69C7-L69C56
- https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-project-info.ts#L73
- https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L55
- https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L91
- https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L16
Top comments (2)
Hey Deepak, That’s a cool repository. Thanks for sharing.