@raystack/chronicle 0.1.0-canary.0efaef0 → 0.1.0-canary.1e5fdae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +70 -4
- package/package.json +1 -1
- package/src/components/mdx/code.tsx +10 -1
- package/src/components/mdx/details.module.css +1 -26
- package/src/components/mdx/details.tsx +2 -3
- package/src/components/mdx/index.tsx +15 -1
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/page-context.tsx +13 -19
- package/src/lib/source.ts +79 -105
- package/src/pages/DocsPage.tsx +1 -1
- package/src/server/api/page/[...slug].ts +5 -6
- package/src/server/entry-client.tsx +38 -25
- package/src/server/entry-server.tsx +28 -23
- package/src/server/routes/sitemap.xml.ts +3 -2
- package/src/server/vite-config.ts +44 -4
- package/src/themes/default/Layout.tsx +13 -10
- package/src/themes/default/Page.module.css +44 -0
- package/src/themes/default/Toc.tsx +14 -30
- package/src/themes/paper/ChapterNav.tsx +14 -14
- package/src/themes/paper/Page.module.css +5 -0
- package/src/themes/paper/Page.tsx +15 -41
- package/src/themes/paper/ReadingProgress.tsx +2 -2
- package/src/types/content.ts +5 -21
- package/src/types/globals.d.ts +0 -1
- package/src/types/theme.ts +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -16,15 +16,48 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
18
|
|
|
19
|
+
// src/lib/remark-unused-directives.ts
|
|
20
|
+
import { visit } from "unist-util-visit";
|
|
21
|
+
var remarkUnusedDirectives = () => {
|
|
22
|
+
return (tree) => {
|
|
23
|
+
visit(tree, ["textDirective"], (node) => {
|
|
24
|
+
const directive = node;
|
|
25
|
+
if (!directive.data) {
|
|
26
|
+
const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0;
|
|
27
|
+
const hasChildren = directive.children && directive.children.length > 0;
|
|
28
|
+
if (!hasAttributes && !hasChildren) {
|
|
29
|
+
const name = directive.name;
|
|
30
|
+
if (!name)
|
|
31
|
+
return;
|
|
32
|
+
Object.keys(directive).forEach((key) => delete directive[key]);
|
|
33
|
+
directive.type = "text";
|
|
34
|
+
directive.value = `:${name}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}, remark_unused_directives_default;
|
|
40
|
+
var init_remark_unused_directives = __esm(() => {
|
|
41
|
+
remark_unused_directives_default = remarkUnusedDirectives;
|
|
42
|
+
});
|
|
43
|
+
|
|
19
44
|
// src/server/vite-config.ts
|
|
20
45
|
var exports_vite_config = {};
|
|
21
46
|
__export(exports_vite_config, {
|
|
22
47
|
createViteConfig: () => createViteConfig
|
|
23
48
|
});
|
|
24
49
|
import react from "@vitejs/plugin-react";
|
|
50
|
+
import { remarkDirectiveAdmonition, remarkMdxMermaid } from "fumadocs-core/mdx-plugins";
|
|
51
|
+
import { defineConfig as defineFumadocsConfig } from "fumadocs-mdx/config";
|
|
25
52
|
import mdx from "fumadocs-mdx/vite";
|
|
26
53
|
import { nitro } from "nitro/vite";
|
|
27
54
|
import path4 from "node:path";
|
|
55
|
+
import remarkDirective from "remark-directive";
|
|
56
|
+
function resolveOutputDir(projectRoot, preset) {
|
|
57
|
+
if (preset === "vercel" || preset === "vercel-static")
|
|
58
|
+
return path4.resolve(projectRoot, ".vercel/output");
|
|
59
|
+
return path4.resolve(projectRoot, ".output");
|
|
60
|
+
}
|
|
28
61
|
async function createViteConfig(options) {
|
|
29
62
|
const { packageRoot, projectRoot, contentDir, preset } = options;
|
|
30
63
|
return {
|
|
@@ -35,7 +68,34 @@ async function createViteConfig(options) {
|
|
|
35
68
|
serverDir: path4.resolve(packageRoot, "src/server"),
|
|
36
69
|
...preset && { preset }
|
|
37
70
|
}),
|
|
38
|
-
mdx({
|
|
71
|
+
mdx({
|
|
72
|
+
default: defineFumadocsConfig({
|
|
73
|
+
mdxOptions: {
|
|
74
|
+
remarkPlugins: [
|
|
75
|
+
remarkDirective,
|
|
76
|
+
[remarkDirectiveAdmonition, {
|
|
77
|
+
tags: {
|
|
78
|
+
CalloutContainer: "Callout",
|
|
79
|
+
CalloutTitle: "CalloutTitle",
|
|
80
|
+
CalloutDescription: "CalloutDescription"
|
|
81
|
+
},
|
|
82
|
+
types: {
|
|
83
|
+
note: "accent",
|
|
84
|
+
tip: "accent",
|
|
85
|
+
info: "accent",
|
|
86
|
+
warn: "attention",
|
|
87
|
+
warning: "attention",
|
|
88
|
+
danger: "alert",
|
|
89
|
+
caution: "alert",
|
|
90
|
+
success: "success"
|
|
91
|
+
}
|
|
92
|
+
}],
|
|
93
|
+
remark_unused_directives_default,
|
|
94
|
+
remarkMdxMermaid
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}, { index: false }),
|
|
39
99
|
react()
|
|
40
100
|
],
|
|
41
101
|
resolve: {
|
|
@@ -58,8 +118,7 @@ async function createViteConfig(options) {
|
|
|
58
118
|
},
|
|
59
119
|
define: {
|
|
60
120
|
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
|
|
61
|
-
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
62
|
-
__CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
|
|
121
|
+
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
63
122
|
},
|
|
64
123
|
css: {
|
|
65
124
|
modules: {
|
|
@@ -77,10 +136,17 @@ async function createViteConfig(options) {
|
|
|
77
136
|
}
|
|
78
137
|
}
|
|
79
138
|
}
|
|
139
|
+
},
|
|
140
|
+
nitro: {
|
|
141
|
+
output: {
|
|
142
|
+
dir: resolveOutputDir(projectRoot, preset)
|
|
143
|
+
}
|
|
80
144
|
}
|
|
81
145
|
};
|
|
82
146
|
}
|
|
83
|
-
var init_vite_config = () => {
|
|
147
|
+
var init_vite_config = __esm(() => {
|
|
148
|
+
init_remark_unused_directives();
|
|
149
|
+
});
|
|
84
150
|
|
|
85
151
|
// src/cli/index.ts
|
|
86
152
|
import { Command as Command6 } from "commander";
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { type ComponentProps, isValidElement, Children } from 'react'
|
|
4
|
+
import { Mermaid } from './mermaid'
|
|
4
5
|
import styles from './code.module.css'
|
|
5
6
|
|
|
6
7
|
type PreProps = ComponentProps<'pre'> & {
|
|
@@ -16,6 +17,14 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function MdxPre({ children, title, className, ...props }: PreProps) {
|
|
20
|
+
// Detect mermaid code blocks
|
|
21
|
+
if (isValidElement(children)) {
|
|
22
|
+
const childProps = children.props as { className?: string; children?: string }
|
|
23
|
+
if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
|
|
24
|
+
return <Mermaid chart={childProps.children} />
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
return (
|
|
20
29
|
<div className={styles.codeBlock}>
|
|
21
30
|
{title && <div className={styles.codeHeader}>{title}</div>}
|
|
@@ -1,35 +1,10 @@
|
|
|
1
1
|
.details {
|
|
2
|
-
border: 1px solid var(--rs-color-border-base-primary);
|
|
3
|
-
border-radius: var(--rs-radius-2);
|
|
4
2
|
margin: var(--rs-space-5) 0;
|
|
5
3
|
}
|
|
6
4
|
|
|
7
|
-
.
|
|
8
|
-
padding: var(--rs-space-4) var(--rs-space-5);
|
|
9
|
-
cursor: pointer;
|
|
5
|
+
.trigger {
|
|
10
6
|
font-weight: 500;
|
|
11
7
|
font-size: var(--rs-font-size-small);
|
|
12
|
-
color: var(--rs-color-text-base-primary);
|
|
13
|
-
background: var(--rs-color-background-base-secondary);
|
|
14
|
-
list-style: none;
|
|
15
|
-
display: flex;
|
|
16
|
-
align-items: center;
|
|
17
|
-
gap: var(--rs-space-3);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.summary::-webkit-details-marker {
|
|
21
|
-
display: none;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.summary::before {
|
|
25
|
-
content: '▶';
|
|
26
|
-
font-size: 10px;
|
|
27
|
-
transition: transform 0.2s ease;
|
|
28
|
-
color: var(--rs-color-text-base-secondary);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.details[open] > .summary::before {
|
|
32
|
-
transform: rotate(90deg);
|
|
33
8
|
}
|
|
34
9
|
|
|
35
10
|
.content {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react'
|
|
2
|
-
import styles from './details.module.css'
|
|
3
2
|
|
|
4
3
|
export function MdxDetails({ children, className, ...props }: ComponentProps<'details'>) {
|
|
5
4
|
return (
|
|
6
|
-
<details className={
|
|
5
|
+
<details className={className} {...props}>
|
|
7
6
|
{children}
|
|
8
7
|
</details>
|
|
9
8
|
)
|
|
@@ -11,7 +10,7 @@ export function MdxDetails({ children, className, ...props }: ComponentProps<'de
|
|
|
11
10
|
|
|
12
11
|
export function MdxSummary({ children, className, ...props }: ComponentProps<'summary'>) {
|
|
13
12
|
return (
|
|
14
|
-
<summary className={
|
|
13
|
+
<summary className={className} {...props}>
|
|
15
14
|
{children}
|
|
16
15
|
</summary>
|
|
17
16
|
)
|
|
@@ -8,6 +8,20 @@ import { Mermaid } from './mermaid'
|
|
|
8
8
|
import { MdxParagraph } from './paragraph'
|
|
9
9
|
import { CalloutContainer, CalloutTitle, CalloutDescription, MdxBlockquote } from '@/components/common/callout'
|
|
10
10
|
import { Tabs } from '@raystack/apsara'
|
|
11
|
+
import { type ComponentProps, useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function ClientOnly({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const [mounted, setMounted] = useState(false)
|
|
15
|
+
useEffect(() => setMounted(true), [])
|
|
16
|
+
return mounted ? <>{children}</> : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function MdxTabs(props: ComponentProps<typeof Tabs>) {
|
|
20
|
+
return <ClientOnly><Tabs {...props} /></ClientOnly>
|
|
21
|
+
}
|
|
22
|
+
MdxTabs.List = Tabs.List
|
|
23
|
+
MdxTabs.Trigger = Tabs.Trigger
|
|
24
|
+
MdxTabs.Content = Tabs.Content
|
|
11
25
|
|
|
12
26
|
export const mdxComponents: MDXComponents = {
|
|
13
27
|
p: MdxParagraph,
|
|
@@ -27,7 +41,7 @@ export const mdxComponents: MDXComponents = {
|
|
|
27
41
|
Callout: CalloutContainer,
|
|
28
42
|
CalloutTitle,
|
|
29
43
|
CalloutDescription,
|
|
30
|
-
Tabs,
|
|
44
|
+
Tabs: MdxTabs,
|
|
31
45
|
Mermaid,
|
|
32
46
|
}
|
|
33
47
|
|
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Breadcrumb } from '@raystack/apsara'
|
|
4
|
-
import
|
|
4
|
+
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb'
|
|
5
|
+
import type { Root } from 'fumadocs-core/page-tree'
|
|
5
6
|
|
|
6
7
|
interface BreadcrumbsProps {
|
|
7
8
|
slug: string[]
|
|
8
|
-
tree:
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function findInTree(items: PageTreeItem[], targetPath: string): PageTreeItem | undefined {
|
|
12
|
-
for (const item of items) {
|
|
13
|
-
const itemUrl = item.url || `/${item.name.toLowerCase().replace(/\s+/g, '-')}`
|
|
14
|
-
if (itemUrl === targetPath || itemUrl === `/${targetPath}`) {
|
|
15
|
-
return item
|
|
16
|
-
}
|
|
17
|
-
if (item.children) {
|
|
18
|
-
const found = findInTree(item.children, targetPath)
|
|
19
|
-
if (found) return found
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return undefined
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getFirstPageUrl(item: PageTreeItem): string | undefined {
|
|
26
|
-
if (item.type === 'page' && item.url) {
|
|
27
|
-
return item.url
|
|
28
|
-
}
|
|
29
|
-
if (item.children) {
|
|
30
|
-
for (const child of item.children) {
|
|
31
|
-
const url = getFirstPageUrl(child)
|
|
32
|
-
if (url) return url
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return undefined
|
|
9
|
+
tree: Root
|
|
36
10
|
}
|
|
37
11
|
|
|
38
12
|
export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
|
|
39
|
-
const
|
|
13
|
+
const url = slug.length === 0 ? '/' : `/${slug.join('/')}`
|
|
14
|
+
const items = getBreadcrumbItems(url, tree, { includePage: true })
|
|
40
15
|
|
|
41
|
-
|
|
42
|
-
const currentPath = `/${slug.slice(0, i + 1).join('/')}`
|
|
43
|
-
const node = findInTree(tree.children, currentPath)
|
|
44
|
-
const href = node?.url || (node && getFirstPageUrl(node)) || currentPath
|
|
45
|
-
const label = node?.name ?? slug[i]
|
|
46
|
-
items.push({
|
|
47
|
-
label: label.charAt(0).toUpperCase() + label.slice(1),
|
|
48
|
-
href,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
16
|
+
if (items.length === 0) return null
|
|
51
17
|
|
|
52
18
|
return (
|
|
53
19
|
<Breadcrumb size="small">
|
|
@@ -55,10 +21,10 @@ export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
|
|
|
55
21
|
const breadcrumbItem = (
|
|
56
22
|
<Breadcrumb.Item
|
|
57
23
|
key={`item-${index}`}
|
|
58
|
-
href={item.
|
|
24
|
+
href={item.url}
|
|
59
25
|
current={index === items.length - 1}
|
|
60
26
|
>
|
|
61
|
-
{item.
|
|
27
|
+
{item.name}
|
|
62
28
|
</Breadcrumb.Item>
|
|
63
29
|
)
|
|
64
30
|
if (index === 0) return [breadcrumbItem]
|
package/src/lib/api-routes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
2
|
import slugify from 'slugify'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Root, Node, Item, Folder } from 'fumadocs-core/page-tree'
|
|
4
4
|
import type { ApiSpec } from './openapi'
|
|
5
5
|
|
|
6
6
|
export function getSpecSlug(spec: ApiSpec): string {
|
|
@@ -56,16 +56,15 @@ export function findApiOperation(specs: ApiSpec[], slug: string[]): ApiRouteMatc
|
|
|
56
56
|
return null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function buildApiPageTree(specs: ApiSpec[]):
|
|
60
|
-
const children:
|
|
59
|
+
export function buildApiPageTree(specs: ApiSpec[]): Root {
|
|
60
|
+
const children: Node[] = []
|
|
61
61
|
|
|
62
62
|
for (const spec of specs) {
|
|
63
63
|
const specSlug = getSpecSlug(spec)
|
|
64
64
|
const paths = spec.document.paths ?? {}
|
|
65
65
|
const tags = spec.document.tags ?? []
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
const opsByTag = new Map<string, PageTreeItem[]>()
|
|
67
|
+
const opsByTag = new Map<string, Item[]>()
|
|
69
68
|
const tagDisplayName = new Map<string, string>()
|
|
70
69
|
|
|
71
70
|
for (const [, pathItem] of Object.entries(paths)) {
|
|
@@ -90,7 +89,6 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
// Use doc.tags display names where available
|
|
94
92
|
for (const t of tags) {
|
|
95
93
|
const key = t.name.toLowerCase()
|
|
96
94
|
if (opsByTag.has(key)) {
|
|
@@ -98,7 +96,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
const tagFolders:
|
|
99
|
+
const tagFolders: Folder[] = Array.from(opsByTag.entries()).map(([key, ops]) => ({
|
|
102
100
|
type: 'folder' as const,
|
|
103
101
|
name: tagDisplayName.get(key) ?? key,
|
|
104
102
|
icon: 'rectangle-stack',
|
|
@@ -110,7 +108,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
110
108
|
type: 'folder',
|
|
111
109
|
name: spec.name,
|
|
112
110
|
children: tagFolders,
|
|
113
|
-
})
|
|
111
|
+
} as Folder)
|
|
114
112
|
} else {
|
|
115
113
|
children.push(...tagFolders)
|
|
116
114
|
}
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
2
|
createContext,
|
|
3
3
|
type ReactNode,
|
|
4
4
|
useContext,
|
|
@@ -6,19 +6,21 @@ import React, {
|
|
|
6
6
|
useState
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { useLocation } from 'react-router';
|
|
9
|
-
import { mdxComponents } from '@/components/mdx';
|
|
10
9
|
import type { ApiSpec } from '@/lib/openapi';
|
|
11
|
-
import type { ChronicleConfig, Frontmatter,
|
|
10
|
+
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
|
|
11
|
+
|
|
12
|
+
export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>;
|
|
12
13
|
|
|
13
14
|
interface PageData {
|
|
14
15
|
slug: string[];
|
|
15
16
|
frontmatter: Frontmatter;
|
|
16
17
|
content: ReactNode;
|
|
18
|
+
toc: TableOfContents;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
interface PageContextValue {
|
|
20
22
|
config: ChronicleConfig;
|
|
21
|
-
tree:
|
|
23
|
+
tree: Root;
|
|
22
24
|
page: PageData | null;
|
|
23
25
|
apiSpecs: ApiSpec[];
|
|
24
26
|
}
|
|
@@ -31,7 +33,7 @@ export function usePageContext(): PageContextValue {
|
|
|
31
33
|
console.error('usePageContext: no context found!');
|
|
32
34
|
return {
|
|
33
35
|
config: { title: 'Documentation' },
|
|
34
|
-
tree: { name: 'root', children: [] },
|
|
36
|
+
tree: { name: 'root', children: [] } as Root,
|
|
35
37
|
page: null,
|
|
36
38
|
apiSpecs: []
|
|
37
39
|
};
|
|
@@ -41,31 +43,23 @@ export function usePageContext(): PageContextValue {
|
|
|
41
43
|
|
|
42
44
|
interface PageProviderProps {
|
|
43
45
|
initialConfig: ChronicleConfig;
|
|
44
|
-
initialTree:
|
|
46
|
+
initialTree: Root;
|
|
45
47
|
initialPage: PageData | null;
|
|
46
48
|
initialApiSpecs: ApiSpec[];
|
|
49
|
+
loadMdx: MdxLoader;
|
|
47
50
|
children: ReactNode;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
|
|
51
|
-
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
52
|
-
const mod = relativePath.endsWith('.md')
|
|
53
|
-
? await import(`../../.content/${withoutExt}.md`)
|
|
54
|
-
: await import(`../../.content/${withoutExt}.mdx`);
|
|
55
|
-
return mod.default
|
|
56
|
-
? React.createElement(mod.default, { components: mdxComponents })
|
|
57
|
-
: null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
53
|
export function PageProvider({
|
|
61
54
|
initialConfig,
|
|
62
55
|
initialTree,
|
|
63
56
|
initialPage,
|
|
64
57
|
initialApiSpecs,
|
|
58
|
+
loadMdx,
|
|
65
59
|
children
|
|
66
60
|
}: PageProviderProps) {
|
|
67
61
|
const { pathname } = useLocation();
|
|
68
|
-
const [tree] = useState<
|
|
62
|
+
const [tree] = useState<Root>(initialTree);
|
|
69
63
|
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
70
64
|
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
71
65
|
const [currentPath, setCurrentPath] = useState(pathname);
|
|
@@ -98,9 +92,9 @@ export function PageProvider({
|
|
|
98
92
|
.then(res => res.json())
|
|
99
93
|
.then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
|
|
100
94
|
if (cancelled.current) return;
|
|
101
|
-
const content = await
|
|
95
|
+
const { content, toc } = await loadMdx(data.relativePath);
|
|
102
96
|
if (cancelled.current) return;
|
|
103
|
-
setPage({ slug, frontmatter: data.frontmatter, content });
|
|
97
|
+
setPage({ slug, frontmatter: data.frontmatter, content, toc });
|
|
104
98
|
})
|
|
105
99
|
.catch(() => {});
|
|
106
100
|
|
package/src/lib/source.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loader } from 'fumadocs-core/source';
|
|
4
|
+
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
|
|
4
5
|
import matter from 'gray-matter';
|
|
5
6
|
import type { MDXContent } from 'mdx/types';
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
export interface SourcePage {
|
|
9
|
-
url: string;
|
|
10
|
-
slugs: string[];
|
|
11
|
-
filePath: string;
|
|
12
|
-
frontmatter: Frontmatter;
|
|
13
|
-
}
|
|
7
|
+
import type { TableOfContents } from 'fumadocs-core/toc';
|
|
8
|
+
import type { Frontmatter } from '@/types';
|
|
14
9
|
|
|
15
10
|
function getContentDir(): string {
|
|
16
11
|
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
|
|
@@ -43,14 +38,18 @@ async function scanFiles(contentDir: string) {
|
|
|
43
38
|
files.push({
|
|
44
39
|
type: 'page',
|
|
45
40
|
path: relativePath,
|
|
46
|
-
data: { ...data,
|
|
41
|
+
data: { ...data, _relativePath: relativePath }
|
|
47
42
|
});
|
|
48
43
|
} else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
46
|
+
const data = entry.name.endsWith('.json')
|
|
47
|
+
? JSON.parse(raw)
|
|
48
|
+
: matter(raw).data;
|
|
49
|
+
files.push({ type: 'meta', path: relativePath, data });
|
|
50
|
+
} catch {
|
|
51
|
+
/* malformed meta file */
|
|
52
|
+
}
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
} catch {
|
|
@@ -63,7 +62,6 @@ async function scanFiles(contentDir: string) {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
66
|
-
let cachedPages: SourcePage[] | null = null;
|
|
67
65
|
|
|
68
66
|
async function getSource() {
|
|
69
67
|
if (cachedSource) return cachedSource;
|
|
@@ -76,111 +74,87 @@ async function getSource() {
|
|
|
76
74
|
return cachedSource;
|
|
77
75
|
}
|
|
78
76
|
|
|
77
|
+
export { getSource as source };
|
|
78
|
+
|
|
79
79
|
export function invalidate() {
|
|
80
80
|
cachedSource = null;
|
|
81
|
-
cachedPages = null;
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cachedPages = s.getPages().map(page => {
|
|
89
|
-
const data = page.data as Record<string, unknown>;
|
|
90
|
-
return {
|
|
91
|
-
url: page.url,
|
|
92
|
-
slugs: page.slugs,
|
|
93
|
-
filePath: (data._absolutePath as string) ?? '',
|
|
94
|
-
frontmatter: {
|
|
95
|
-
title:
|
|
96
|
-
(data.title as string) ??
|
|
97
|
-
page.slugs[page.slugs.length - 1] ??
|
|
98
|
-
'Untitled',
|
|
99
|
-
description: data.description as string | undefined,
|
|
100
|
-
order: data.order as number | undefined,
|
|
101
|
-
icon: data.icon as string | undefined,
|
|
102
|
-
lastModified: data.lastModified as string | undefined
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return cachedPages;
|
|
83
|
+
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
|
|
84
|
+
if (node.type === 'page') return orderMap.get(node.url);
|
|
85
|
+
if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
|
|
86
|
+
return undefined;
|
|
108
87
|
}
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
89
|
+
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
|
|
90
|
+
return [...nodes]
|
|
91
|
+
.map(n =>
|
|
92
|
+
n.type === 'folder'
|
|
93
|
+
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
|
|
94
|
+
: n
|
|
95
|
+
)
|
|
96
|
+
.sort(
|
|
97
|
+
(a, b) =>
|
|
98
|
+
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
99
|
+
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
100
|
+
);
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return null;
|
|
103
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
|
|
104
|
+
const orderMap = new Map<string, number>();
|
|
105
|
+
for (const page of pages) {
|
|
106
|
+
const d = page.data as Record<string, unknown>;
|
|
107
|
+
const order = d.order as number | undefined;
|
|
108
|
+
if (order !== undefined) orderMap.set(page.url, order);
|
|
109
|
+
if (page.url === '/') orderMap.set('/', order ?? 0);
|
|
124
110
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return mod.default;
|
|
111
|
+
return { ...tree, children: sortNodes(tree.children, orderMap) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getPageTree(): Promise<Root> {
|
|
115
|
+
const s = await getSource();
|
|
116
|
+
return sortTreeByOrder(s.pageTree as Root, s.getPages());
|
|
132
117
|
}
|
|
133
118
|
|
|
134
|
-
export async function
|
|
119
|
+
export async function getPages() {
|
|
135
120
|
const s = await getSource();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const rootPages: PageTreeItem[] = [];
|
|
121
|
+
return s.getPages();
|
|
122
|
+
}
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
type: 'page',
|
|
145
|
-
name: (data.title as string) ?? page.slugs.join('/') ?? 'Untitled',
|
|
146
|
-
url: page.url,
|
|
147
|
-
order: (data.order as number | undefined) ?? (isIndex ? 0 : undefined)
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (page.slugs.length > 1) {
|
|
151
|
-
const folder = page.slugs[0];
|
|
152
|
-
if (!folders.has(folder)) {
|
|
153
|
-
folders.set(folder, []);
|
|
154
|
-
}
|
|
155
|
-
folders.get(folder)?.push(item);
|
|
156
|
-
} else {
|
|
157
|
-
rootPages.push(item);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
124
|
+
export async function getPage(slugs?: string[]) {
|
|
125
|
+
const s = await getSource();
|
|
126
|
+
return s.getPage(slugs);
|
|
127
|
+
}
|
|
160
128
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
129
|
+
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
|
|
130
|
+
const d = page.data as Record<string, unknown>;
|
|
131
|
+
return {
|
|
132
|
+
title: (d.title as string) ?? fallbackTitle ?? 'Untitled',
|
|
133
|
+
description: d.description as string | undefined,
|
|
134
|
+
order: d.order as number | undefined,
|
|
135
|
+
icon: d.icon as string | undefined,
|
|
136
|
+
lastModified: d.lastModified as string | undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
167
139
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
for (const [folder, items] of folders) {
|
|
172
|
-
const sorted = sortByOrder(items);
|
|
173
|
-
const indexPage = items.find(item => item.url === `/${folder}`);
|
|
174
|
-
const folderOrder = indexPage?.order ?? sorted[0]?.order;
|
|
175
|
-
folderItems.push({
|
|
176
|
-
type: 'folder',
|
|
177
|
-
name: `${folder.charAt(0).toUpperCase()}${folder.slice(1)}`,
|
|
178
|
-
order: folderOrder,
|
|
179
|
-
children: sorted
|
|
180
|
-
});
|
|
181
|
-
}
|
|
140
|
+
export function getRelativePath(page: { data: unknown }): string {
|
|
141
|
+
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
|
|
142
|
+
}
|
|
182
143
|
|
|
183
|
-
|
|
144
|
+
const ssrModules = import.meta.glob<{ default?: MDXContent; toc?: TableOfContents }>(
|
|
145
|
+
'../../.content/**/*.{mdx,md}'
|
|
146
|
+
);
|
|
184
147
|
|
|
185
|
-
|
|
148
|
+
export async function loadPageModule(
|
|
149
|
+
relativePath: string
|
|
150
|
+
): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
|
|
151
|
+
if (!relativePath || relativePath.includes('..')) return { default: null, toc: [] };
|
|
152
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
153
|
+
const key = relativePath.endsWith('.md')
|
|
154
|
+
? `../../.content/${withoutExt}.md`
|
|
155
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
156
|
+
const loader = ssrModules[key];
|
|
157
|
+
if (!loader) return { default: null, toc: [] };
|
|
158
|
+
const mod = await loader();
|
|
159
|
+
return { default: mod.default ?? null, toc: mod.toc ?? [] };
|
|
186
160
|
}
|