Files
shadcn-ui/apps/www/lib/blocks.ts
2024-07-06 02:06:40 +04:00

154 lines
3.9 KiB
TypeScript

"use server"
import { promises as fs } from "fs"
import { tmpdir } from "os"
import path from "path"
import { Index } from "@/__registry__"
import { Project, ScriptKind, SourceFile, SyntaxKind } from "ts-morph"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"
import { BlockChunk, blockSchema, registryEntrySchema } from "@/registry/schema"
import { Style } from "@/registry/styles"
const DEFAULT_BLOCKS_STYLE = "default" satisfies Style["name"]
const project = new Project({
compilerOptions: {},
})
export async function getAllBlockIds(
style: Style["name"] = DEFAULT_BLOCKS_STYLE
) {
const blocks = await _getAllBlocks(style)
return blocks.map((block) => block.name)
}
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,
description: content.description || "",
type: "components:block",
})
}
async function _getAllBlocks(style: Style["name"] = DEFAULT_BLOCKS_STYLE) {
const index = z.record(registryEntrySchema).parse(Index[style])
return Object.values(index).filter(
(block) => block.type === "components:block"
)
}
async function _getBlockCode(
name: string,
style: Style["name"] = DEFAULT_BLOCKS_STYLE
) {
const entry = Index[style][name]
if (!entry) {
console.error(`Block ${name} not found in style ${style}`)
return ""
}
const block = registryEntrySchema.parse(entry)
if (!block.source) {
return ""
}
return await readFile(block.source)
}
async function readFile(source: string) {
const filepath = path.join(process.cwd(), source)
return await fs.readFile(filepath, "utf-8")
}
async function createTempSourceFile(filename: string) {
const dir = await fs.mkdtemp(path.join(tmpdir(), "codex-"))
return path.join(dir, filename)
}
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,
},
}
}
function _extractVariable(sourceFile: SourceFile, name: string) {
const variable = sourceFile.getVariableDeclaration(name)
if (!variable) {
return null
}
const value = variable
.getInitializerIfKindOrThrow(SyntaxKind.StringLiteral)
.getLiteralValue()
variable.remove()
return value
}