shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 2

ramunarasinga

Ramu Narasinga

Posted on June 18, 2024

shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 2

In this article, I discuss how Blocks page is built on ui.shadcn.com. Blocks page has a lot of utilities used, hence I broke down this Blocks page analysis into 5 parts.

  1. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 1
  2. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 2
  3. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 3 (Coming soon)
  4. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 4 (Coming soon)
  5. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 5 (Coming soon)

In part 1, we looked at two important modular functions named getAllBlockIds and _getAllBlocks.

In part 2, we will look at the following:

  1. Where is BlockDisplay used?
  2. Where to find BlockDisplay component?
  3. BlockDisplay component explained
  4. getBlock function

Where is BlockDisplay used?

BlockDisplay is used in blocks/page.tsx. In part 1, I explained the code behind fetching blocks; When you visit blocks page on ui.shadcn.com, you will find a lot of blocks rendered, it is in fact from blocks array above. These blocks so far do not contain the code that translates to a components used in an individual block. BlockDisplay has it own logic to deal with rendering components for each block. Important concept here to remember is to pass the minimal information required.

Where to find BlockDisplay component?

You can find BlockDisplay component exported from components/block-display.tsx.

Has about 29 lines of code.

BlockDisplay component explained

Let’s try to understand the BlockDisplay code at a high level.

import { getBlock } from "@/lib/blocks"
import { BlockPreview } from "@/components/block-preview"
import { styles } from "@/registry/styles"

export async function BlockDisplay({ name }: { name: string }) {
  const blocks = await Promise.all(
    styles.map(async (style) => {
      const block = await getBlock(name, style.name)
      const hasLiftMode = block?.chunks ? block?.chunks?.length > 0 : false

      // Cannot (and don't need to) pass to the client.
      delete block?.component
      delete block?.chunks

      return {
        ...block,
        hasLiftMode,
      }
    })
  )

  if (!blocks?.length) {
    return null
  }

  return blocks.map((block) => (
    <BlockPreview key={\`${block.style}-${block.name}\`} block={block} />
  ))
}
Enter fullscreen mode Exit fullscreen mode

There is a blocks array that is populated after Promise.all resolves. Promise.all has an array of styles imported from registry/styles. These styles are mapped over and each style is further processed.

getBlock is a utility function that accepts two parameters, name and style.name and there is also a flag named hasLiftMode that is based on block.chunks.length. block.components and block.chunks are deleted as they are not required on the client side.

These blocks are then mapped over and each block is used in BlockPreview.

getBlock function

getBlock function is imported lib/blocks.ts and contains the code below:

export async function getBlock(
  name: string,
  style: Style\["name"\] = DEFAULT\_BLOCKS\_STYLE
) {
  const entry = Index\[style\]\[name\]

  const content = await \_getBlockContent(name, style)

  const chunks = await Promise.all(
    entry.chunks?.map(async (chunk: BlockChunk) => {
      const code = await readFile(chunk.file)

      const tempFile = await createTempSourceFile(\`${chunk.name}.tsx\`)
      const sourceFile = project.createSourceFile(tempFile, code, {
        scriptKind: ScriptKind.TSX,
      })

      sourceFile
        .getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
        .filter((node) => {
          return node.getAttribute("x-chunk") !== undefined
        })
        ?.map((component) => {
          component
            .getAttribute("x-chunk")
            ?.asKind(SyntaxKind.JsxAttribute)
            ?.remove()
        })

      return {
        ...chunk,
        code: sourceFile
          .getText()
          .replaceAll(\`@/registry/${style}/\`, "@/components/"),
      }
    })
  )

  return blockSchema.parse({
    style,
    highlightedCode: content.code ? await highlightCode(content.code) : "",
    ...entry,
    ...content,
    chunks,
    type: "components:block",
  })
}
Enter fullscreen mode Exit fullscreen mode

As you can see, there is a lot going on here. We will cover some part of this code in this article and the rest in the coming articles.

const entry = Index\[style\]\[name\]
const content = await \_getBlockContent(name, style)
Enter fullscreen mode Exit fullscreen mode

Index is imported from _registry_ and is autogenerated by scripts/build-registry.ts (more on this later).

getBlockContent returns an object that is assigned to content. The below code is picked from getBlockContent.

async function \_getBlockContent(name: string, style: Style\["name"\]) {
  const raw = await \_getBlockCode(name, style)
Enter fullscreen mode Exit fullscreen mode

as you can this, in turn, calls _getBlockCode.

_getBlockCode has the code

async function \_getBlockCode(
  name: string,
  style: Style\["name"\] = DEFAULT\_BLOCKS\_STYLE
) {
  const entry = Index\[style\]\[name\]
  const block = registryEntrySchema.parse(entry)

  if (!block.source) {
    return ""
  }

  return await readFile(block.source)
}
Enter fullscreen mode Exit fullscreen mode

I have talked about Index, registryEntrySchema and parse in great detail in part 1. we did not understand the readFile function yet.

readFile

readFile function is used to read file content from block.source

async function readFile(source: string) {
  const filepath = path.join(process.cwd(), source)
  return await fs.readFile(filepath, "utf-8")
}
Enter fullscreen mode Exit fullscreen mode

Index contains blocks that do not have source, but at the end, you will find some blocks with source as shown below

For example, for authentication-04, code is available at registry/new-york/block/authentication-04.tsx.

What you see above as one of the blocks shown on blocks page on ui.shadcn.com is the code from registry/new-york/block/authentication-04.tsx. Isn’t this awesome!?

Let’s take a step back now and get back on track with out function call stack. We got to readFile from _getBlockCode. With this so far, we have understood the _getBlockCode. We got to _getBlockCode from __getBlockContent and only covered the first line as shown below

_getBlockContent

async function \_getBlockContent(name: string, style: Style\["name"\]) {
  const raw = await \_getBlockCode(name, style)

  const tempFile = await createTempSourceFile(\`${name}.tsx\`)
  const sourceFile = project.createSourceFile(tempFile, raw, {
    scriptKind: ScriptKind.TSX,
  })

  // Extract meta.
  const description = \_extractVariable(sourceFile, "description")
  const iframeHeight = \_extractVariable(sourceFile, "iframeHeight")
  const containerClassName = \_extractVariable(sourceFile, "containerClassName")

  // Format the code.
  let code = sourceFile.getText()
  code = code.replaceAll(\`@/registry/${style}/\`, "@/components/")
  code = code.replaceAll("export default", "export")

  return {
    description,
    code,
    container: {
      height: iframeHeight,
      className: containerClassName,
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

In part 3, I will explain how createTempSourceFile, createSourceFile and extractVariable work in order to understand getBlockCode completely. Keep in mind, we still need to get back to getBlock since this is used in BlockDisplay . Did you notice the chain of function calls here? functions following single responsibility principle and self explanatory and modular.

Conclusion

In part 2, I discuss the BlockDisplay component. This component has functions that are chained together so well that it respects the SRP and is quite module.

I will highlight the key function in this chain. It goes like this:

BlockDisplay => getBlock => _getBlockContent => readFile

readFile is where you will find the code that reads the source code available at registry/ that loads the “blocks” and is rendered via an iframe. Jesus! I wasn’t expecting magic of this sort.

So far, this only completes getBlockContent explanation, I still need to get back to getBlock and BlockDisplay which has its own complex functions such as createTempSourceFile_,_ createSourceFile and extractVariable. I have 0 clue as how they work, but I will make a good attempt to understand and explain them in an easy way in the next article.

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/apps/www/components/block-display.tsx#L5
  2. https://github.com/shadcn-ui/ui/blob/main/apps/www/lib/blocks.ts#L27
💖 💪 🙅 🚩
ramunarasinga
Ramu Narasinga

Posted on June 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related