shadcn-ui/ui codebase analysis: How does shadcn-ui CLI work? — Part 2.2
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 thesearchFrom
argument tosearch()
), look for configuration objects in the following places:
- A
goldengrahams
property in apackage.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
, orgoldengrahamsrc.cjs
file in the.config
subdirectory. - A
goldengrahams.config.js
,goldengrahams.config.ts
,goldengrahams.config.mjs
, orgoldengrahams.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
Want to learn how to build shadcn-ui/ui from scratch? Check out build-from-scratch
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
Build shadcn-ui/ui from scratch
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