Migrate off contentlayer2 to Velite (design-system, ui-library, learn) (#48546)

## Summary
- `contentlayer2@0.4.6` is unmaintained and drags in a heavy, stale
dependency graph (esbuild pinned to 0.17–0.20, mdx-bundler, old
`@opentelemetry/core`) that was the recurring source of vuln bumps.
- Migrates all three apps that used it — `design-system`, `ui-library`,
`learn` — to [Velite](https://velite.js.org), preserving the generated
typed `allDocs`/`Doc` collection and the `body.code` + `useMDXComponent`
runtime via a small shared local hook.
- Same MDX pipeline (remark-gfm, remark-code-import, rehype-slug,
rehype-pretty-code w/ Shiki compat + local theme,
rehype-autolink-headings, custom
`__rawString__`/`__src__`/`__event__`/`__style__` visitors) ported 1:1
into each app's `velite.config.js`.
- `learn`'s extra frontmatter fields (`chapterNumber`, `explore`,
`courseHero`) are now backed by real Velite/Zod schema types, so the
`(doc as any)` casts in `get-next-page.ts` / `get-current-chapter.ts` /
the doc page could be dropped.
- `next.config.mjs` no longer wraps with `withContentlayer`; since
Velite has no Next.js webpack-plugin equivalent, each app's `dev` script
now runs `velite dev` and `next dev` in parallel via `npm-run-all`.

Ref:
[FE-3861](https://linear.app/supabase/issue/FE-3861/migrate-off-contentlayer2-learn-ui-library-design-system-to-shed)

## Test plan
- [x] `pnpm build:content` (Velite build) succeeds for all three apps
- [x] `pnpm typecheck` passes for all three apps
- [ ] Manual smoke test of `pnpm dev` for each app in a browser (docs
pages render, TOC, copy-button, code highlighting)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Improved content generation across documentation, learning materials,
and the UI library for more consistent pages.
* Preserved MDX rendering, navigation, table of contents, course
metadata, source previews, and component examples.
* Improved consistency when displaying documentation and interactive
examples.
* Improved application loading by optimizing how interface components
are delivered.
* **Chores**
* Streamlined content compilation and development workflows across the
design system, learning area, and UI library.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ivan Vasilov authored and GitHub committed 2026-09-04 15:49:44 +02:00
1 parent 1308a1d0d0
commit c6cdf4bd53
45 files changed
+971 -2194

No files matched your search

+1
View File
@@ -39,3 +39,4 @@ next-env.d.ts
# Generated by `pnpm generate:registry` (`scripts/build-registry.mts`)
/__registry__
.velite
@@ -9,7 +9,6 @@ import { absoluteUrl } from '@/lib/utils'
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
import { allDocs } from 'contentlayer/generated'
import { ChevronRight } from 'lucide-react'
import type { Metadata } from 'next'
import Link from 'next/link'
@@ -17,6 +16,8 @@ import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea, Separator } from 'ui'
import { allDocs } from '@/.velite'
interface DocPageProps {
params: Promise<{
slug: string[]
@@ -83,7 +84,7 @@ export default async function DocPage(props: DocPageProps) {
notFound()
}
const toc = await getTableOfContents(doc.body.raw)
const toc = await getTableOfContents(doc.raw)
const breadcrumbSegments = getBreadcrumbSegments(doc)
return (
@@ -128,7 +129,7 @@ export default async function DocPage(props: DocPageProps) {
<Separator className="mb-6" />
<SourcePanel doc={doc} />
<div className="pb-12">
<Mdx code={doc.body.code} />
<Mdx code={doc.code} />
</div>
<DocsPager doc={doc} />
</div>
@@ -1,6 +1,5 @@
'use client'
import { useMDXComponent } from 'next-contentlayer2/hooks'
import Image from 'next/image'
import Link from 'next/link'
import * as React from 'react'
@@ -43,6 +42,7 @@ import {
import { Icons } from '@/components/icons'
import { ThemeSettings } from '@/components/theme-settings'
import { useConfig } from '@/hooks/use-config'
import { useMDXComponent } from '@/lib/use-mdx-component'
import { Style } from '@/registry/styles'
const components = {
+1 -1
View File
@@ -1,7 +1,7 @@
import { Doc } from 'contentlayer/generated'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { Doc } from '@/.velite'
import { docsConfig } from '@/config/docs'
import { cn } from '@/lib/utils'
import { NavItem, NavItemWithChildren } from '@/types/nav'
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { forwardRef } from 'react'
import { Button, cn } from 'ui'
import { Doc } from '@/.contentlayer/generated'
import { Doc } from '@/.velite'
const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> & { doc: Doc }>(
({ doc, children, ...props }, ref) => {
@@ -4,8 +4,6 @@ description: Displays a card with header, content, and footer.
component: true
---
<ComponentPreview name="card-with-form" />
## Installation
<Tabs defaultValue="cli">
@@ -65,7 +63,3 @@ import {
</CardFooter>
</Card>
```
## Examples
<ComponentPreview name="card-demo" peekCode wide />
@@ -7,8 +7,6 @@ links:
api: https://www.embla-carousel.com/api
---
<ComponentPreview name="carousel-demo" peekCode wide />
## About
The carousel component is built using the [Embla Carousel](https://www.embla-carousel.com/) library.
@@ -42,7 +40,7 @@ npm install embla-carousel-react
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="carousel" />
{/* <ComponentSource name="carousel" /> */}
<Step>Update the import paths to match your project setup.</Step>
@@ -82,8 +80,6 @@ import {
To set the size of the items, you can use the `basis` utility class on the `<CarouselItem />`.
<ComponentPreview name="carousel-size" />
```tsx title="Example" showLineNumbers {4-6}
// 33% of the carousel width.
<Carousel>
@@ -120,8 +116,6 @@ You can always adjust this in your own project if you need to.
</Callout>
<ComponentPreview name="carousel-spacing" />
```tsx title="Example" showLineNumbers /-ml-4/ /pl-4/
<Carousel>
<CarouselContent className="-ml-4">
@@ -146,8 +140,6 @@ You can always adjust this in your own project if you need to.
Use the `orientation` prop to set the orientation of the carousel.
<ComponentPreview name="carousel-orientation" />
```tsx showLineNumbers /vertical | horizontal/
<Carousel orientation="vertical | horizontal">
<CarouselContent>
@@ -181,8 +173,6 @@ You can pass options to the carousel using the `opts` prop. See the [Embla Carou
Use a state and the `setApi` props to get an instance of the carousel API.
<ComponentPreview name="carousel-api" />
```tsx showLineNumbers {1,4,22}
import { type CarouselApi } from '@/components/ui/carousel'
@@ -272,6 +262,4 @@ export function Example() {
}
```
<ComponentPreview name="carousel-plugin" />
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on using plugins.
@@ -6,8 +6,6 @@ source:
shadcn: true
---
<ComponentPreview name="pagination-demo" peekCode wide />
## Installation
<Tabs defaultValue="cli">
@@ -30,7 +28,7 @@ npx shadcn-ui@latest add pagination
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="pagination" />
{/* <ComponentSource name="pagination" /> */}
<Step>Update the import paths to match your project setup.</Step>
@@ -4,19 +4,6 @@ description: A composable, themeable and customizable sidebar component.
component: true
---
<figure className="flex flex-col gap-4">
<ComponentPreview
name="sidebar-07"
title="Sidebar"
type="block"
description="A composable, themeable and customizable sidebar component built using shadcn/ui"
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar that collapses to icons.
</figcaption>
</figure>
Sidebars are one of the most complex components to build. They are central
to any application and often contain a lot of moving parts.
@@ -331,17 +318,6 @@ export function AppSidebar() {
<Step>You've created your first sidebar.</Step>
<figure className="flex flex-col gap-4">
<ComponentPreview
name="sidebar-demo"
title="Sidebar"
type="block"
description="Your first sidebar."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">Your first sidebar.</figcaption>
</figure>
</Steps>
## Components
@@ -546,19 +522,6 @@ Use the `SidebarHeader` component to add a sticky header to the sidebar.
The following example adds a `<DropdownMenu>` to the `SidebarHeader`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-header"
title="Sidebar"
type="block"
description="A sidebar header with a dropdown menu."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar header with a dropdown menu.
</figcaption>
</figure>
```tsx showLineNumbers title="components/app-sidebar.tsx"
<Sidebar>
<SidebarHeader>
@@ -592,19 +555,6 @@ Use the `SidebarFooter` component to add a sticky footer to the sidebar.
The following example adds a `<DropdownMenu>` to the `SidebarFooter`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-footer"
title="Sidebar"
type="block"
description="A sidebar footer with a dropdown menu."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar footer with a dropdown menu.
</figcaption>
</figure>
```tsx showLineNumbers title="components/app-sidebar.tsx"
export function AppSidebar() {
return (
@@ -668,17 +618,6 @@ Use the `SidebarGroup` component to create a section within the sidebar.
A `SidebarGroup` has a `SidebarGroupLabel`, a `SidebarGroupContent` and an optional `SidebarGroupAction`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-group"
title="Sidebar Group"
type="block"
description="A sidebar group."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">A sidebar group.</figcaption>
</figure>
```tsx showLineNumbers
import { Sidebar, SidebarContent, SidebarGroup } from '@/components/ui/sidebar'
@@ -703,19 +642,6 @@ export function AppSidebar() {
To make a `SidebarGroup` collapsible, wrap it in a `Collapsible`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-group-collapsible"
title="Sidebar Group"
type="block"
description="A collapsible sidebar group."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A collapsible sidebar group.
</figcaption>
</figure>
```tsx showLineNumbers
export function AppSidebar() {
return (
@@ -758,19 +684,6 @@ export function AppSidebar() {
}
```
<figure className="flex flex-col gap-4">
<ComponentPreview
name="sidebar-group-action"
title="Sidebar Group"
type="block"
description="A sidebar group with an action button."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar group with an action button.
</figcaption>
</figure>
## SidebarMenu
The `SidebarMenu` component is used for building a menu within a `SidebarGroup`.
@@ -794,19 +707,6 @@ A `SidebarMenu` component is composed of `SidebarMenuItem`, `SidebarMenuButton`,
Here's an example of a `SidebarMenu` component rendering a list of projects.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-menu"
title="Sidebar Menu"
type="block"
description="A sidebar menu with a list of projects."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar menu with a list of projects.
</figcaption>
</figure>
```tsx showLineNumbers
<Sidebar>
<SidebarContent>
@@ -892,19 +792,6 @@ This button works independently of the `SidebarMenuButton` i.e you can have the
Here's an example of a `SidebarMenuAction` component rendering a `DropdownMenu`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-menu-action"
title="Sidebar Menu Action"
type="block"
description="A sidebar menu action with a dropdown menu."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar menu action with a dropdown menu.
</figcaption>
</figure>
```tsx showLineNumbers
<SidebarMenuItem>
<SidebarMenuButton asChild>
@@ -937,19 +824,6 @@ The `SidebarMenuSub` component is used to render a submenu within a `SidebarMenu
Use `<SidebarMenuSubItem />` and `<SidebarMenuSubButton />` to render a submenu item.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-menu-sub"
title="Sidebar Menu Sub"
type="block"
description="A sidebar menu sub."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar menu with a submenu.
</figcaption>
</figure>
```tsx showLineNumbers
<SidebarMenuItem>
<SidebarMenuButton />
@@ -968,17 +842,6 @@ Use `<SidebarMenuSubItem />` and `<SidebarMenuSubButton />` to render a submenu
To make a `SidebarMenu` component collapsible, wrap it and the `SidebarMenuSub` components in a `Collapsible`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-menu-collapsible"
title="Sidebar Menu"
type="block"
description="A collapsible sidebar menu."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">A collapsible sidebar menu.</figcaption>
</figure>
```tsx showLineNumbers
<SidebarMenu>
<Collapsible defaultOpen className="group/collapsible">
@@ -1000,19 +863,6 @@ To make a `SidebarMenu` component collapsible, wrap it and the `SidebarMenuSub`
The `SidebarMenuBadge` component is used to render a badge within a `SidebarMenuItem`.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-menu-badge"
title="Sidebar Menu Badge"
type="block"
description="A sidebar menu badge."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar menu with a badge.
</figcaption>
</figure>
```tsx showLineNumbers
<SidebarMenuItem>
<SidebarMenuButton />
@@ -1104,19 +954,6 @@ The `SidebarRail` component is used to render a rail within a `Sidebar`. This ra
Here's an example of a `SidebarMenu` component rendering a list of projects using React Server Components.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-rsc"
title="Sidebar Menu RSC"
type="block"
description="A sidebar menu using React Server Components."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">
A sidebar menu using React Server Components.
</figcaption>
</figure>
```tsx showLineNumbers {6} title="Skeleton to show loading state."
function NavProjectsSkeleton() {
return (
@@ -1253,17 +1090,6 @@ function NavProjects() {
Use the `open` and `onOpenChange` props to control the sidebar.
<figure className="flex flex-col gap-4 mt-6">
<ComponentPreview
name="sidebar-controlled"
title="Sidebar Controlled"
type="block"
description="A controlled sidebar."
className="w-full"
/>
<figcaption className="text-center text-sm text-gray-500">A controlled sidebar.</figcaption>
</figure>
```tsx showLineNumbers
export function AppSidebar() {
const [open, setOpen] = React.useState(false)
@@ -8,9 +8,9 @@ component: true
## Examples
{/* ### Multiple sections */}
### Multiple sections
{/* <ComponentPreview name="inner-side-menu-multiple-sections" peekCode /> */}
<ComponentPreview name="inner-side-menu-multiple-sections" peekCode />
### Collapsible menus
+28 -9
View File
@@ -5,10 +5,29 @@ import React from 'react'
import { u } from 'unist-builder'
import { visit } from 'unist-util-visit'
import { Index } from '../__registry__'
import { registry } from '../registry/registry'
import { styles } from '../registry/styles'
import { UnistNode, UnistTree } from '@/types/unist'
// Resolves registry files from the plain registry data (name/type/files
// metadata only) rather than `__registry__` — the generated index that also
// wires up `React.lazy` component imports. This plugin only ever needs file
// paths to read source off disk, and pulling in the generated index (with its
// deep chain of real component + npm dependencies) breaks Velite's esbuild-based
// config bundling under pnpm's strict module resolution.
function getRegistryFiles(styleName: string, name: string) {
const item = registry.find((entry) => entry.name === name)
if (!item || item.files.length === 0) {
throw new Error(`Registry item "${name}" has no source files`)
}
// `components:ui` items are the real shadcn primitives shared via `packages/ui`,
// not local copies under `registry/${styleName}/` like examples/fragments/blocks.
if (item.type === 'components:ui') {
return item.files.map((file) => `../../packages/ui/src/components/shadcn/ui/${file}`)
}
return item.files.map((file) => `registry/${styleName}/${file}`)
}
export function rehypeComponent() {
return async (tree: UnistTree) => {
visit(tree, (node: UnistNode) => {
@@ -39,16 +58,16 @@ export function rehypeComponent() {
if (srcPath) {
src = srcPath
} else {
const component = Index[style.name][name]
const files = getRegistryFiles(style.name, name)
// console.log('got to ELSE STATEMENT')
// console.log('filename', fileName)
// console.log('name', name)
src = fileName
? component.files.find((file: string) => {
? files.find((file: string) => {
return file.endsWith(`${fileName}.tsx`) || file.endsWith(`${fileName}.ts`)
}) || component.files[0]
: component.files[0]
}) || files[0]
: files[0]
// console.log('got to END of ELSE STATEMENT')
}
@@ -108,9 +127,9 @@ export function rehypeComponent() {
try {
for (const style of styles) {
const component = Index[style.name][name]
const files = getRegistryFiles(style.name, name)
// console.log('GOT HERE')
const src = component.files[0]
const src = files[0]
// Read the source file.
const filePath = path.join(process.cwd(), src)
@@ -160,9 +179,9 @@ export function rehypeComponent() {
try {
for (const style of styles) {
const component = Index[style.name][name]
const files = getRegistryFiles(style.name, name)
// console.log('GOT HERE')
const src = component.files[0]
const src = files[0]
// Read the source file.
const filePath = path.join(process.cwd(), src)
@@ -0,0 +1,16 @@
import * as React from 'react'
import * as runtime from 'react/jsx-runtime'
// Velite compiles MDX to a "function-body" (see velite.config.js `code: s.mdx()`)
// that reads its jsx runtime off `arguments[0]` — mirrors the pattern from
// https://velite.js.org/guide/using-mdx.
const EMPTY_GLOBALS: Record<string, unknown> = {}
function getMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
const fn = new Function(code)
return fn({ ...runtime, ...globals }).default
}
export function useMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
return React.useMemo(() => getMDXComponent(code, globals), [code, globals])
}
+1 -5
View File
@@ -1,5 +1,3 @@
import { withContentlayer } from 'next-contentlayer2'
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
/** @type {import('next').NextConfig} */
@@ -37,8 +35,6 @@ const nextConfig = {
]
},
// Turbopack configuration to handle .md files with raw-loader
// This mirrors the webpack configuration added by withContentlayer
// and ensures both bundlers can process content files properly
turbopack: {
rules: {
'*.md': {
@@ -49,4 +45,4 @@ const nextConfig = {
},
}
export default withContentlayer(nextConfig)
export default nextConfig
+9 -10
View File
@@ -5,23 +5,22 @@
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "pnpm run generate:registry && run-p --race dev:*",
"dev:content": "velite dev",
"dev:next": "next dev --turbopack --port 3003",
"dev:content": "contentlayer2 dev",
"build": "pnpm run build:registry && pnpm run content:build && next build --turbopack",
"generate:registry": "tsx ./scripts/build-registry.mts",
"build:registry": "pnpm run generate:registry && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"dev": "run-p build:registry build:content && run-p --race dev:*",
"build:content": "velite build --strict",
"build:registry": "tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"build:next": "next build --turbopack",
"build": "run-p build:registry build:content && pnpm build:next",
"start": "next start",
"lint": "eslint .",
"content:build": "contentlayer2 build",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo __registry__",
"typecheck": "pnpm run generate:registry && contentlayer2 build && tsc --noEmit -p tsconfig.json"
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite __registry__",
"typecheck": "run-p build:registry build:content && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
"@tanstack/react-table": "catalog:",
"common": "workspace:*",
"contentlayer2": "0.4.6",
"date-fns": "^2.30.0",
"dayjs": "1.11.13",
"eslint-config-supabase": "workspace:*",
@@ -30,7 +29,6 @@
"lucide-react": "*",
"markdown-wasm": "^1.2.0",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "catalog:",
"react": "catalog:",
"react-data-grid": "7.0.0-beta.47",
@@ -51,6 +49,7 @@
"ui": "workspace:*",
"ui-patterns": "workspace:*",
"unist-util-visit": "^5.0.0",
"velite": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
+2 -1
View File
@@ -3,5 +3,6 @@ import { charts } from '@/registry/charts'
import { copyWriting } from '@/registry/copy-writing'
import { fragments } from '@/registry/fragments'
import { Registry } from '@/registry/schema'
import { ui } from '@/registry/ui'
export const registry: Registry = [...fragments, ...examples, ...charts, ...copyWriting]
export const registry: Registry = [...fragments, ...examples, ...charts, ...copyWriting, ...ui]
+58
View File
@@ -0,0 +1,58 @@
import { Registry } from './schema'
// Base shadcn/ui primitives. Unlike `examples`/`fragments`/`charts`, these don't
// live under `registry/default/*` — their source is the real, shared components
// consumed via `import { X } from 'ui'`, at `packages/ui/src/components/shadcn/ui`.
// `getRegistryFiles` in `lib/rehype-component.ts` resolves `components:ui` files
// against that package path instead of the usual `registry/${style}/` prefix.
export const ui: Registry = [
{ name: 'accordion', type: 'components:ui', files: ['accordion.tsx'] },
{ name: 'alert', type: 'components:ui', files: ['alert.tsx'] },
{ name: 'alert-dialog', type: 'components:ui', files: ['alert-dialog.tsx'] },
{ name: 'aspect-ratio', type: 'components:ui', files: ['aspect-ratio.tsx'] },
{ name: 'avatar', type: 'components:ui', files: ['avatar.tsx'] },
{ name: 'badge', type: 'components:ui', files: ['badge.tsx'] },
{ name: 'breadcrumb', type: 'components:ui', files: ['breadcrumb.tsx'] },
{ name: 'button', type: 'components:ui', files: ['button.tsx'] },
{ name: 'button-group', type: 'components:ui', files: ['button-group.tsx'] },
{ name: 'calendar', type: 'components:ui', files: ['calendar.tsx'] },
{ name: 'card', type: 'components:ui', files: ['card.tsx'] },
{ name: 'chart', type: 'components:ui', files: ['chart.tsx'] },
{ name: 'checkbox', type: 'components:ui', files: ['checkbox.tsx'] },
{ name: 'collapsible', type: 'components:ui', files: ['collapsible.tsx'] },
{ name: 'command', type: 'components:ui', files: ['command.tsx'] },
{ name: 'context-menu', type: 'components:ui', files: ['context-menu.tsx'] },
{ name: 'dialog', type: 'components:ui', files: ['dialog.tsx'] },
{ name: 'drawer', type: 'components:ui', files: ['drawer.tsx'] },
{ name: 'dropdown-menu', type: 'components:ui', files: ['dropdown-menu.tsx'] },
{ name: 'field', type: 'components:ui', files: ['field.tsx'] },
{ name: 'form', type: 'components:ui', files: ['form.tsx'] },
{ name: 'hover-card', type: 'components:ui', files: ['hover-card.tsx'] },
{ name: 'input', type: 'components:ui', files: ['input.tsx'] },
{ name: 'input-group', type: 'components:ui', files: ['input-group.tsx'] },
{ name: 'input-otp', type: 'components:ui', files: ['input-otp.tsx'] },
{ name: 'label', type: 'components:ui', files: ['label.tsx'] },
{ name: 'menubar', type: 'components:ui', files: ['menubar.tsx'] },
{ name: 'navigation-menu', type: 'components:ui', files: ['navigation-menu.tsx'] },
{ name: 'popover', type: 'components:ui', files: ['popover.tsx'] },
{ name: 'progress', type: 'components:ui', files: ['progress.tsx'] },
{ name: 'radio-group', type: 'components:ui', files: ['radio-group.tsx'] },
{ name: 'resizable', type: 'components:ui', files: ['resizable.tsx'] },
{ name: 'scroll-area', type: 'components:ui', files: ['scroll-area.tsx'] },
{ name: 'select', type: 'components:ui', files: ['select.tsx'] },
{ name: 'separator', type: 'components:ui', files: ['separator.tsx'] },
{ name: 'sheet', type: 'components:ui', files: ['sheet.tsx'] },
{ name: 'sidebar', type: 'components:ui', files: ['sidebar.tsx'] },
{ name: 'skeleton', type: 'components:ui', files: ['skeleton.tsx'] },
{ name: 'slider', type: 'components:ui', files: ['slider.tsx'] },
{ name: 'sonner', type: 'components:ui', files: ['sonner.tsx'] },
{ name: 'sonner-progress', type: 'components:ui', files: ['sonner-progress.tsx'] },
{ name: 'switch', type: 'components:ui', files: ['switch.tsx'] },
{ name: 'table', type: 'components:ui', files: ['table.tsx'] },
{ name: 'tabs', type: 'components:ui', files: ['tabs.tsx'] },
{ name: 'text-area', type: 'components:ui', files: ['text-area.tsx'] },
{ name: 'textarea', type: 'components:ui', files: ['textarea.tsx'] },
{ name: 'toggle', type: 'components:ui', files: ['toggle.tsx'] },
{ name: 'toggle-group', type: 'components:ui', files: ['toggle-group.tsx'] },
{ name: 'tooltip', type: 'components:ui', files: ['tooltip.tsx'] },
]
+1 -8
View File
@@ -6,17 +6,10 @@
"lib": ["dom", "dom.iterable", "esnext"],
"paths": {
"@/*": ["./*"],
"contentlayer/generated": ["./.contentlayer/generated"],
"icons/*": ["./../../packages/icons/*"]
},
"plugins": [{ "name": "next" }]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".velite"],
"exclude": ["node_modules", "./scripts/build-registry.mts"]
}
+2 -13
View File
@@ -4,19 +4,8 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": [
"NEXT_PUBLIC_BASE_PATH",
"NEXT_PUBLIC_APP_URL",
// These envs are used in the packages
"NEXT_PUBLIC_STORAGE_KEY",
"NEXT_PUBLIC_AUTH_DEBUG_KEY",
"NEXT_PUBLIC_AUTH_PERSISTED_KEY",
"NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY",
"NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL",
"NEXT_PUBLIC_GOTRUE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
],
"outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*", ".contentlayer/**"],
"env": ["NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_APP_URL"],
"outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*", ".velite/**"],
},
},
}
@@ -1,169 +1,74 @@
import path from 'path'
import { getHighlighter, loadTheme } from '@shikijs/compat'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { codeImport } from 'remark-code-import'
import remarkGfm from 'remark-gfm'
import { visit } from 'unist-util-visit'
import { defineConfig, s } from 'velite'
/** @type {import('contentlayer2/source-files').ComputedFields} */
const computedFields = {
slug: {
type: 'string',
resolve: (doc) => `/${doc._raw.flattenedPath}`,
},
slugAsParams: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath,
},
}
import { rehypeComponent } from './lib/rehype-component'
const LinksProperties = defineNestedType(() => ({
name: 'LinksProperties',
fields: {
doc: {
type: 'string',
},
api: {
type: 'string',
const LinksProperties = s.object({
doc: s.string().optional(),
api: s.string().optional(),
})
const NestedProperties = s.object({
radix: s.boolean().optional(),
shadcn: s.boolean().optional(),
vaul: s.boolean().optional(),
inputOtp: s.boolean().optional(),
reactAccessibleTreeview: s.boolean().optional(),
recharts: s.boolean().optional(),
})
const docs = s
.object({
title: s.string(),
description: s.string(),
published: s.boolean().default(true),
links: LinksProperties.optional(),
featured: s.boolean().default(false),
component: s.boolean().default(false),
fragment: s.boolean().default(false),
toc: s.boolean().default(true),
source: NestedProperties.optional(),
// mirrors contentlayer2's `_raw.flattenedPath`: file path relative to the
// content dir, extension stripped, trailing `/index` dropped.
path: s.path(),
raw: s.raw(),
// internal doc cross-links (e.g. `[Button](components/button)`) aren't
// real files on disk — disable Velite's default asset-copying behavior,
// which otherwise treats every relative link as a local file to copy.
// Minification is also disabled: it's pure CPU-bound Terser work with no
// real benefit for a dev-only content cache, and dominates build time.
code: s.mdx({ copyLinkedFiles: false, minify: false }),
})
.transform(({ path: flattenedPath, ...data }) => ({
...data,
slug: `/${flattenedPath}`,
slugAsParams: flattenedPath.split('/').slice(1).join('/'),
}))
export default defineConfig({
root: './content',
output: {
clean: true,
},
collections: {
allDocs: {
name: 'Doc',
pattern: 'docs/**/*.mdx',
schema: docs,
},
},
}))
const ExploreItem = defineNestedType(() => ({
name: 'ExploreItem',
fields: {
title: {
type: 'string',
required: true,
},
link: {
type: 'string',
required: true,
},
itemType: {
type: 'string',
required: false,
},
description: {
type: 'string',
required: false,
},
},
}))
const CourseHero = defineNestedType(() => ({
name: 'CourseHero',
fields: {
title: {
type: 'string',
required: true,
},
subtitle: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
},
}))
const NestedProperties = defineNestedType(() => ({
name: 'NestedProperties',
fields: {
radix: {
type: 'boolean',
},
shadcn: {
type: 'boolean',
},
vaul: {
type: 'boolean',
},
inputOtp: {
type: 'boolean',
},
reactAccessibleTreeview: {
type: 'boolean',
},
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: `**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
published: {
type: 'boolean',
default: true,
},
links: {
type: 'nested',
of: LinksProperties,
},
featured: {
type: 'boolean',
default: false,
required: false,
},
component: {
type: 'boolean',
default: false,
required: false,
},
fragment: {
type: 'boolean',
default: false,
required: false,
},
toc: {
type: 'boolean',
default: true,
required: false,
},
chapterNumber: {
type: 'number',
required: false,
},
explore: {
type: 'list',
of: ExploreItem,
required: false,
},
courseHero: {
type: 'nested',
of: CourseHero,
required: false,
},
source: {
type: 'nested',
of: NestedProperties,
},
},
computedFields,
}))
export default makeSource({
disableImportAliasWarning: true,
contentDirPath: './content',
documentTypes: [Doc],
mdx: {
remarkPlugins: [remarkGfm, codeImport],
rehypePlugins: [
rehypeSlug,
rehypeComponent,
() => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'pre') {
@@ -190,12 +95,19 @@ export default makeSource({
},
[
rehypePrettyCode,
// rehypePrettyCodeOptions,
{
getHighlighter: async () => {
const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json'))
return await getHighlighter({ theme })
},
// Memoized so the (expensive) theme parse + oniguruma WASM init runs once
// for the whole build, instead of once per file — velite compiles every
// file concurrently, so without this every doc pays that cost redundantly.
getHighlighter: (() => {
let highlighterPromise
return () => {
highlighterPromise ??= loadTheme(
path.join(process.cwd(), '/lib/themes/supabase-2.json')
).then((theme) => getHighlighter({ theme }))
return highlighterPromise
}
})(),
onVisitLine(node) {
// Prevent lines from collapsing in `display: grid` mode, and allow empty
// lines to be copy/pasted
@@ -240,7 +152,6 @@ export default makeSource({
}
})
},
// rehypeNpmCommand,
[
rehypeAutolinkHeadings,
{
+2 -1
View File
@@ -35,4 +35,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
.contentlayer
.contentlayer
.velite
+8 -9
View File
@@ -5,19 +5,22 @@ import { ExploreMore } from '@/components/explore-more'
import { Mdx } from '@/components/mdx-components'
import { NextUp } from '@/components/next-up'
import { DashboardTableOfContents } from '@/components/toc'
import { getTableOfContents } from '@/lib/toc'
import { getCurrentChapter } from '@/lib/get-current-chapter'
import { getNextPage } from '@/lib/get-next-page'
import { getTableOfContents } from '@/lib/toc'
import { absoluteUrl, cn } from '@/lib/utils'
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
import { allDocs } from 'contentlayer/generated'
import { ChevronRight } from 'lucide-react'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea } from 'ui'
import { allDocs } from '@/.velite'
interface DocPageProps {
params: Promise<{
slug: string[]
@@ -75,17 +78,13 @@ export default async function DocPage(props: DocPageProps) {
notFound()
}
const toc = await getTableOfContents(doc.body.raw)
const toc = await getTableOfContents(doc.raw)
const nextPage = getNextPage(doc.slugAsParams)
const currentChapter = getCurrentChapter(doc.slugAsParams)
const slugSegments = doc.slugAsParams.split('/')
const isIntroductionPage = slugSegments[slugSegments.length - 1] === 'introduction'
const exploreItems = (
doc as {
explore?: Array<{ title: string; link: string; itemType?: string; description?: string }>
}
).explore
const exploreItems = doc.explore
return (
<div className={cn('relative')}>
@@ -121,7 +120,7 @@ export default async function DocPage(props: DocPageProps) {
</div>
</div>
<div className="pb-12">
<Mdx code={doc.body.code} />
<Mdx code={doc.code} />
</div>
{exploreItems && exploreItems.length > 0 && <ExploreMore items={exploreItems} />}
{currentChapter && nextPage && (
+1 -1
View File
@@ -1,10 +1,10 @@
import { useMDXComponent } from 'next-contentlayer2/hooks'
import Link from 'next/link'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, cn } from 'ui'
import { Callout } from './callout'
import { CopyButton } from './copy-button'
import TanStackBeta from './tanstack-beta'
import { useMDXComponent } from '@/lib/use-mdx-component'
const components = {
Accordion,
+2 -2
View File
@@ -1,5 +1,5 @@
import { allDocs } from 'contentlayer/generated'
import { normalizeSlug } from './get-next-page'
import { allDocs } from '@/.velite'
/**
* Gets the current chapter number from a doc
@@ -15,7 +15,7 @@ export function getCurrentChapter(
}
// Get chapterNumber from frontmatter first, then fallback to parsing from title
let chapterNumber: number | undefined = (doc as any)?.chapterNumber
let chapterNumber: number | undefined = doc?.chapterNumber
if (!chapterNumber) {
// Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2)
const chapterMatch = doc.title.match(/^(\d+):/)
+2 -3
View File
@@ -1,4 +1,4 @@
import { allDocs } from 'contentlayer/generated'
import { allDocs } from '@/.velite'
import { courses } from '@/config/docs'
import { SidebarNavItem } from '@/types/nav'
@@ -65,8 +65,7 @@ export function getNextPage(
const description = doc?.description || nextPage.title
// Get chapterNumber from frontmatter first, then fallback to parsing from title
// Using type assertion since contentlayer types may need regeneration
let chapterNumber: number | undefined = (doc as any)?.chapterNumber
let chapterNumber: number | undefined = doc?.chapterNumber
if (!chapterNumber) {
// Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2)
const chapterMatch = nextPage.title.match(/^(\d+):/)
+16
View File
@@ -0,0 +1,16 @@
import * as React from 'react'
import * as runtime from 'react/jsx-runtime'
// Velite compiles MDX to a "function-body" (see velite.config.js `code: s.mdx()`)
// that reads its jsx runtime off `arguments[0]` — mirrors the pattern from
// https://velite.js.org/guide/using-mdx.
const EMPTY_GLOBALS: Record<string, unknown> = {}
function getMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
const fn = new Function(code)
return fn({ ...runtime, ...globals }).default
}
export function useMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
return React.useMemo(() => getMDXComponent(code, globals), [code, globals])
}
+1 -3
View File
@@ -1,5 +1,3 @@
import { withContentlayer } from 'next-contentlayer2'
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'],
@@ -32,4 +30,4 @@ const nextConfig = {
},
}
export default withContentlayer(nextConfig)
export default nextConfig
+10 -8
View File
@@ -5,24 +5,24 @@
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3007",
"build": "pnpm run content:build && next build --turbopack",
"dev:content": "velite dev",
"dev:next": "next dev --port 3007",
"dev": "pnpm run build:content && run-p dev:content dev:next",
"build": "pnpm run build:content && next build --turbopack",
"build:content": "velite build --strict",
"internal:sync": "tsx ./scripts/sync-internal-content.mts",
"start": "next start",
"lint": "eslint .",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
"content:build": "contentlayer2 build",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"typecheck": "contentlayer2 build && tsc --noEmit"
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite tsconfig.tsbuildinfo",
"typecheck": "pnpm build:content && tsc --noEmit"
},
"dependencies": {
"common": "workspace:*",
"contentlayer2": "0.4.6",
"framer-motion": "^11.18.2",
"jotai": "^2.8.0",
"lucide-react": "*",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
@@ -35,7 +35,8 @@
"remark-gfm": "^4.0.0",
"ui": "workspace:*",
"ui-patterns": "workspace:*",
"unist-util-visit": "^5.0.0"
"unist-util-visit": "^5.0.0",
"velite": "catalog:"
},
"devDependencies": {
"@shikijs/compat": "^1.1.7",
@@ -43,6 +44,7 @@
"@types/react": "catalog:",
"config": "workspace:^",
"mdast-util-toc": "^6.1.1",
"npm-run-all": "^4.1.5",
"shiki": "^1.1.7",
"tailwindcss": "catalog:",
"tsconfig": "workspace:*",
+2 -9
View File
@@ -5,19 +5,12 @@
"paths": {
// @deprecated: Don't import directly without suffix. Use @/ always.
"*": ["./*"],
"@/*": ["./*"],
"contentlayer/generated": ["./.contentlayer/generated"]
"@/*": ["./*"]
},
"plugins": [{ "name": "next" }],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".velite"],
"exclude": ["node_modules", "./scripts/build-registry.mts"]
}
+1 -1
View File
@@ -4,7 +4,7 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**/*", "!.next/dev/**/*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**/*", "!.next/dev/**/*", ".velite/**"],
},
},
}
@@ -1,111 +1,80 @@
import path from 'path'
import { getHighlighter, loadTheme } from '@shikijs/compat'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { codeImport } from 'remark-code-import'
import remarkGfm from 'remark-gfm'
import { visit } from 'unist-util-visit'
import { defineConfig, s } from 'velite'
/** @type {import('contentlayer2/source-files').ComputedFields} */
const computedFields = {
slug: {
type: 'string',
resolve: (doc) => `/${doc._raw.flattenedPath}`,
},
slugAsParams: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath.split('/').slice(1).join('/'),
},
}
const LinksProperties = s.object({
doc: s.string().optional(),
api: s.string().optional(),
})
const LinksProperties = defineNestedType(() => ({
name: 'LinksProperties',
fields: {
doc: {
type: 'string',
},
api: {
type: 'string',
const ExploreItem = s.object({
title: s.string(),
link: s.string(),
itemType: s.string().optional(),
description: s.string().optional(),
})
const CourseHero = s.object({
title: s.string(),
subtitle: s.string(),
description: s.string(),
})
const NestedProperties = s.object({
radix: s.boolean().optional(),
shadcn: s.boolean().optional(),
vaul: s.boolean().optional(),
inputOtp: s.boolean().optional(),
reactAccessibleTreeview: s.boolean().optional(),
})
const docs = s
.object({
title: s.string(),
description: s.string(),
published: s.boolean().default(true),
links: LinksProperties.optional(),
featured: s.boolean().default(false),
component: s.boolean().default(false),
fragment: s.boolean().default(false),
toc: s.boolean().default(true),
chapterNumber: s.number().optional(),
explore: s.array(ExploreItem).optional(),
courseHero: CourseHero.optional(),
source: NestedProperties.optional(),
// mirrors contentlayer2's `_raw.flattenedPath`: file path relative to the
// content dir, extension stripped, trailing `/index` dropped.
path: s.path(),
raw: s.raw(),
// internal doc cross-links (e.g. `[Button](components/button)`) aren't
// real files on disk — disable Velite's default asset-copying behavior,
// which otherwise treats every relative link as a local file to copy.
code: s.mdx({ copyLinkedFiles: false }),
})
.transform(({ path: flattenedPath, ...data }) => ({
...data,
slug: `/${flattenedPath}`,
slugAsParams: flattenedPath,
}))
export default defineConfig({
root: './content',
output: {
clean: true,
},
collections: {
allDocs: {
name: 'Doc',
pattern: '**/*.mdx',
schema: docs,
},
},
}))
const NestedProperties = defineNestedType(() => ({
name: 'NestedProperties',
fields: {
radix: {
type: 'boolean',
},
shadcn: {
type: 'boolean',
},
vaul: {
type: 'boolean',
},
inputOtp: {
type: 'boolean',
},
reactAccessibleTreeview: {
type: 'boolean',
},
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: `docs/**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
published: {
type: 'boolean',
default: true,
},
links: {
type: 'nested',
of: LinksProperties,
},
featured: {
type: 'boolean',
default: false,
required: false,
},
component: {
type: 'boolean',
default: false,
required: false,
},
fragment: {
type: 'boolean',
default: false,
required: false,
},
toc: {
type: 'boolean',
default: true,
required: false,
},
source: {
type: 'nested',
of: NestedProperties,
},
},
computedFields,
}))
export default makeSource({
contentDirPath: './content',
disableImportAliasWarning: true,
documentTypes: [Doc],
mdx: {
remarkPlugins: [remarkGfm, codeImport],
rehypePlugins: [
@@ -136,7 +105,6 @@ export default makeSource({
},
[
rehypePrettyCode,
// rehypePrettyCodeOptions,
{
getHighlighter: async () => {
const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json'))
@@ -186,7 +154,6 @@ export default makeSource({
}
})
},
// rehypeNpmCommand,
[
rehypeAutolinkHeadings,
{
+34
View File
@@ -770,6 +770,25 @@ export default defineConfig(({ command, mode }) => {
// with `c is not a function` because its `styleHandler` import
// came in through the now-too-large `lucide-react` chunk). Pin
// React explicitly so it stays a leaf vendor chunk.
//
// `packages/ui/src/components/shadcn/ui/field.tsx` — its only
// non-barrel importer is Storage's `FileExplorerHeader`, so
// Rolldown pools it into the storage bucket page chunk while the
// `ui` package barrel (`packages/ui/index.tsx`) re-exports it —
// `ui` ends up importing `FieldDescription` back from the page
// chunk it's itself imported by.
//
// `packages/ui/src/components/shadcn/ui/drawer.tsx` — same shape,
// its only non-barrel importer sits inside the Logs Explorer page
// tree (`DataTableFilterControlsDrawer`), so it gets pooled into
// the `logs` page chunk while `ui`'s barrel re-exports it too.
//
// `packages/ui/src/components/shadcn/ui/form.tsx` and
// `packages/ui/src/components/shadcn/ui/sidebar.tsx` (+
// `use-mobile.tsx`) — same shape again: each gets pooled into
// whichever page/feature chunk happens to be its only non-barrel
// importer (a form page, `components/interfaces/Sidebar.tsx`)
// while `ui`'s barrel re-exports them too.
build: {
rollupOptions: {
output: {
@@ -791,6 +810,21 @@ export default defineConfig(({ command, mode }) => {
if (id.includes('node_modules/lucide-react/')) {
return 'lucide-react'
}
if (id.includes('packages/ui/src/components/shadcn/ui/field.tsx')) {
return 'ui-field'
}
if (id.includes('packages/ui/src/components/shadcn/ui/drawer.tsx')) {
return 'ui-drawer'
}
if (id.includes('packages/ui/src/components/shadcn/ui/form.tsx')) {
return 'ui-form'
}
if (
id.includes('packages/ui/src/components/shadcn/ui/sidebar.tsx') ||
id.includes('packages/ui/src/components/hooks/use-mobile.tsx')
) {
return 'ui-sidebar'
}
return undefined
},
},
+1
View File
@@ -36,6 +36,7 @@ yarn-error.log*
next-env.d.ts
.contentlayer
.velite
# Auto-generated on every Vercel build by `pnpm build:llms` -> scripts/build-llms-txt.ts
public/llms.txt
@@ -1,13 +1,13 @@
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
import { allDocs } from 'contentlayer/generated'
import { ChevronRight } from 'lucide-react'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea } from 'ui'
import { allDocs } from '@/.velite'
import { metadata as mainMetadata } from '@/app/layout'
import { FrameworkSelector } from '@/components/framework-selector'
import { Mdx } from '@/components/mdx-components'
@@ -77,7 +77,7 @@ export default async function DocPage(props: DocPageProps) {
notFound()
}
const toc = await getTableOfContents(doc.body.raw)
const toc = await getTableOfContents(doc.raw)
return (
<main className="relative lg:gap-10 xl:grid xl:grid-cols-[1fr_200px] px-8 md:px-16 py-20">
@@ -100,7 +100,7 @@ export default async function DocPage(props: DocPageProps) {
</div>
<SourcePanel doc={doc} />
<div className="pb-12">
<Mdx code={doc.body.code} />
<Mdx code={doc.code} />
</div>
</div>
{doc.toc && (
@@ -1,4 +1,3 @@
import { useMDXComponent } from 'next-contentlayer2/hooks'
import Link from 'next/link'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, cn } from 'ui'
@@ -14,6 +13,7 @@ import { RegistryBlock } from './registry-block'
import { StyleWrapper } from './style-wrapper'
import TanStackBeta from './tanstack-beta'
import { TanstackDBGenerator } from './tanstack-db-generator'
import { useMDXComponent } from '@/lib/use-mdx-component'
import type { Style } from '@/registry/styles'
const components = {
+1 -1
View File
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { forwardRef } from 'react'
import { Button, cn } from 'ui'
import { Doc } from '@/.contentlayer/generated'
import { Doc } from '@/.velite'
const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> & { doc: Doc }>(
({ doc, children, ...props }, ref) => {
+16
View File
@@ -0,0 +1,16 @@
import * as React from 'react'
import * as runtime from 'react/jsx-runtime'
// Velite compiles MDX to a "function-body" (see velite.config.js `code: s.mdx()`)
// that reads its jsx runtime off `arguments[0]` — mirrors the pattern from
// https://velite.js.org/guide/using-mdx.
const EMPTY_GLOBALS: Record<string, unknown> = {}
function getMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
const fn = new Function(code)
return fn({ ...runtime, ...globals }).default
}
export function useMDXComponent(code: string, globals: Record<string, unknown> = EMPTY_GLOBALS) {
return React.useMemo(() => getMDXComponent(code, globals), [code, globals])
}
+1 -3
View File
@@ -1,5 +1,3 @@
import { withContentlayer } from 'next-contentlayer2'
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'],
@@ -37,4 +35,4 @@ const nextConfig = {
},
}
export default withContentlayer(nextConfig)
export default nextConfig
+10 -9
View File
@@ -5,21 +5,22 @@
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3004",
"predev": "pnpm run build:markdown",
"build": "pnpm run content:build && pnpm run build:registry && pnpm run build:markdown && pnpm run build:llms && pnpm run build:app",
"dev:content": "velite dev",
"dev:next": "next dev --port 3004",
"dev": "run-p build:content build:registry build:markdown && run-p --race dev:*",
"build:content": "velite build --strict",
"build:registry": "rimraf -G public/r/* && tsx ./scripts/build-registry.mts && shadcn build public/r/registry.json && tsx scripts/clean-registry.ts",
"build:markdown": "tsx ./scripts/build-markdown.ts",
"build:llms": "tsx ./scripts/build-llms-txt.ts",
"build:next": "next build --turbopack",
"build": "run-p build:content build:registry build:llms build:markdown && pnpm build:next",
"test:markdown": "tsx --test scripts/library-mdx-to-markdown.test.ts",
"test:middleware": "pnpm run build:markdown && tsx --test middleware.test.ts",
"build:app": "next build --turbopack",
"start": "next start",
"lint": "eslint .",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
"content:build": "contentlayer2 build",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite",
"typecheck": "pnpm build:content && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
@@ -37,7 +38,6 @@
"cmdk": "^1.1.1",
"common": "workspace:*",
"common-tags": "^1.8.2",
"contentlayer2": "0.4.6",
"eslint-config-supabase": "workspace:*",
"framer-motion": "^11.18.2",
"icons": "workspace:*",
@@ -45,7 +45,6 @@
"lucide-react": "*",
"monaco-editor": "catalog:",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "catalog:",
"openai": "^5.9.0",
"openapi-fetch": "0.12.4",
@@ -67,6 +66,7 @@
"ui-patterns": "workspace:*",
"unist-util-visit": "^5.0.0",
"vaul": "^1.1.2",
"velite": "catalog:",
"y-monaco": "^0.1.6",
"y-protocols": "^1.0.7",
"yjs": "^13.6.29",
@@ -97,6 +97,7 @@
"mdast-util-toc": "^6.1.1",
"micromark-extension-gfm": "^2.0.3",
"micromark-extension-mdxjs": "^1.0.1",
"npm-run-all": "^4.1.5",
"postcss": "catalog:",
"react-dropzone": "^14.3.8",
"react-router": "^7.13.2",
+2 -9
View File
@@ -5,18 +5,11 @@
"allowJs": false,
"lib": ["dom", "dom.iterable", "esnext"],
"paths": {
"@/*": ["./*"],
"contentlayer/generated": ["./.contentlayer/generated"]
"@/*": ["./*"]
},
"plugins": [{ "name": "next" }]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".velite"],
"exclude": [
"node_modules",
"./scripts/build-registry.mts",
+1 -1
View File
@@ -29,7 +29,7 @@
".next/**",
"!.next/cache/**",
"!.next/dev/**/*",
".contentlayer/**",
".velite/**",
"public/markdown/**",
],
},
@@ -1,122 +1,68 @@
import path from 'path'
import { getHighlighter, loadTheme } from '@shikijs/compat'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { codeImport } from 'remark-code-import'
import remarkGfm from 'remark-gfm'
import { visit } from 'unist-util-visit'
import { defineConfig, s } from 'velite'
import { rehypeComponent } from './lib/rehype-component'
import { rehypeNpmCommand } from './lib/rehype-npm-command'
const LinksProperties = s.object({
doc: s.string().optional(),
api: s.string().optional(),
})
/** @type {import('contentlayer2/source-files').ComputedFields} */
const computedFields = {
slug: {
type: 'string',
resolve: (doc) => `/${doc._raw.flattenedPath}`,
const NestedProperties = s.object({
radix: s.boolean().optional(),
shadcn: s.boolean().optional(),
vaul: s.boolean().optional(),
inputOtp: s.boolean().optional(),
reactAccessibleTreeview: s.boolean().optional(),
})
const docs = s
.object({
title: s.string(),
description: s.string(),
published: s.boolean().default(true),
links: LinksProperties.optional(),
featured: s.boolean().default(false),
component: s.boolean().default(false),
fragment: s.boolean().default(false),
toc: s.boolean().default(true),
source: NestedProperties.optional(),
// mirrors contentlayer2's `_raw.flattenedPath`: file path relative to the
// content dir, extension stripped, trailing `/index` dropped.
path: s.path(),
raw: s.raw(),
// internal doc cross-links (e.g. `[Button](components/button)`) aren't
// real files on disk — disable Velite's default asset-copying behavior,
// which otherwise treats every relative link as a local file to copy.
code: s.mdx({ copyLinkedFiles: false }),
})
.transform(({ path: flattenedPath, ...data }) => ({
...data,
slug: `/${flattenedPath}`,
slugAsParams: flattenedPath.split('/').slice(1).join('/'),
}))
export default defineConfig({
root: './content',
output: {
clean: true,
},
slugAsParams: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath.split('/').slice(1).join('/'),
},
}
const LinksProperties = defineNestedType(() => ({
name: 'LinksProperties',
fields: {
doc: {
type: 'string',
},
api: {
type: 'string',
collections: {
allDocs: {
name: 'Doc',
pattern: 'docs/**/*.mdx',
schema: docs,
},
},
}))
const NestedProperties = defineNestedType(() => ({
name: 'NestedProperties',
fields: {
radix: {
type: 'boolean',
},
shadcn: {
type: 'boolean',
},
vaul: {
type: 'boolean',
},
inputOtp: {
type: 'boolean',
},
reactAccessibleTreeview: {
type: 'boolean',
},
recharts: {
type: 'boolean',
},
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: `docs/**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
published: {
type: 'boolean',
default: true,
},
links: {
type: 'nested',
of: LinksProperties,
},
featured: {
type: 'boolean',
default: false,
required: false,
},
component: {
type: 'boolean',
default: false,
required: false,
},
fragment: {
type: 'boolean',
default: false,
required: false,
},
toc: {
type: 'boolean',
default: true,
required: false,
},
source: {
type: 'nested',
of: NestedProperties,
},
},
computedFields,
}))
export default makeSource({
contentDirPath: './content',
disableImportAliasWarning: true,
documentTypes: [Doc],
mdx: {
remarkPlugins: [remarkGfm, codeImport],
rehypePlugins: [
rehypeSlug,
rehypeComponent,
() => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'pre') {
@@ -143,7 +89,6 @@ export default makeSource({
},
[
rehypePrettyCode,
// rehypePrettyCodeOptions,
{
getHighlighter: async () => {
const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json'))
@@ -193,7 +138,6 @@ export default makeSource({
}
})
},
// rehypeNpmCommand,
[
rehypeAutolinkHeadings,
{
+1
View File
@@ -106,6 +106,7 @@ module.exports = defineConfig([
'vitest.config.ts',
'postcss.config.js',
'postcss.config.cjs',
'velite.config.js',
],
rules: {
'no-restricted-exports': [
-1
View File
@@ -13,7 +13,6 @@ export * from './src/components/NavMenu'
export * from './src/components/SidePanel'
// HTML
export {
Heading,
getAnchor,
+545 -1521
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -49,6 +49,7 @@ catalog:
typescript: ~6.0.2
valtio: ^2.3.2
vite: ^8.0.16
velite: ^0.4.0
vite-tsconfig-paths: ^6.1.1
vitest: ^4.1.4
zod: 3.25.76