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 2.11, we looked at runInit function and how shadcn-ui/ui ensures directories provided in resolvedPaths in config exist.
The following operations are performed in runInit function:
Ensure all resolved paths directories exist.
Write tailwind config.
Write css file.
Write cn file.
Install dependencies.
Let’s understand how shadcn-ui/ui CLI writes to tailwind config in runInit function.
Write tailwind config
After checking the directories exist, there are few more operations performed before writing tailwind config as shown in the below code.
This code is picked from cli/src/commands/init.ts.
const extension = config.tsx ? "ts" : "js"
const tailwindConfigExtension = path.extname(
config.resolvedPaths.tailwindConfig
)
let tailwindConfigTemplate: string
if (tailwindConfigExtension === ".ts") {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_TS_WITH_VARIABLES
: templates.TAILWIND_CONFIG_TS
} else {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG
}
// Write tailwind config.
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
template(tailwindConfigTemplate)({
extension,
prefix: config.tailwind.prefix,
}),
"utf8"
)
Let’s understand this code snippet
Extension
const extension = config.tsx ? "ts" : "js"
This extension is used in later parts of code that deal with writing cn file.
tailwindConfigExtension
const tailwindConfigExtension = path.extname(
config.resolvedPaths.tailwindConfig
)
The path.extname() method returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last portion of the path. If there is no . in the last portion of the path, or if there are no . characters other than the first character of the basename of path (see path.basename()) , an empty string is returned.
let tailwindConfigTemplate: string
if (tailwindConfigExtension === ".ts") {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_TS_WITH_VARIABLES
: templates.TAILWIND_CONFIG_TS
} else {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG
}
Depending on the tailwindConfigExtension and config.tailwind.cssVariables, tailwindConfigTemplate is set to a value using templates. Templates is imported from utils/templates and contains some variables initialised with values related to tailwind config
Write to tailwind config file
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
template(tailwindConfigTemplate)({
extension,
prefix: config.tailwind.prefix,
}),
"utf8"
)
template used here in the above snippet is different from utils/templates. Different how? this template is imported from lodash.template
Read more about lodash.template
An example using lodash.template:
// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// => 'hello fred!'
This explains why there is %extension% in utils/templates.ts
It gets replaced with w/e is passed. Interesting…
Conclusion:
After checking the directories exist (explained in the part 2.11), there are few more operations performed before writing tailwind config as shown in the below code.
const extension = config.tsx ? "ts" : "js"
const tailwindConfigExtension = path.extname(
config.resolvedPaths.tailwindConfig
)
let tailwindConfigTemplate: string
if (tailwindConfigExtension === ".ts") {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_TS_WITH_VARIABLES
: templates.TAILWIND_CONFIG_TS
} else {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG
}
// Write tailwind config.
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
template(tailwindConfigTemplate)({
extension,
prefix: config.tailwind.prefix,
}),
"utf8"
)
extension — This extension is used in later parts of code that deal with writing cn file.
tailwindConfigExtension — The path.extname() method returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last portion of the path.
Depending on the tailwindConfigExtension and config.tailwind.cssVariables, tailwindConfigTemplate is set to a value using templates. Templates is imported from utils/templates and contains some variables initialised with values related to tailwind config
Write to tailwind config file — using fs.writeFile, tailwindConfig is updated but there is a catch. template from lodash.template is used to perform some replacements before writing to the file.
An example usage of lodash.template from the docs:
// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// => 'hello fred!'
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#L331C3-L356C4
https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L53
https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L43
https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L20
https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/templates.ts