+
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
785 changes: 766 additions & 19 deletions scripts/build-docs.test.ts

Large diffs are not rendered by default.

16 changes: 7 additions & 9 deletions scripts/build-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async function main() {
docsPath: '../docs',
baseDocsLink: '/docs/',
manifestPath: '../docs/manifest.json',
partialsPath: '../docs/_partials',
partialsFolderName: '_partials',
distPath: '../dist',
typedocPath: '../clerk-typedoc',
localTypedocOverridePath: '../local-clerk-typedoc',
Expand Down Expand Up @@ -605,8 +605,6 @@ export async function build(config: BuildConfig, store: Store = createBlankStore

const validatedPartials = await Promise.all(
partials.map(async (partial) => {
const partialPath = `${config.partialsRelativePath}/${partial.path}`

try {
let node: Node | null = null
const links: Set<string> = new Set()
Expand All @@ -615,23 +613,23 @@ export async function build(config: BuildConfig, store: Store = createBlankStore
.use(remarkFrontmatter)
.use(remarkMdx)
.use(
validateLinks(config, docsMap, partialPath, 'partials', (linkInPartial) => {
validateLinks(config, docsMap, partial.path, 'partials', (linkInPartial) => {
links.add(linkInPartial)
}),
)
.use(() => (tree, vfile) => {
.use(() => (tree) => {
node = tree
})
.process(partial.vfile)
.process({ path: partial.vfile.path, value: partial.content })

if (node === null) {
throw new Error(errorMessages['partial-parse-error'](partial.path))
}

return {
...partial,
node: node as Node,
vfile,
node: partial.node, // Use the embedded node (with nested includes)
vfile, // Use the vfile from validation
links,
}
} catch (error) {
Expand Down Expand Up @@ -1146,7 +1144,7 @@ ${yaml.stringify({
})
const mdxFilePaths = mdxFiles
.map((entry) => entry.path.replace(/\\/g, '/')) // Replace backslashes with forward slashes
.filter((filePath) => !filePath.startsWith(config.partialsRelativePath)) // Exclude partials
.filter((filePath) => !filePath.includes(config.partialsFolderName)) // Exclude partials
.map((path) => ({
path,
url: `${config.baseDocsLink}${removeMdxSuffix(path)
Expand Down
5 changes: 2 additions & 3 deletions scripts/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type BuildConfigOptions = {
docsPath: string
baseDocsLink: string
manifestPath: string
partialsPath: string
partialsFolderName: string
distPath: string
typedocPath: string
localTypedocOverridePath?: string
Expand Down Expand Up @@ -98,8 +98,7 @@ export async function createConfig(config: BuildConfigOptions) {
manifestRelativePath: config.manifestPath,
manifestFilePath: resolve(config.manifestPath),

partialsRelativePath: config.partialsPath,
partialsPath: resolve(config.partialsPath),
partialsFolderName: config.partialsFolderName,

dataRelativePath: config.dataPath,
dataPath: resolve(config.dataPath),
Expand Down
14 changes: 8 additions & 6 deletions scripts/lib/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export const errorMessages = {
`Doc "${href}" contains a duplicate heading id "${id}", please ensure all heading ids are unique`,

// Include component errors
'include-src-not-partials': (): string => `<Include /> prop "src" must start with "_partials/"`,
'include-src-not-partials': (): string =>
`<Include /> prop "src" must start with "_partials/" (global) or "./_partials/" or "../_partials/" (relative)`,
'partial-not-found': (src: string): string => `Partial /docs/${src}.mdx not found`,
'partials-inside-partials': (): string =>
'Partials inside of partials is not yet supported (this is a bug with the build script, please report)',
Expand Down Expand Up @@ -98,13 +99,14 @@ export const shouldIgnoreWarning = (
warningCode: WarningCode,
): boolean => {
const replacements = {
docs: config.baseDocsLink,
partials: config.partialsRelativePath + '/',
typedoc: config.typedocRelativePath + '/',
docs: (filePath: string) => filePath.replace(config.baseDocsLink, ''),
typedoc: (filePath: string) => filePath.replace(config.typedocRelativePath + '/', ''),
partials: (filePath: string) => filePath,
tooltips: (filePath: string) =>
config.tooltips ? filePath.replace(config.tooltips.inputPathRelative + '/', '') : filePath,
}

const relativeFilePath = filePath.replace(replacements[section], '')

const relativeFilePath = replacements[section](filePath)
const ignoreList = config.ignoreWarnings[section][relativeFilePath]

if (!ignoreList) {
Expand Down
4 changes: 3 additions & 1 deletion scripts/lib/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const readDocsFolder = (config: BuildConfig) => async () => {
type: 'files',
fileFilter: (entry) =>
// Partials are inside the docs folder, so we need to exclude them
`${config.docsRelativePath}/${entry.path}`.startsWith(config.partialsRelativePath) === false &&
// Exclude global _partials folder and any relative _partials folders
!entry.path.startsWith(`${config.partialsFolderName}/`) &&
!entry.path.includes(`/${config.partialsFolderName}/`) &&
// Tooltips are inside the docs folder too, also ignore them as they are not full pages
(config.tooltips?.inputPathRelative
? `${config.docsRelativePath}/${entry.path}`.startsWith(config.tooltips.inputPathRelative) === false
Expand Down
201 changes: 170 additions & 31 deletions scripts/lib/partials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// responsible for reading in and parsing the partials markdown
// for validation see validators/checkPartials.ts
// for partials we currently do not allow them to embed other partials
// partials can now embed other partials recursively
// this also removes the .mdx suffix from the urls in the markdown

import path from 'node:path'
Expand All @@ -10,22 +10,45 @@ import remarkFrontmatter from 'remark-frontmatter'
import remarkMdx from 'remark-mdx'
import type { Node } from 'unist'
import { visit as mdastVisit } from 'unist-util-visit'
import reporter from 'vfile-reporter'
import type { BuildConfig } from './config'
import { errorMessages, safeFail } from './error-messages'
import { errorMessages } from './error-messages'
import { readMarkdownFile } from './io'
import { removeMdxSuffixPlugin } from './plugins/removeMdxSuffixPlugin'
import { getPartialsCache, type Store } from './store'
import { getPartialsCache, markDocumentDirty, type Store } from './store'
import { extractComponentPropValueFromNode } from './utils/extractComponentPropValueFromNode'
import { removeMdxSuffix } from './utils/removeMdxSuffix'
import { z } from 'zod'

export const readPartialsFolder = (config: BuildConfig) => async () => {
return readdirp.promise(config.partialsPath, {
// Read all partials from the docs directory, including:
// 1. Global partials in /docs/_partials/
// 2. Relative partials in any subdirectory's _partials folder (e.g., /docs/billing/_partials/)
const files = await readdirp.promise(config.docsPath, {
type: 'files',
fileFilter: '*.mdx',
fileFilter: (entry) => {
// Only include .mdx files that are inside any _partials folder
// Check for both "/_partials/" (relative partials) and starting with "_partials/" (global partials)
return (entry.path.includes('/_partials/') || entry.path.startsWith('_partials/')) && entry.path.endsWith('.mdx')
},
})

return files
}

export const readPartial = (config: BuildConfig) => async (filePath: string) => {
const fullPath = path.join(config.partialsPath, filePath)
export const readPartial = (config: BuildConfig, store: Store) => async (filePath: string) => {
const setDirty = markDocumentDirty(store)

// filePath can be:
// 1. Global partial: "_partials/billing/enable-billing.mdx" -> /docs/_partials/billing/enable-billing.mdx
// 2. Relative partial: "billing/_partials/local.mdx" -> /docs/billing/_partials/local.mdx
const isRelativePartial = filePath.includes('/_partials/') && !filePath.startsWith('_partials/')
const isGlobalPartial = filePath.startsWith('_partials/')

if (!isRelativePartial && !isGlobalPartial) {
throw new Error(`Invalid partial path: ${filePath}. Must start with "_partials/" or contain "/_partials/"`)
}

const fullPath = path.join(config.docsPath, filePath)

const [error, content] = await readMarkdownFile(fullPath)

Expand All @@ -36,45 +59,161 @@ export const readPartial = (config: BuildConfig) => async (filePath: string) =>
let partialNode: Node | null = null

try {
const partialContentVFile = await remark()
const vfile = await remark()
.use(remarkFrontmatter)
.use(remarkMdx)
.use(() => (tree) => {
partialNode = tree
})
.use(() => (tree, vfile) => {
mdastVisit(
tree,
(node) =>
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
'name' in node &&
node.name === 'Include',
(node) => {
safeFail(config, vfile, fullPath, 'partials', 'partials-inside-partials', [], node.position)
},
)
})
.use(removeMdxSuffixPlugin(config))
.process({
path: `docs/_partials/${filePath}`,
path: `docs/${filePath}`,
value: content,
})

const partialContentReport = reporter([partialContentVFile], { quiet: true })

if (partialContentReport !== '') {
console.error(partialContentReport)
process.exit(1)
}

if (partialNode === null) {
throw new Error(errorMessages['partial-parse-error'](filePath))
}

// Handle nested partials by finding and replacing Include nodes
// We need to handle this carefully because we need to splice in multiple children
let hasIncludes = false
mdastVisit(partialNode as Node, (node) => {
if (
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
'name' in node &&
node.name === 'Include'
) {
hasIncludes = true
}
})

if (hasIncludes) {
// Collect all Include nodes and their paths
const includesToReplace: Array<{ src: string; path: string }> = []

mdastVisit(partialNode as Node, (node) => {
const isIncludeComponent =
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
'name' in node &&
node.name === 'Include'

if (!isIncludeComponent) return

const partialSrc = extractComponentPropValueFromNode(
config,
node,
undefined,
'Include',
'src',
false,
'partials',
filePath,
z.string(),
)

if (!partialSrc) return

// Resolve the nested partial path
let nestedPath: string

if (partialSrc.startsWith('./') || partialSrc.startsWith('../')) {
const parentDir = path.dirname(filePath)
nestedPath = path.normalize(path.join(parentDir, `${removeMdxSuffix(partialSrc)}.mdx`)).replace(/\\/g, '/')
} else if (partialSrc.startsWith('_partials/')) {
nestedPath = `${removeMdxSuffix(partialSrc)}.mdx`
} else {
return
}

// Check for circular dependency
if (nestedPath === filePath) {
throw new Error(`Circular dependency detected: partial ${filePath} includes itself`)
}

includesToReplace.push({ src: partialSrc, path: nestedPath })
})

// Load all nested partials
const uniquePaths = Array.from(new Set(includesToReplace.map((i) => i.path)))
const partialsMap = new Map(
(
await Promise.all(
uniquePaths.map(async (nestedPath) => {
try {
const nestedPartial = await readPartial(config, store)(nestedPath)
// Track the dependency: when nestedPath changes, the parent partial should be invalidated
// dirtyDocMap convention for partials:
// - Keys: relative paths (e.g., "guides/_partials/child.mdx")
// - Values: absolute system paths (e.g., "/var/.../docs/guides/_partials/parent.mdx")
// nestedPath is already relative: "guides/_partials/child.mdx"
// fullPath is the absolute system path of the parent
setDirty(fullPath, nestedPath)
return [nestedPath, nestedPartial.node] as const
} catch (error) {
console.error(`Failed to load nested partial ${nestedPath}:`, error)
return null
}
}),
)
).filter((p) => p !== null),
)

// Now replace Include nodes with their content
// We need to traverse the tree and replace nodes in the children arrays
const replaceIncludes = (node: any): any => {
if ('children' in node && Array.isArray(node.children)) {
const newChildren: Node[] = []

for (const child of node.children) {
const isInclude =
(child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') &&
'name' in child &&
child.name === 'Include'

if (isInclude) {
const partialSrc = extractComponentPropValueFromNode(
config,
child,
undefined,
'Include',
'src',
false,
'partials',
filePath,
z.string(),
)

if (partialSrc) {
const include = includesToReplace.find((i) => i.src === partialSrc)
if (include) {
const loadedNode = partialsMap.get(include.path)
if (loadedNode && loadedNode.type === 'root' && 'children' in loadedNode) {
// Splice in the children of the root node
newChildren.push(...(loadedNode.children as Node[]))
continue
}
}
}
}

// Not an Include or couldn't replace - keep the node and recurse
newChildren.push(replaceIncludes(child))
}

return { ...node, children: newChildren }
}

return node
}

partialNode = replaceIncludes(partialNode)
}

return {
path: filePath,
content,
vfile: partialContentVFile,
vfile,
node: partialNode as Node,
}
} catch (error) {
Expand All @@ -84,7 +223,7 @@ export const readPartial = (config: BuildConfig) => async (filePath: string) =>
}

export const readPartialsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => {
const read = readPartial(config)
const read = readPartial(config, store)
const partialsCache = getPartialsCache(store)

return Promise.all(paths.map(async (markdownPath) => partialsCache(markdownPath, () => read(markdownPath))))
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载