mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## 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 -->
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import fs from 'fs'
|
|
import path from 'path'
|
|
// import { Column, IColumnProps } from './sample-component'
|
|
import React from 'react'
|
|
import { u } from 'unist-builder'
|
|
import { visit } from 'unist-util-visit'
|
|
|
|
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) => {
|
|
// src prop overrides both name and fileName.
|
|
const { value: srcPath, name: componentName } =
|
|
(getNodeAttributeByName(node, 'src') as {
|
|
name: string
|
|
value?: string
|
|
type?: string
|
|
}) || {}
|
|
|
|
if (node.name === 'ComponentSource') {
|
|
// console.log('DO NOT USE THIS COMPONENT')
|
|
// This component should not be in use!
|
|
// console.log('node', node)
|
|
|
|
const name = getNodeAttributeByName(node, 'name')?.value as string
|
|
const fileName = getNodeAttributeByName(node, 'fileName')?.value as string | undefined
|
|
|
|
if (!name && !srcPath) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
for (const style of styles) {
|
|
let src: string
|
|
|
|
if (srcPath) {
|
|
src = srcPath
|
|
} else {
|
|
const files = getRegistryFiles(style.name, name)
|
|
// console.log('got to ELSE STATEMENT')
|
|
// console.log('filename', fileName)
|
|
// console.log('name', name)
|
|
|
|
src = fileName
|
|
? files.find((file: string) => {
|
|
return file.endsWith(`${fileName}.tsx`) || file.endsWith(`${fileName}.ts`)
|
|
}) || files[0]
|
|
: files[0]
|
|
// console.log('got to END of ELSE STATEMENT')
|
|
}
|
|
|
|
// Read the source file.
|
|
const filePath = path.join(process.cwd(), src)
|
|
let source = fs.readFileSync(filePath, 'utf8')
|
|
|
|
// Replace imports.
|
|
// TODO: Use @swc/core and a visitor to replace this.
|
|
// For now a simple regex should do.
|
|
// source = source.replaceAll(`@/registry/${style.name}/`, '@/components/') // COMMENT THEE OUT
|
|
// source = source.replaceAll('export default', 'export')
|
|
|
|
// Add code as children so that rehype can take over at build time.
|
|
node.children?.push(
|
|
u('element', {
|
|
tagName: 'pre',
|
|
properties: {
|
|
__src__: src,
|
|
__style__: style.name,
|
|
},
|
|
attributes: [
|
|
{
|
|
name: 'styleName',
|
|
type: 'mdxJsxAttribute',
|
|
value: style.name,
|
|
},
|
|
],
|
|
children: [
|
|
u('element', {
|
|
tagName: 'code',
|
|
properties: {
|
|
className: ['language-tsx'],
|
|
},
|
|
children: [
|
|
{
|
|
type: 'text',
|
|
value: source,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
})
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
if (node.name === 'ComponentPreview') {
|
|
const name = getNodeAttributeByName(node, 'name')?.value as string
|
|
|
|
if (!name) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
for (const style of styles) {
|
|
const files = getRegistryFiles(style.name, name)
|
|
// console.log('GOT HERE')
|
|
const src = files[0]
|
|
|
|
// Read the source file.
|
|
const filePath = path.join(process.cwd(), src)
|
|
let source = fs.readFileSync(filePath, 'utf8')
|
|
|
|
// Replace imports.
|
|
// TODO: Use @swc/core and a visitor to replace this.
|
|
// For now a simple regex should do.
|
|
source = source.replaceAll(`@/registry/${style.name}/`, '@/components/')
|
|
source = source.replaceAll('export default', 'export')
|
|
|
|
// Add code as children so that rehype can take over at build time.
|
|
node.children?.push(
|
|
u('element', {
|
|
tagName: 'pre',
|
|
properties: {
|
|
__src__: src,
|
|
},
|
|
children: [
|
|
u('element', {
|
|
tagName: 'code',
|
|
properties: {
|
|
className: ['language-tsx'],
|
|
},
|
|
children: [
|
|
{
|
|
type: 'text',
|
|
value: source,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
})
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
if (node.name === 'CodeFragment') {
|
|
const name = getNodeAttributeByName(node, 'name')?.value as string
|
|
|
|
if (!name) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
for (const style of styles) {
|
|
const files = getRegistryFiles(style.name, name)
|
|
// console.log('GOT HERE')
|
|
const src = files[0]
|
|
|
|
// Read the source file.
|
|
const filePath = path.join(process.cwd(), src)
|
|
let source = fs.readFileSync(filePath, 'utf8')
|
|
|
|
// Replace imports.
|
|
// TODO: Use @swc/core and a visitor to replace this.
|
|
// For now a simple regex should do.
|
|
source = source.replaceAll(`@/registry/${style.name}/`, '@/components/')
|
|
source = source.replaceAll('export default', 'export')
|
|
|
|
// Add code as children so that rehype can take over at build time.
|
|
node.children?.push(
|
|
u('element', {
|
|
tagName: 'pre',
|
|
properties: {
|
|
__src__: src,
|
|
},
|
|
children: [
|
|
u('element', {
|
|
tagName: 'code',
|
|
properties: {
|
|
className: ['language-tsx'],
|
|
},
|
|
children: [
|
|
{
|
|
type: 'text',
|
|
value: source,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
})
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
// if (node.name === "ComponentExample") {
|
|
// const source = getComponentSourceFileContent(node)
|
|
// if (!source) {
|
|
// return
|
|
// }
|
|
|
|
// // Replace the Example component with a pre element.
|
|
// node.children?.push(
|
|
// u("element", {
|
|
// tagName: "pre",
|
|
// properties: {
|
|
// __src__: src,
|
|
// },
|
|
// children: [
|
|
// u("element", {
|
|
// tagName: "code",
|
|
// properties: {
|
|
// className: ["language-tsx"],
|
|
// },
|
|
// children: [
|
|
// {
|
|
// type: "text",
|
|
// value: source,
|
|
// },
|
|
// ],
|
|
// }),
|
|
// ],
|
|
// })
|
|
// )
|
|
|
|
// const extractClassname = getNodeAttributeByName(
|
|
// node,
|
|
// "extractClassname"
|
|
// )
|
|
// if (
|
|
// extractClassname &&
|
|
// typeof extractClassname.value !== "undefined" &&
|
|
// extractClassname.value !== "false"
|
|
// ) {
|
|
// // Extract className from string
|
|
// // TODO: Use @swc/core and a visitor to extract this.
|
|
// // For now, a simple regex should do.
|
|
// const values = source.match(/className="(.*)"/)
|
|
// const className = values ? values[1] : ""
|
|
|
|
// // Add the className as a jsx prop so we can pass it to the copy button.
|
|
// node.attributes?.push({
|
|
// name: "extractedClassNames",
|
|
// type: "mdxJsxAttribute",
|
|
// value: className,
|
|
// })
|
|
|
|
// // Add a pre element with the className only.
|
|
// node.children?.push(
|
|
// u("element", {
|
|
// tagName: "pre",
|
|
// properties: {},
|
|
// children: [
|
|
// u("element", {
|
|
// tagName: "code",
|
|
// properties: {
|
|
// className: ["language-tsx"],
|
|
// },
|
|
// children: [
|
|
// {
|
|
// type: "text",
|
|
// value: className,
|
|
// },
|
|
// ],
|
|
// }),
|
|
// ],
|
|
// })
|
|
// )
|
|
// }
|
|
// }
|
|
|
|
// if (node.name === "ComponentSource") {
|
|
// const source = getComponentSourceFileContent(node)
|
|
// if (!source) {
|
|
// return
|
|
// }
|
|
|
|
// // Replace the Source component with a pre element.
|
|
// node.children?.push(
|
|
// u("element", {
|
|
// tagName: "pre",
|
|
// properties: {
|
|
// __src__: src,
|
|
// },
|
|
// children: [
|
|
// u("element", {
|
|
// tagName: "code",
|
|
// properties: {
|
|
// className: ["language-tsx"],
|
|
// },
|
|
// children: [
|
|
// {
|
|
// type: "text",
|
|
// value: source,
|
|
// },
|
|
// ],
|
|
// }),
|
|
// ],
|
|
// })
|
|
// )
|
|
// }
|
|
})
|
|
}
|
|
}
|
|
|
|
function getNodeAttributeByName(node: UnistNode, name: string) {
|
|
return node.attributes?.find((attribute) => attribute.name === name)
|
|
}
|
|
|
|
function getComponentSourceFileContent(node: UnistNode) {
|
|
const src = getNodeAttributeByName(node, 'src')?.value as string
|
|
|
|
if (!src) {
|
|
return null
|
|
}
|
|
|
|
// Read the source file.
|
|
const filePath = path.join(process.cwd(), src)
|
|
console.log('filePath', filePath)
|
|
const source = fs.readFileSync(filePath, 'utf8')
|
|
|
|
return source
|
|
}
|