shadcn-ui/ui codebase analysis: How does shadcn-ui CLI work? — Part 2.2
Tags: javascript, opensource, shadcnui, nextjs
Published: 2024-06-28 20:54:39

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](https://github.com/shadcn-ui/ui/blob/main/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

![](https://media.licdn.com/dms/image/D4E12AQHzJOHlhQt2MA/article-inline_image-shrink_1500_2232/0/1719607650873?e=1724889600&v=beta&t=_n_IDwlAfr508mnlpEx0HRiLDHOlFYz0oaNuFVDA_CM)

getProjectConfig
----------------

getProjectConfig is imported from [utils/get-project-info](https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-project-info.ts#L73).

```js
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

```js
const existingConfig = await getConfig(cwd)
if (existingConfig) {
  return existingConfig
}
```

getConfig is imported from a different file named [get-config](https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L55). 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.

```js
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

```js
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).

```js
// 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](https://www.npmjs.com/package/cosmiconfig#explorersearch) Searches for a configuration file. Returns a Promise that resolves with a [result](https://www.npmjs.com/package/cosmiconfig#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:

1.  A goldengrahams property in a package.json file.
2.  A .goldengrahamsrc file with JSON or YAML syntax.
3.  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"](https://www.npmjs.com/package/cosmiconfig#loading-js-modules).)
4.  A goldengrahamsrc, goldengrahamsrc.json, goldengrahamsrc.yaml, goldengrahamsrc.yml, goldengrahamsrc.js, goldengrahamsrc.ts, goldengrahamsrc.mjs, or goldengrahamsrc.cjs file in the .config subdirectory.
5.  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"](https://www.npmjs.com/package/cosmiconfig#loading-js-modules).)

Read more about [explorer.search](https://www.npmjs.com/package/cosmiconfig#explorersearch).

So what is it shadcn-ui/ui searching for? the answer lies in the below code:

```js
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?

```js
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!

```js
 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.

```js
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).

```js
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_](https://tthroo.com/)

About me:
---------

Website: [https://ramunarasinga.com/](https://ramunarasinga.com/)

Linkedin: [https://www.linkedin.com/in/ramu-narasinga-189361128/](https://www.linkedin.com/in/ramu-narasinga-189361128/)

Github: [https://github.com/Ramu-Narasinga](https://github.com/Ramu-Narasinga)

Email: ramu.narasinga@gmail.com

[Build shadcn-ui/ui from scratch](https://tthroo.com/)

References:
-----------

1.  [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/commands/init.ts#L69C7-L69C56)
2.  [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-project-info.ts#L73)
3.  [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#L55)
4.  [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#L91)
5.  [https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L16](https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L16)