Files
supabase/apps/design-system/scripts/build-registry.mts
Saxon Fletcher ff599edf14 Page components (#40289)
* change database to pagelayout

* page components

* design system documentation

* reduce preview width

* revert studio

* functions apply page

* layout

* interfaces

* remove demos

* pckage

* format

* format

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* build registry

* pckg

* build script to allow index files

* switch exports

* fixes

* gen exports

* remove invalid prop

* fix typecheck

* fix prettier

* Revert "remove invalid prop"

This reverts commit 118ecd40c5.

* Revert "fix typecheck"

This reverts commit 0e1ec8fb61.

* Revert "fix prettier"

This reverts commit 2f25ed695c.

* pretter

* fresh pnpm i

* Update pnpm lock

* fix import

* revert

* Update packages/ui-patterns/src/PageContainer/index.tsx

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* Update packages/ui-patterns/src/PageHeader/index.tsx

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* Update packages/ui-patterns/src/PageSection/index.tsx

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* ts fix

* Update apps/design-system/content/docs/ui-patterns/page.mdx

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>

* change to layouts and examples

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
2025-11-11 04:02:30 +00:00

341 lines
11 KiB
TypeScript

// @sts-nocheck
import { existsSync, promises as fs } from 'fs'
import { tmpdir } from 'os'
import path from 'path'
import { cwd } from 'process'
import { rimraf } from 'rimraf'
import { Project, ScriptKind, SyntaxKind } from 'ts-morph'
import { registry } from '../registry/registry'
import { Registry, registrySchema } from '../registry/schema'
import { styles } from '../registry/styles'
const REGISTRY_PATH = path.join(process.cwd(), 'public/registry')
// ----------------------------------------------------------------------------
// Build __registry__/index.tsx.
// ----------------------------------------------------------------------------
async function buildRegistry(registry: Registry) {
const project = new Project({
compilerOptions: {},
})
async function createTempSourceFile(filename: string) {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-'))
return path.join(dir, filename)
}
let index = `// @ts-nocheck
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
import * as React from "react"
export const Index: Record<string, any> = {
`
for (const style of styles) {
index += ` "${style.name}": {`
// Build style index.
for (const item of registry) {
const resolveFiles = item.files.map((file) => `registry/${style.name}/${file}`)
const type = item.type.split(':')[1]
let sourceFilename = ''
let chunks: any = []
if (item.type === 'components:block') {
const file = resolveFiles[0]
const filename = path.basename(file)
const raw = await fs.readFile(file, 'utf8')
const tempFile = await createTempSourceFile(filename)
const sourceFile = project.createSourceFile(tempFile, raw, {
scriptKind: ScriptKind.TSX,
})
// Find all imports.
const imports = new Map<
string,
{
module: string
text: string
isDefault?: boolean
}
>()
sourceFile.getImportDeclarations().forEach((node) => {
const module = node.getModuleSpecifier().getLiteralValue()
node.getNamedImports().forEach((item) => {
imports.set(item.getText(), {
module,
text: node.getText(),
})
})
const defaultImport = node.getDefaultImport()
if (defaultImport) {
imports.set(defaultImport.getText(), {
module,
text: defaultImport.getText(),
isDefault: true,
})
}
})
// Find all opening tags with x-chunk attribute.
const components = sourceFile
.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
.filter((node) => {
return node.getAttribute('x-chunk') !== undefined
})
chunks = await Promise.all(
components.map(async (component, index) => {
const chunkName = `${item.name}-chunk-${index}`
// Get the value of x-chunk attribute.
const attr = component
.getAttributeOrThrow('x-chunk')
.asKindOrThrow(SyntaxKind.JsxAttribute)
const description = attr
.getInitializerOrThrow()
.asKindOrThrow(SyntaxKind.StringLiteral)
.getLiteralValue()
// Delete the x-chunk attribute.
attr.remove()
// Add a new attribute to the component.
component.addAttribute({
name: 'x-chunk',
initializer: `"${chunkName}"`,
})
// Get the value of x-chunk-container attribute.
const containerAttr = component
.getAttribute('x-chunk-container')
?.asKindOrThrow(SyntaxKind.JsxAttribute)
const containerClassName = containerAttr
?.getInitializer()
?.asKindOrThrow(SyntaxKind.StringLiteral)
.getLiteralValue()
containerAttr?.remove()
const parentJsxElement = component.getParentIfKindOrThrow(SyntaxKind.JsxElement)
// Find all opening tags on component.
const children = parentJsxElement
.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
.map((node) => {
return node.getTagNameNode().getText()
})
.concat(
parentJsxElement
.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
.map((node) => {
return node.getTagNameNode().getText()
})
)
const componentImports = new Map<string, string | string[] | Set<string>>()
children.forEach((child) => {
const importLine = imports.get(child)
if (importLine) {
const imports = componentImports.get(importLine.module) || []
const newImports = importLine.isDefault
? importLine.text
: new Set([...imports, child])
componentImports.set(
importLine.module,
importLine?.isDefault ? newImports : Array.from(newImports)
)
}
})
const componnetImportLines = Array.from(componentImports.keys()).map((key) => {
const values = componentImports.get(key)
const specifier = Array.isArray(values) ? `{${values.join(',')}}` : values
return `import ${specifier} from "${key}"`
})
const code = `
${componnetImportLines.join('\n')}
export default function Component() {
return (${parentJsxElement.getText()})
}`
const targetFile = file.replace(item.name, `${chunkName}`)
const targetFilePath = path.join(
cwd(),
`registry/${style.name}/${type}/${chunkName}.tsx`
)
// Write component file.
rimraf.sync(targetFilePath)
await fs.writeFile(targetFilePath, code, 'utf8')
return {
name: chunkName,
description,
component: `React.lazy(() => import("@/registry/${style.name}/${type}/${chunkName}")),`,
file: targetFile,
container: {
className: containerClassName,
},
}
})
)
// // Write the source file for blocks only.
sourceFilename = `__registry__/${style.name}/${type}/${item.name}.tsx`
const sourcePath = path.join(process.cwd(), sourceFilename)
if (!existsSync(sourcePath)) {
await fs.mkdir(sourcePath, { recursive: true })
}
rimraf.sync(sourcePath)
await fs.writeFile(sourcePath, sourceFile.getText())
}
// console.log('type', type)
// console.log('item', item)
let packagePath = ''
let componentImportPath = ''
if (type === 'ui') {
packagePath = `../../packages/ui/src/components/shadcn/ui`
componentImportPath = `@/${packagePath}/${item.name}`
}
if (type === 'fragment') {
packagePath = `../../packages/ui-patterns/src${item.optionalPath}`
// Check if the file is index.tsx - if so, don't append the item name
const isIndexFile = item.files.some((file) => {
const basename = path.basename(file)
return basename === 'index.tsx' || basename === 'index.ts'
})
componentImportPath = isIndexFile
? `@/${packagePath}`
: `@/${packagePath}/${item.name}`
}
if (type === 'example') {
packagePath = `registry/${style.name}/${type}`
componentImportPath = `@/${packagePath}/${item.name}`
}
if (type === 'block') {
packagePath = `registry/${style.name}/${type}`
componentImportPath = `@/${packagePath}/${item.name}`
}
index += `
"${item.name}": {
name: "${item.name}",
type: "${item.type}",
registryDependencies: ${JSON.stringify(item.registryDependencies)},
component: React.lazy(() => import("${componentImportPath}")),
source: "${sourceFilename}",
files: [${resolveFiles.map((file) => `"${file}"`)}],
category: "${item.category}",
subcategory: "${item.subcategory}",
chunks: [${chunks.map(
(chunk) => `{
name: "${chunk.name}",
description: "${chunk.description}",
component: ${chunk.component}
file: "${chunk.file}",
container: {
className: "${chunk.container.className}"
}
}`
)}]
},`
}
index += `
},`
}
index += `
}
`
// ----------------------------------------------------------------------------
// Build registry/index.json.
// ----------------------------------------------------------------------------
// const names = registry.filter((item) => item.type === 'components:ui')
// const registryJson = JSON.stringify(names, null, 2)
// rimraf.sync(path.join(REGISTRY_PATH, 'index.json'))
// await fs.writeFile(path.join(REGISTRY_PATH, 'index.json'), registryJson, 'utf8')
// Write style index.
rimraf.sync(path.join(process.cwd(), '__registry__/index.tsx'))
await fs.writeFile(path.join(process.cwd(), '__registry__/index.tsx'), index)
}
// ----------------------------------------------------------------------------
// Build registry/styles/[style]/[name].json.
// ----------------------------------------------------------------------------
// async function buildStyles(registry: Registry) {
// for (const style of styles) {
// const targetPath = path.join(REGISTRY_PATH, 'styles', style.name)
// console.log('targetPath', targetPath)
// // Create directory if it doesn't exist.
// if (!existsSync(targetPath)) {
// await fs.mkdir(targetPath, { recursive: true })
// }
// for (const item of registry) {
// if (item.type !== 'components:ui') {
// continue
// }
// const files = item.files?.map((file) => {
// console.log('path', path.join(process.cwd(), 'registry', style.name, file))
// const content = readFileSync(path.join(process.cwd(), 'registry', style.name, file), 'utf8')
// return {
// name: basename(file),
// content,
// }
// })
// const payload = {
// ...item,
// files,
// }
// await fs.writeFile(
// path.join(targetPath, `${item.name}.json`),
// JSON.stringify(payload, null, 2),
// 'utf8'
// )
// }
// }
try {
console.log('🚀 Building registry...')
const result = registrySchema.safeParse(registry)
if (!result.success) {
console.error(result.error)
process.exit(1)
}
await buildRegistry(result.data)
// await buildStyles(result.data)
// await buildThemes()
console.log('✅ Done!')
} catch (error) {
console.error(error)
process.exit(1)
}