Zeta: Support build-page-tree API

This commit is contained in:
SonMooSans
2023-10-21 22:29:47 +08:00
parent fa615ce668
commit 24245a3525
12 changed files with 203 additions and 111 deletions
+12
View File
@@ -0,0 +1,12 @@
---
'next-docs-zeta': major
---
Support build-page-tree API
The old `buildPageTree` function provided by 'next-docs-zeta/contentlayer' is now removed. Please use build-page-tree API directly.
```diff
- import { buildPageTree } from 'next-docs-zeta/contentlayer'
+ import { buildPageTree } from 'next-docs-zeta/build-page-tree'
```
+2 -5
View File
@@ -1,9 +1,6 @@
import { allDocs, allMeta } from 'contentlayer/generated'
import {
buildPageTree,
createUtils,
loadContext
} from 'next-docs-zeta/contentlayer'
import { buildPageTree } from 'next-docs-zeta/build-page-tree'
import { createUtils, loadContext } from 'next-docs-zeta/contentlayer'
import type { PageTree } from 'next-docs-zeta/server'
const ctx = loadContext(allMeta, allDocs)
@@ -1,5 +1,4 @@
import { getPage, getPageUrl, tree } from '@/app/source'
import { allDocs } from 'contentlayer/generated'
import { allPages, getPage, getPageUrl, tree } from '@/app/source'
import type { Metadata } from 'next'
import { MDXContent } from 'next-docs-ui/mdx'
import { DocsPage } from 'next-docs-ui/page'
@@ -31,8 +30,8 @@ export default async function Page({
}
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
return allDocs.map(page => ({
slug: page.slug.split('/')
return allPages.map(page => ({
slug: page.slugs
}))
}
+4
View File
@@ -18,6 +18,10 @@
"import": "./dist/breadcrumb.mjs",
"types": "./dist/breadcrumb.d.ts"
},
"./build-page-tree": {
"import": "./dist/build-page-tree.mjs",
"types": "./dist/build-page-tree.d.ts"
},
"./toc": {
"import": "./dist/toc.mjs",
"types": "./dist/toc.d.ts"
@@ -1,9 +1,58 @@
import type { FileNode, FolderNode, PageTree, TreeNode } from '../server'
import type { DocsPageBase, MetaPageBase, PagesContext } from './types'
import { getKey, pathToName } from './utils'
import type { ReactElement } from 'react'
import type { FileNode, FolderNode, PageTree, TreeNode } from './server/types'
type Context = PagesContext & {
export type File = {
/**
* Original path of file
*/
path: string
/**
* File path without extension & prefix
*/
flattenedPath: string
/**
* File name without extension
*/
name: string
dirname: string
}
export type AbstractMeta = {
file: File
icon?: string
title?: string
pages: string[]
}
export type AbstractPage = {
file: File
icon?: string
title: string
url: string
}
export type Context = {
lang?: string
getMetaByPath: (flattenPath: string) => AbstractMeta | null
getPageByPath: (flattenPath: string) => AbstractPage | null
resolveIcon: (icon: string) => ReactElement | undefined
/**
* Default pages without specified langauge
*/
basePages: AbstractPage[]
}
type InternationalizedContext<Languages extends string> = Omit<
Context,
'lang'
> & {
languages: Languages[]
}
export type Options = {
@@ -18,8 +67,8 @@ const separator = /---(.*?)---/
const rest = '...'
const extractor = /\.\.\.(.+)/
function buildMeta(meta: MetaPageBase, ctx: Context): FolderNode {
const segments = meta._raw.sourceFileDir.split('/')
function buildMeta(meta: AbstractMeta, ctx: Context): FolderNode {
const segments = meta.file.dirname.split('/')
let index: FileNode | undefined = undefined
const filtered = new Set<string>()
@@ -41,12 +90,12 @@ function buildMeta(meta: MetaPageBase, ctx: Context): FolderNode {
item = extract_result[1]
}
const path = meta._raw.sourceFileDir + '/' + item
const page = ctx.docsMap.get(path)
const path = meta.file.dirname + '/' + item
const page = ctx.getPageByPath(path)
if (page != null) {
const node = buildFileNode(page, ctx)
filtered.add(page._raw.sourceFilePath)
filtered.add(page.file.path)
if (item === 'index') index = node
return node
@@ -73,7 +122,7 @@ function buildMeta(meta: MetaPageBase, ctx: Context): FolderNode {
if (item === '...') {
return getFolderNodes(
ctx,
meta._raw.sourceFileDir,
meta.file.dirname,
false,
path => !filtered.has(path)
).children
@@ -83,16 +132,16 @@ function buildMeta(meta: MetaPageBase, ctx: Context): FolderNode {
})
if (index == null) {
const page = ctx.docsMap.get(meta._raw.sourceFileDir + '/index')
const page = ctx.getPageByPath(meta.file.dirname + '/index')
if (page != null) index = buildFileNode(page, ctx)
}
return {
name: meta.title ?? pathToName(segments),
index,
type: 'folder',
icon: meta.icon && ctx.resolveIcon ? ctx.resolveIcon(meta.icon) : undefined,
name: meta.title ?? index?.name ?? pathToName(segments),
icon: meta.icon ? ctx.resolveIcon(meta.icon) : undefined,
index,
children
}
}
@@ -109,17 +158,15 @@ function getFolderNodes(
path: string,
joinIndex: boolean,
filter: (path: string) => boolean = () => true
): { index: FileNode | null; children: TreeNode[] } {
const pages = ctx.pages.get(ctx.lang ?? '') ?? []
let index: FileNode | null = null
): { index?: FileNode; children: TreeNode[] } {
let index: FileNode | undefined
const children: TreeNode[] = []
for (const page of pages) {
if (page._raw.sourceFileDir !== path || !filter(page._raw.sourceFilePath))
continue
for (const page of ctx.basePages) {
if (page.file.dirname !== path || !filter(page.file.path)) continue
const node = buildFileNode(page, ctx)
if (page._raw.flattenedPath === path) {
if (page.file.name === 'index') {
index = node
continue
}
@@ -127,13 +174,18 @@ function getFolderNodes(
children.push(node)
}
const segmentIndex = path.split('/').length
const segments = path.split('/')
const folders = new Set<string>(
pages
.filter(page => page._raw.sourceFileDir.startsWith(path + '/'))
.map(
page => path + '/' + page._raw.sourceFileDir.split('/')[segmentIndex]
)
ctx.basePages.flatMap(page => {
const dirnameSegments = page.file.dirname.split('/')
if (path.length === 0 && path !== page.file.dirname)
return dirnameSegments[0]
if (page.file.dirname.startsWith(path + '/'))
return path + '/' + dirnameSegments[segments.length]
return []
})
)
for (const folder of folders) {
@@ -148,16 +200,16 @@ function getFolderNodes(
return { index, children }
}
function buildFileNode(page: DocsPageBase, ctx: Context): FileNode {
function buildFileNode(page: AbstractPage, ctx: Context): FileNode {
if (ctx.lang) {
page = ctx.docsMap.get(getKey(page) + `.${ctx.lang}`) ?? page
page = ctx.getPageByPath(page.file.flattenedPath + `.${ctx.lang}`) ?? page
}
return {
type: 'page',
name: page.title,
url: ctx.getUrl(page.slug.split('/'), ctx.lang),
icon: page.icon && ctx.resolveIcon ? ctx.resolveIcon(page.icon) : undefined
icon: page.icon ? ctx.resolveIcon(page.icon) : undefined,
url: page.url
}
}
@@ -166,20 +218,20 @@ function buildFolderNode(
ctx: Context,
keepIndex: boolean = false
): FolderNode {
let meta = ctx.lang ? ctx.metaMap.get(path + `/meta-${ctx.lang}`) : null
meta ??= ctx.metaMap.get(path + '/meta')
let meta: AbstractMeta | null = null
if (ctx.lang) meta = ctx.getMetaByPath(path + `/meta-${ctx.lang}`)
meta ??= ctx.getMetaByPath(path + '/meta')
if (meta != null) {
return buildMeta(meta, ctx)
}
const segments = path.split('/')
const { index, children } = getFolderNodes(ctx, path, keepIndex)
return {
name: index?.name ?? pathToName(segments),
type: 'folder',
index: index ?? undefined,
name: index?.name ?? pathToName(path.split('/')),
index,
children
}
}
@@ -189,13 +241,12 @@ function build(root: string, ctx: Context): PageTree {
return {
name: folder.name,
url: ctx.getUrl([], ctx.lang),
children: folder.children
}
}
export function buildPageTree(
context: PagesContext,
context: Context,
{ root = 'docs' }: Partial<Options> = {}
): PageTree {
return build(root, context)
@@ -209,10 +260,10 @@ export function buildPageTree(
* @param languages All supported languages
*/
export function buildI18nPageTree<Languages extends string = string>(
context: PagesContext,
{ languages, ...context }: InternationalizedContext<Languages>,
{ root = 'docs' }: Partial<Options> = {}
): Record<Languages, PageTree> {
const entries = context.languages.map(lang => {
const entries = languages.map(lang => {
const tree = build(root, {
...context,
lang
@@ -223,3 +274,8 @@ export function buildI18nPageTree<Languages extends string = string>(
return Object.fromEntries(entries)
}
function pathToName(path: string[]): string {
const name = path[path.length - 1] ?? 'docs'
return name.slice(0, 1).toUpperCase() + name.slice(1)
}
@@ -1,5 +1,5 @@
import { buildI18nPageTree, buildPageTree } from '@/build-page-tree'
import type { PageTree } from '@/server/types'
import { buildI18nPageTree, buildPageTree } from './build-tree'
import { loadContext, type ContextOptions } from './load-context'
import type { DocsPageBase, MetaPageBase, PagesContext } from './types'
import { createUtils, type ContentlayerUtils } from './utils'
@@ -36,8 +36,8 @@ export const createContentlayer: CreateContentlayer = (
return {
__pageContext: ctx,
tree: (options?.languages != null
? buildI18nPageTree(ctx)
tree: (options.languages != null
? buildI18nPageTree({ ...ctx, languages: options.languages })
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
buildPageTree(ctx)) as any,
...createUtils(ctx)
@@ -1,5 +1,4 @@
export * from './load-context'
export * from './build-tree'
export { createUtils, ContentlayerUtils } from './utils'
export { createContentlayer } from './create-contentlayer'
@@ -1,58 +1,96 @@
import type { AbstractMeta, AbstractPage, File } from '@/build-page-tree'
import type { RawDocumentData } from 'contentlayer/source-files'
import type { ReactElement } from 'react'
import type { DocsPageBase, MetaPageBase, PagesContext } from './types'
import { getKey, getPageUrl } from './utils'
import { getPageUrl } from './utils'
export type ContextOptions = {
languages: string[]
/**
* @default '/docs'
*/
baseUrl: string
/**
* Get url from slugs and locale, override the default getUrl function
*/
getUrl: (slugs: string[], locale?: string) => string
resolveIcon: PagesContext['resolveIcon']
resolveIcon: (icon: string) => ReactElement | undefined
}
export function loadContext<
Meta extends MetaPageBase,
Docs extends DocsPageBase
>(
metaPages: Meta[],
type MappedPage<Docs extends DocsPageBase> = { page: AbstractPage; ref: Docs }
export function loadContext<Docs extends DocsPageBase>(
metaPages: MetaPageBase[],
docsPages: Docs[],
{
languages = [],
baseUrl = '/docs',
getUrl,
resolveIcon
getUrl = (slugs, locale) => getPageUrl(slugs, baseUrl, locale),
resolveIcon = () => undefined
}: Partial<ContextOptions> = {}
): PagesContext<Meta, Docs> {
const docsMap = new Map<string, Docs>()
const metaMap = new Map<string, Meta>()
): PagesContext<Docs> {
const basePages: AbstractPage[] = []
const pageMap = new Map<string, MappedPage<Docs>>()
const metaMap = new Map<string, AbstractMeta>()
for (const page of docsPages) {
docsMap.set(getKey(page), page)
const file = getFileData(page._raw)
const mapped: MappedPage<Docs> = {
ref: page,
page: {
file,
title: page.title,
icon: page.icon,
url: getUrl(page.slug.split('/'), page.locale)
}
}
if (!page.locale) basePages.push(mapped.page)
pageMap.set(file.flattenedPath, mapped)
}
for (const meta of metaPages) {
metaMap.set(meta._raw.flattenedPath, meta)
const file = getFileData(meta._raw)
metaMap.set(file.flattenedPath, {
file,
pages: meta.pages,
icon: meta.icon,
title: meta.title
})
}
return {
i18nMap: getI18nPages(pageMap, languages),
getMetaByPath(flattenPath) {
return metaMap.get(flattenPath) ?? null
},
getPageByPath(flattenPath) {
return pageMap.get(flattenPath)?.page ?? null
},
basePages,
resolveIcon,
languages,
pages: getI18nPages(docsMap, languages),
docsMap,
metaMap,
getUrl(slugs, locale) {
return getUrl ? getUrl(slugs, locale) : getPageUrl(slugs, baseUrl, locale)
}
getUrl
}
}
function getFileData(raw: RawDocumentData): File {
const dotIndex = raw.sourceFileName.lastIndexOf('.')
const flattenedPath =
raw.sourceFileDir === raw.flattenedPath
? raw.flattenedPath + '/index'
: raw.flattenedPath
return {
dirname: raw.sourceFileDir,
name: raw.sourceFileName.slice(0, dotIndex === -1 ? undefined : dotIndex),
flattenedPath,
path: raw.sourceFilePath
}
}
function getI18nPages<Docs extends DocsPageBase>(
docsMap: Map<string, Docs>,
docsMap: Map<string, MappedPage<Docs>>,
languages: string[]
): Map<string, Docs[]> {
const pages = new Map<string, Docs[]>()
@@ -63,13 +101,15 @@ function getI18nPages<Docs extends DocsPageBase>(
}
for (const [key, value] of docsMap) {
if (value.locale) continue
if (value.ref.locale) continue
for (const lang of languages) {
pages.get(lang)?.push(docsMap.get(`${key}.${lang}`) ?? value)
const v = docsMap.get(`${key}.${lang}`) ?? value
pages.get(lang)?.push(v.ref)
}
pages.get('')?.push(value)
pages.get('')?.push(value.ref)
}
return pages
+3 -10
View File
@@ -1,5 +1,5 @@
import type { Context } from '@/build-page-tree'
import type { RawDocumentData } from 'contentlayer/source-files'
import type { ReactElement } from 'react'
export type MetaPageBase = {
/** File path relative to `contentDirPath` */
@@ -28,17 +28,10 @@ export type DocsPageBase = {
slug: string
}
export type PagesContext<
Meta extends MetaPageBase = MetaPageBase,
Docs extends DocsPageBase = DocsPageBase
> = {
export type PagesContext<Docs extends DocsPageBase = DocsPageBase> = Context & {
/**
* Language -> Page[]
*/
pages: Map<string, Docs[]>
languages: string[]
docsMap: Map<string, Docs>
metaMap: Map<string, Meta>
resolveIcon?: (icon: string) => ReactElement | undefined
i18nMap: Map<string, Docs[]>
getUrl: (slugs: string[], locale?: string) => string
}
+4 -15
View File
@@ -1,15 +1,4 @@
import type { DocsPageBase, MetaPageBase, PagesContext } from './types'
export function pathToName(path: string[]): string {
const name = path[path.length - 1] ?? 'docs'
return name.slice(0, 1).toUpperCase() + name.slice(1)
}
export function getKey(page: DocsPageBase): string {
return page._raw.sourceFileDir === page._raw.flattenedPath
? page._raw.flattenedPath + '/index'
: page._raw.flattenedPath
}
import type { DocsPageBase, PagesContext } from './types'
export function getPageUrl(slug: string[], baseUrl: string, locale?: string) {
const url = [baseUrl, locale, ...slug]
@@ -43,16 +32,16 @@ export type ContentlayerUtils<Docs extends DocsPageBase> = {
}
export function createUtils<Docs extends DocsPageBase>(
context: PagesContext<MetaPageBase, Docs>
context: PagesContext<Docs>
): ContentlayerUtils<Docs> {
return {
getPages(language = '') {
return context.pages.get(language)
return context.i18nMap.get(language)
},
getPage(slugs = [], language = '') {
const path = slugs.join('/')
return context.pages.get(language)?.find(page => page.slug === path)
return context.i18nMap.get(language)?.find(page => page.slug === path)
},
getPageUrl(slug = [], language) {
const slugs = typeof slug === 'string' ? slug.split('/') : slug
+1 -1
View File
@@ -4,7 +4,7 @@ import tsconfig from './tsconfig.json'
export default defineConfig({
entry: [
'src/{toc,link,breadcrumb,sidebar}.tsx',
'src/{middleware,server,mdx-plugins}.ts',
'src/{middleware,server,mdx-plugins,build-page-tree}.ts',
'src/{search,search-algolia}/{client,server,shared}.ts',
'src/contentlayer/{index,configuration}.ts'
],
+7 -4
View File
@@ -359,9 +359,6 @@ importers:
packages/next-docs-mdx:
dependencies:
'@next/mdx':
specifier: ^13.5.6
version: 13.5.6(@mdx-js/[email protected])(@mdx-js/[email protected])
chokidar:
specifier: ^3.5.3
version: 3.5.3
@@ -384,12 +381,18 @@ importers:
'@mdx-js/react':
specifier: ^2.3.0
version: 2.3.0([email protected])
'@next/mdx':
specifier: 13.5.6
version: 13.5.6(@mdx-js/[email protected])(@mdx-js/[email protected])
'@types/mdast':
specifier: ^3.0.12
version: 3.0.12
'@types/mdx':
specifier: ^2.0.8
version: 2.0.8
'@types/react':
specifier: 18.2.0
version: 18.2.0
next:
specifier: 13.5.6
version: 13.5.6(@babel/[email protected])([email protected])([email protected])
@@ -1644,7 +1647,7 @@ packages:
'@mdx-js/loader': 2.3.0([email protected])
'@mdx-js/react': 2.3.0([email protected])
source-map: 0.7.4
dev: false
dev: true
/@next/[email protected]:
resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}