DEV Community

Cover image for shadcn-ui/ui codebase analysis: How does shadcn-ui CLI work? — Part 2.2
Ramu Narasinga
Ramu Narasinga

Posted on • Edited on

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)
}
Enter fullscreen mode Exit fullscreen mode

let’s begin our analysis with getConfig.

getConfig

const existingConfig = await getConfig(cwd)
if (existingConfig) {
  return existingConfig
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.\`)
  }
Enter fullscreen mode Exit fullscreen mode

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"\],
})
Enter fullscreen mode Exit fullscreen mode

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:
  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".)
  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".)

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)
Enter fullscreen mode Exit fullscreen mode

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"\],
})
Enter fullscreen mode Exit fullscreen mode

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.\`)
  }
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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
  3. 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
  5. https://github.com/shadcn-ui/ui/blob/main/packages/cli/src/utils/get-config.ts#L16

Top comments (2)

Collapse
 
ramunarasinga profile image
Ramu Narasinga

Hey Deepak, That’s a cool repository. Thanks for sharing.