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.
shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 1
shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 2
shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 3 (Coming soon)
shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 4 (Coming soon)
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:
Where is BlockDisplay used?
Where to find BlockDisplay component?
BlockDisplay component explained
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} />
))
}
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",
})
}
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)
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)
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)
}
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")
}
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,
},
}
}
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.
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