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

Ramu Narasinga - Jun 21 - - Dev Community

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
  4. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 4
  5. shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 5

This is final part 5 where I will discuss the following:

  1. getBlock function
  2. BlockPreview component
  3. BlockDisplay

getBlock function

getBlock function uses that same function such as readFile, createTempSourceFile and project.createSourceFile. I explained about this in great detail in part 4.

To summarise, project.createSourceFile is an API provided by ts-morph to perform Typescript manipulations such as removing a variable from a file by access Typescript’s AST. This can simplify refactoring as it can save a lot of time in performing repetitive tasks such as renaming a property or function when dealing with Typescript code across a large code base.

// source: https://github.com/shadcn-ui/ui/blob/main/apps/www/lib/blocks.ts#L27
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

This function attempts to remove nodes with “x-chunk” attribute. To my surprise, there’s some block examples that do contain this attribute as shown in the below image

and getBlock function returns the below object to a variable in BlockDisplay component.

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

BlockDisplay Component

Now that we fully understand what happens behind the scenes when you call getBlock as shown in the below code, BlockDisplay uses a Promise.all and waits till it gets all the blocks.

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

and then BlockPreview is used to show a block example.

BlockPreview component

The below code is picked from BlockPreview component

"use client"

import \* as React from "react"
import { ImperativePanelHandle } from "react-resizable-panels"

import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { useLiftMode } from "@/hooks/use-lift-mode"
import { BlockToolbar } from "@/components/block-toolbar"
import { Icons } from "@/components/icons"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/registry/new-york/ui/resizable"
import { Tabs, TabsContent } from "@/registry/new-york/ui/tabs"
import { Block } from "@/registry/schema"

export function BlockPreview({
  block,
}: {
  block: Block & { hasLiftMode: boolean }
}) {
  const \[config\] = useConfig()
  const { isLiftMode } = useLiftMode(block.name)
  const \[isLoading, setIsLoading\] = React.useState(true)
  const ref = React.useRef<ImperativePanelHandle>(null)

  if (config.style !== block.style) {
    return null
  }

  return (
    <Tabs
      id={block.name}
      defaultValue="preview"
      className="relative grid w-full scroll-m-20 gap-4"
      style={
        {
          "--container-height": block.container?.height,
        } as React.CSSProperties
      }
    >
      <BlockToolbar block={block} resizablePanelRef={ref} />
      <TabsContent
        value="preview"
        className="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted"
      >
        <ResizablePanelGroup direction="horizontal" className="relative z-10">
          <ResizablePanel
            ref={ref}
            className={cn(
              "relative rounded-lg border bg-background",
              isLiftMode ? "border-border/50" : "border-border"
            )}
            defaultSize={100}
            minSize={30}
          >
            {isLoading ? (
              <div className="absolute inset-0 z-10 flex h-\[--container-height\] w-full items-center justify-center gap-2 text-sm text-muted-foreground">
                <Icons.spinner className="h-4 w-4 animate-spin" />
                Loading...
              </div>
            ) : null}
            <iframe
              src={\`/blocks/${block.style}/${block.name}\`}
              height={block.container?.height}
              className="chunk-mode relative z-20 w-full bg-background"
              onLoad={() => {
                setIsLoading(false)
              }}
            />
          </ResizablePanel>
          <ResizableHandle
            className={cn(
              "relative hidden w-3 bg-transparent p-0 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-\[6px\] after:-translate-y-1/2 after:translate-x-\[-1px\] after:rounded-full after:bg-border after:transition-all after:hover:h-10 sm:block",
              isLiftMode && "invisible"
            )}
          />
          <ResizablePanel defaultSize={0} minSize={0} />
        </ResizablePanelGroup>
      </TabsContent>
      <TabsContent value="code">
        <div
          data-rehype-pretty-code-fragment
          dangerouslySetInnerHTML={{ \_\_html: block.highlightedCode }}
          className="w-full overflow-hidden rounded-md \[&\_pre\]:my-0 \[&\_pre\]:h-\[--container-height\] \[&\_pre\]:overflow-auto \[&\_pre\]:whitespace-break-spaces \[&\_pre\]:p-6 \[&\_pre\]:font-mono \[&\_pre\]:text-sm \[&\_pre\]:leading-relaxed"
        />
      </TabsContent>
    </Tabs>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, blocks are rendered using an iframe. An example URL is provided in the screenshot below.

Similarly, you can load other blocks by visiting their relevant URL.

Conclusion:

In these 5 parts, I studied the code used in building the Blocks page found on ui.shadcn.com/blocks.

On this side of codebase, I have seen some advanced Typescript patterns such as using Records and parsing objects with zod to make sure they meet certain set schema standards. My favourite was using ts-morph to perform some variable removing operations on the code picked from a file using AST API (that sounds cool, lol) just so the code presented to the “client” component requires what’s needed and nothing more than that.

Frankly speaking, it was not easy to read and understand this code. In my next adventure, I will use this momentum to understand how the shadcn-ui/ui’s CLI package is built and write articles about this CLI package. It will be interesting to find out what happens under the hood when you type npx shadcn-ui add button\ for example.

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

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