@movk/nuxt-docs 1.12.4 → 1.13.1
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/README.md +16 -18
- package/app/app.config.ts +0 -5
- package/app/components/content/CommitChangelog.vue +103 -26
- package/app/components/content/prose/ProsePre.global.vue +10 -3
- package/modules/mermaid.ts +93 -0
- package/nuxt.config.ts +2 -8
- package/package.json +11 -3
- package/server/api/github/releases.json.get.ts +28 -0
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
- ⚡ **基于 Nuxt 4** - 充分利用最新的 Nuxt 框架,实现卓越性能
|
|
42
42
|
- 🎨 **采用 Nuxt UI** - 集成全面的 UI 组件库,开箱即用
|
|
43
43
|
- 📝 **MDC 语法增强** - 支持 Markdown 与 Vue 组件的无缝集成
|
|
44
|
-
- 📊 **Mermaid 图表** -
|
|
44
|
+
- 📊 **Mermaid 图表** - 可选按需启用,渲染流程图、时序图、类图等可视化图表,支持自动主题切换和全屏查看
|
|
45
45
|
- 🔍 **全文搜索** - 基于 Nuxt Content 的 `ContentSearch` 组件,支持键盘快捷键(⌘K)
|
|
46
46
|
- 🌙 **暗黑模式** - 支持亮色/暗色主题切换
|
|
47
47
|
- 📱 **响应式设计** - 移动优先的响应式布局
|
|
@@ -180,7 +180,20 @@ icon: i-lucide-rocket
|
|
|
180
180
|
|
|
181
181
|
### Mermaid 图表
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
Mermaid 是可选功能,默认不启用。先安装依赖,再开启配置:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pnpm add mermaid dompurify
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```ts [nuxt.config.ts]
|
|
190
|
+
export default defineNuxtConfig({
|
|
191
|
+
extends: ['@movk/nuxt-docs'],
|
|
192
|
+
mermaid: { enabled: true }
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
启用后,使用 ` ```mermaid ` 代码块渲染可视化图表,支持流程图、时序图、类图等多种图表类型:
|
|
184
197
|
|
|
185
198
|
````md [md]
|
|
186
199
|
```mermaid
|
|
@@ -210,22 +223,7 @@ graph TD
|
|
|
210
223
|
- **甘特图**(`gantt`):用于展示项目时间线
|
|
211
224
|
- **饼图**(`pie`):用于展示数据占比
|
|
212
225
|
- **Git 图**(`gitGraph`):用于展示分支历史
|
|
213
|
-
- 以及更多 [Mermaid 支持的图表类型](https://mermaid.js.org/intro
|
|
214
|
-
|
|
215
|
-
**带文件名的图表:**
|
|
216
|
-
|
|
217
|
-
````md [md]
|
|
218
|
-
```mermaid [auth-flow.mmd]
|
|
219
|
-
sequenceDiagram
|
|
220
|
-
participant U as 用户
|
|
221
|
-
participant A as 认证服务
|
|
222
|
-
participant D as 数据库
|
|
223
|
-
U->>A: 登录请求
|
|
224
|
-
A->>D: 验证凭证
|
|
225
|
-
D-->>A: 返回用户信息
|
|
226
|
-
A-->>U: 返回 Token
|
|
227
|
-
```
|
|
228
|
-
````
|
|
226
|
+
- 以及更多 [Mermaid 支持的图表类型](https://mermaid.js.org/intro())
|
|
229
227
|
|
|
230
228
|
## 🛠️ 开发
|
|
231
229
|
|
package/app/app.config.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { camelCase, kebabCase, upperFirst } from '
|
|
2
|
+
import { camelCase, kebabCase, upperFirst } from 'scule'
|
|
3
3
|
|
|
4
4
|
interface Commit {
|
|
5
5
|
sha: string
|
|
6
|
+
date: string
|
|
6
7
|
message: string
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
interface Release {
|
|
11
|
+
tag_name: string
|
|
12
|
+
published_at: string
|
|
13
|
+
html_url: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReleaseGroup {
|
|
17
|
+
tag: string
|
|
18
|
+
url?: string
|
|
19
|
+
icon?: string
|
|
20
|
+
title: string
|
|
21
|
+
commits: Commit[]
|
|
22
|
+
published_at?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
const props = defineProps<{
|
|
10
26
|
/**
|
|
11
27
|
* 仓库中的文件路径
|
|
@@ -45,7 +61,6 @@ const SHA_SHORT_LENGTH = 5
|
|
|
45
61
|
const { github } = useAppConfig()
|
|
46
62
|
const route = useRoute()
|
|
47
63
|
|
|
48
|
-
// 计算文件路径相关的值
|
|
49
64
|
const routeName = computed(() => route.path.split('/').pop() ?? '')
|
|
50
65
|
const githubUrl = computed(() => (github && typeof github === 'object' ? github.url : ''))
|
|
51
66
|
|
|
@@ -55,7 +70,6 @@ const filePath = computed(() => {
|
|
|
55
70
|
const fileExtension = props.suffix ?? (github && typeof github === 'object' ? github.suffix : 'vue')
|
|
56
71
|
const fileName = props.name ?? routeName.value
|
|
57
72
|
|
|
58
|
-
// 根据 casing 参数转换文件名
|
|
59
73
|
const transformedName = (() => {
|
|
60
74
|
const casing = props.casing ?? (github && typeof github === 'object' ? github.casing : undefined) ?? 'auto'
|
|
61
75
|
|
|
@@ -82,38 +96,101 @@ const { data: commits } = await useLazyFetch<Commit[]>('/api/github/commits', {
|
|
|
82
96
|
query: { path: [filePath.value], author: props.author }
|
|
83
97
|
})
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
const { data: releases } = await useLazyFetch<Release[]>('/api/github/releases.json')
|
|
100
|
+
|
|
101
|
+
const groupedByRelease = computed<ReleaseGroup[]>(() => {
|
|
87
102
|
if (!commits.value?.length) return []
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
const sortedReleases = (releases.value ?? [])
|
|
105
|
+
.filter(r => r.published_at)
|
|
106
|
+
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime())
|
|
107
|
+
|
|
108
|
+
const releasesOldestFirst = [...sortedReleases].reverse()
|
|
109
|
+
const groups: ReleaseGroup[] = []
|
|
110
|
+
const unreleased: Commit[] = []
|
|
111
|
+
|
|
112
|
+
for (const commit of commits.value) {
|
|
113
|
+
const commitDate = new Date(commit.date).getTime()
|
|
114
|
+
const release = releasesOldestFirst.find(r => new Date(r.published_at).getTime() >= commitDate)
|
|
115
|
+
|
|
116
|
+
if (release) {
|
|
117
|
+
const majorTag = release.tag_name.replace(/-(alpha|beta|rc)\.\d+$/, '')
|
|
118
|
+
let group = groups.find(g => g.tag === majorTag)
|
|
119
|
+
if (!group) {
|
|
120
|
+
group = { tag: majorTag, title: majorTag, icon: 'i-lucide-tag', published_at: release.published_at, url: release.html_url, commits: [] }
|
|
121
|
+
groups.push(group)
|
|
122
|
+
}
|
|
123
|
+
if (new Date(release.published_at) > new Date(group.published_at!)) {
|
|
124
|
+
group.published_at = release.published_at
|
|
125
|
+
group.url = release.html_url
|
|
126
|
+
}
|
|
127
|
+
group.commits.push(commit)
|
|
128
|
+
} else {
|
|
129
|
+
unreleased.push(commit)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
92
132
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
const result: ReleaseGroup[] = []
|
|
134
|
+
if (unreleased.length) {
|
|
135
|
+
result.push({ tag: 'unreleased', title: 'Soon', icon: 'i-lucide-tag', commits: unreleased })
|
|
136
|
+
}
|
|
97
137
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
const uniqueTags = [...new Set(sortedReleases.map(r => r.tag_name.replace(/-(alpha|beta|rc)\.\d+$/, '')))]
|
|
139
|
+
groups.sort((a, b) => uniqueTags.indexOf(a.tag) - uniqueTags.indexOf(b.tag))
|
|
140
|
+
result.push(...groups)
|
|
141
|
+
|
|
142
|
+
return result
|
|
103
143
|
})
|
|
144
|
+
|
|
145
|
+
function normalizeCommitMessage(commit: Commit) {
|
|
146
|
+
const prefix = `[\`${commit.sha.slice(0, SHA_SHORT_LENGTH)}\`](${githubUrl.value}/commit/${commit.sha})`
|
|
147
|
+
const content = commit.message
|
|
148
|
+
.replace(/#(\d+)/g, `<a href='${githubUrl.value}/issues/$1'>#$1</a>`)
|
|
149
|
+
.replace(/`(.*?)`/g, '<code class="text-xs">$1</code>')
|
|
150
|
+
|
|
151
|
+
return `${prefix} — ${content}`
|
|
152
|
+
}
|
|
104
153
|
</script>
|
|
105
154
|
|
|
106
155
|
<template>
|
|
107
|
-
<div v-if="!
|
|
156
|
+
<div v-if="!commits?.length">
|
|
108
157
|
No recent changes
|
|
109
158
|
</div>
|
|
110
159
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
160
|
+
<UTimeline
|
|
161
|
+
v-else
|
|
162
|
+
:items="groupedByRelease"
|
|
163
|
+
size="xs"
|
|
164
|
+
:ui="{ root: '', wrapper: 'mt-0 pb-0', title: 'mb-1.5 flex items-center justify-between' }"
|
|
165
|
+
>
|
|
166
|
+
<template #title="{ item }">
|
|
167
|
+
<UBadge
|
|
168
|
+
v-if="item.tag === 'unreleased'"
|
|
169
|
+
color="neutral"
|
|
170
|
+
variant="subtle"
|
|
171
|
+
:label="item.title"
|
|
172
|
+
class="w-12.5 justify-center"
|
|
173
|
+
/>
|
|
174
|
+
<NuxtLink
|
|
175
|
+
v-else
|
|
176
|
+
:to="item.url"
|
|
177
|
+
target="_blank"
|
|
178
|
+
class="hover:underline"
|
|
179
|
+
>
|
|
180
|
+
<UBadge variant="subtle" :label="item.tag" />
|
|
181
|
+
</NuxtLink>
|
|
182
|
+
|
|
183
|
+
<time v-if="item.published_at" :datetime="item.published_at" class="text-xs text-dimmed font-normal">
|
|
184
|
+
{{ useTimeAgo(new Date(item.published_at)) }}
|
|
185
|
+
</time>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<template #description="{ item }">
|
|
189
|
+
<ul class="flex flex-col gap-1.5">
|
|
190
|
+
<li v-for="commit of item.commits" :key="commit.sha">
|
|
191
|
+
<MDC :value="normalizeCommitMessage(commit)" class="text-sm [&_code]:text-xs" unwrap="p" />
|
|
192
|
+
</li>
|
|
193
|
+
</ul>
|
|
194
|
+
</template>
|
|
195
|
+
</UTimeline>
|
|
119
196
|
</template>
|
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
import type { ProsePreProps } from '@nuxt/ui'
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
import NuxtUIProsePre from '@nuxt/ui/components/prose/Pre.vue'
|
|
5
|
-
import Mermaid from '../Mermaid.vue'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<ProsePreProps>()
|
|
8
7
|
|
|
9
8
|
const isMermaid = computed(() => props.language === 'mermaid')
|
|
9
|
+
|
|
10
|
+
// 动态解析 Mermaid 组件(仅在 mermaid 模块启用时可用)
|
|
11
|
+
const MermaidComponent = computed(() => {
|
|
12
|
+
if (!isMermaid.value) return null
|
|
13
|
+
const resolved = resolveComponent('Mermaid')
|
|
14
|
+
return typeof resolved === 'string' ? null : resolved
|
|
15
|
+
})
|
|
10
16
|
</script>
|
|
11
17
|
|
|
12
18
|
<template>
|
|
13
|
-
<
|
|
14
|
-
|
|
19
|
+
<component
|
|
20
|
+
:is="MermaidComponent"
|
|
21
|
+
v-if="isMermaid && MermaidComponent"
|
|
15
22
|
:code="props.code || ''"
|
|
16
23
|
:filename="props.filename"
|
|
17
24
|
:icon="props.icon"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createResolver, defineNuxtModule, logger } from '@nuxt/kit'
|
|
2
|
+
|
|
3
|
+
export interface MermaidModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 是否启用 Mermaid 图表支持
|
|
6
|
+
*
|
|
7
|
+
* 启用前需安装依赖: `pnpm add mermaid dompurify`
|
|
8
|
+
* @default false
|
|
9
|
+
*/
|
|
10
|
+
enabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const log = logger.withTag('movk-nuxt-docs')
|
|
14
|
+
|
|
15
|
+
export default defineNuxtModule<MermaidModuleOptions>({
|
|
16
|
+
meta: {
|
|
17
|
+
name: 'mermaid',
|
|
18
|
+
configKey: 'mermaid'
|
|
19
|
+
},
|
|
20
|
+
defaults: {
|
|
21
|
+
enabled: false
|
|
22
|
+
},
|
|
23
|
+
setup(options, nuxt) {
|
|
24
|
+
const { resolve } = createResolver(import.meta.url)
|
|
25
|
+
const mermaidFilePath = resolve('../app/components/content/Mermaid.vue')
|
|
26
|
+
|
|
27
|
+
// Layer 自动扫描会注册 Mermaid.vue,通过 components:extend 统一管理:
|
|
28
|
+
// - 禁用时:从列表移除,resolveComponent('Mermaid') 返回字符串,ProsePre fallback 到普通代码块
|
|
29
|
+
// - 启用时:将 mode 改为 'client',避免 SSR 阶段执行
|
|
30
|
+
nuxt.hooks.hook('components:extend', (components) => {
|
|
31
|
+
const idx = components.findIndex(c => c.filePath === mermaidFilePath)
|
|
32
|
+
if (idx === -1) return
|
|
33
|
+
|
|
34
|
+
if (!options.enabled) {
|
|
35
|
+
components.splice(idx, 1)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
components[idx]!.mode = 'client'
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (!options.enabled) return
|
|
43
|
+
|
|
44
|
+
// 验证 mermaid 依赖是否已安装
|
|
45
|
+
try {
|
|
46
|
+
import.meta.resolve('mermaid')
|
|
47
|
+
} catch {
|
|
48
|
+
log.warn('[mermaid] `mermaid` package not found. Install it with: pnpm add mermaid dompurify')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 添加 mermaid 语言高亮
|
|
53
|
+
const contentOptions = nuxt.options.content as Record<string, any> | false
|
|
54
|
+
if (contentOptions) {
|
|
55
|
+
const build = contentOptions.build ??= {}
|
|
56
|
+
const markdown = build.markdown ??= {}
|
|
57
|
+
const highlight = markdown.highlight ??= {}
|
|
58
|
+
const langs = highlight.langs ??= [] as string[]
|
|
59
|
+
if (!langs.includes('mermaid')) {
|
|
60
|
+
langs.push('mermaid')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 为 mermaid ESM 兼容性配置 optimizeDeps
|
|
65
|
+
nuxt.hooks.hook('vite:extendConfig', (config) => {
|
|
66
|
+
const cfg = config as { optimizeDeps?: { include?: string[] } }
|
|
67
|
+
cfg.optimizeDeps ??= {}
|
|
68
|
+
cfg.optimizeDeps.include ??= []
|
|
69
|
+
cfg.optimizeDeps.include.push(
|
|
70
|
+
'@movk/nuxt-docs > mermaid',
|
|
71
|
+
'@movk/nuxt-docs > mermaid > dayjs',
|
|
72
|
+
'@movk/nuxt-docs > mermaid > @braintree/sanitize-url',
|
|
73
|
+
'@movk/nuxt-docs > mermaid > d3',
|
|
74
|
+
'@movk/nuxt-docs > mermaid > dompurify'
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// 注入 mermaid 代码图标配置到 appConfig
|
|
79
|
+
const appConfig = nuxt.options.appConfig as Record<string, any>
|
|
80
|
+
appConfig.ui ??= {}
|
|
81
|
+
appConfig.ui.prose ??= {}
|
|
82
|
+
appConfig.ui.prose.codeIcon ??= {}
|
|
83
|
+
appConfig.ui.prose.codeIcon.mmd ??= 'i-vscode-icons-file-type-mermaid'
|
|
84
|
+
|
|
85
|
+
log.info('[mermaid] Mermaid diagram support enabled')
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
declare module 'nuxt/schema' {
|
|
90
|
+
interface NuxtConfig {
|
|
91
|
+
mermaid?: MermaidModuleOptions
|
|
92
|
+
}
|
|
93
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -27,7 +27,7 @@ export default defineNuxtConfig({
|
|
|
27
27
|
build: {
|
|
28
28
|
markdown: {
|
|
29
29
|
highlight: {
|
|
30
|
-
langs: ['bash', 'diff', 'json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml'
|
|
30
|
+
langs: ['bash', 'diff', 'json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml']
|
|
31
31
|
},
|
|
32
32
|
remarkPlugins: {
|
|
33
33
|
'remark-mdc': {
|
|
@@ -100,13 +100,7 @@ export default defineNuxtConfig({
|
|
|
100
100
|
|
|
101
101
|
include.push(
|
|
102
102
|
'@movk/nuxt-docs > @nuxt/content > slugify',
|
|
103
|
-
'@movk/nuxt-docs > @ai-sdk/gateway > @vercel/oidc'
|
|
104
|
-
// Fix mermaid ESM compatibility issues
|
|
105
|
-
'@movk/nuxt-docs > mermaid',
|
|
106
|
-
'@movk/nuxt-docs > mermaid > dayjs',
|
|
107
|
-
'@movk/nuxt-docs > mermaid > @braintree/sanitize-url',
|
|
108
|
-
'@movk/nuxt-docs > mermaid > d3',
|
|
109
|
-
'@movk/nuxt-docs > mermaid > dompurify'
|
|
103
|
+
'@movk/nuxt-docs > @ai-sdk/gateway > @vercel/oidc'
|
|
110
104
|
)
|
|
111
105
|
}
|
|
112
106
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@movk/nuxt-docs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.13.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Modern Nuxt 4 documentation theme with auto-generated component docs, AI chat assistant, MCP server, and complete developer experience optimization.",
|
|
7
7
|
"author": "YiXuan <mhaibaraai@gmail.com>",
|
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
"main": "./nuxt.config.ts",
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"better-sqlite3": "12.x",
|
|
17
|
+
"dompurify": "3.x",
|
|
18
|
+
"mermaid": "11.x",
|
|
17
19
|
"nuxt": "4.x",
|
|
18
20
|
"tailwindcss": "4.x"
|
|
19
21
|
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"dompurify": {
|
|
24
|
+
"optional": true
|
|
25
|
+
},
|
|
26
|
+
"mermaid": {
|
|
27
|
+
"optional": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
20
30
|
"files": [
|
|
21
31
|
"app",
|
|
22
32
|
"content.config.ts",
|
|
@@ -56,10 +66,8 @@
|
|
|
56
66
|
"@vueuse/nuxt": "^14.2.1",
|
|
57
67
|
"ai": "^6.0.99",
|
|
58
68
|
"defu": "^6.1.4",
|
|
59
|
-
"dompurify": "^3.3.1",
|
|
60
69
|
"exsolve": "^1.0.8",
|
|
61
70
|
"git-url-parse": "^16.1.0",
|
|
62
|
-
"mermaid": "^11.12.3",
|
|
63
71
|
"minimark": "^1.0.0",
|
|
64
72
|
"motion-v": "^2.0.0",
|
|
65
73
|
"nuxt": "^4.3.1",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest'
|
|
2
|
+
|
|
3
|
+
export default defineCachedEventHandler(async () => {
|
|
4
|
+
if (!process.env.NUXT_GITHUB_TOKEN) {
|
|
5
|
+
return []
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { github } = useAppConfig()
|
|
9
|
+
|
|
10
|
+
if (!github || typeof github === 'boolean') {
|
|
11
|
+
throw createError({
|
|
12
|
+
status: 500,
|
|
13
|
+
statusText: 'GitHub configuration is not available'
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
|
|
18
|
+
|
|
19
|
+
const { data: releases } = await octokit.rest.repos.listReleases({
|
|
20
|
+
owner: github.owner,
|
|
21
|
+
repo: github.name
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return releases
|
|
25
|
+
}, {
|
|
26
|
+
maxAge: 60 * 60,
|
|
27
|
+
getKey: () => 'releases'
|
|
28
|
+
})
|