@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 CHANGED
@@ -41,7 +41,7 @@
41
41
  - ⚡ **基于 Nuxt 4** - 充分利用最新的 Nuxt 框架,实现卓越性能
42
42
  - 🎨 **采用 Nuxt UI** - 集成全面的 UI 组件库,开箱即用
43
43
  - 📝 **MDC 语法增强** - 支持 Markdown 与 Vue 组件的无缝集成
44
- - 📊 **Mermaid 图表** - 内置 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
- 使用 ` ```mermaid ` 代码块渲染可视化图表,支持流程图、时序图、类图等多种图表类型:
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
@@ -14,11 +14,6 @@ export default defineAppConfig({
14
14
  font: 'Public Sans'
15
15
  },
16
16
  ui: {
17
- prose: {
18
- codeIcon: {
19
- mmd: 'i-vscode-icons-file-type-mermaid'
20
- }
21
- },
22
17
  colors: {
23
18
  primary: 'green',
24
19
  neutral: 'slate'
@@ -1,11 +1,27 @@
1
1
  <script setup lang="ts">
2
- import { camelCase, kebabCase, upperFirst } from '@movk/core'
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
- const formattedCommits = computed(() => {
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
- return commits.value.map((commit) => {
90
- const shortSha = commit.sha.slice(0, SHA_SHORT_LENGTH)
91
- const commitLink = `[\`${shortSha}\`](${githubUrl.value}/commit/${commit.sha})`
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
- const content = commit.message
94
- .replace(/\(.*?\)/, '')
95
- .replace(/#(\d+)/g, `<a href='${githubUrl.value}/issues/$1'>#$1</a>`)
96
- .replace(/`(.*?)`/g, '<code class="text-xs">$1</code>')
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
- return {
99
- sha: commit.sha,
100
- formatted: `${commitLink} — ${content}`
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="!formattedCommits.length">
156
+ <div v-if="!commits?.length">
108
157
  No recent changes
109
158
  </div>
110
159
 
111
- <div v-else class="flex flex-col gap-1.5 relative">
112
- <div class="bg-accented w-px h-full absolute left-[11px] z-[-1]" />
113
-
114
- <div v-for="commit of formattedCommits" :key="commit.sha" class="flex gap-1.5 items-center">
115
- <div class="bg-accented ring-2 ring-bg size-1.5 mx-[8.5px] rounded-full" />
116
- <MDC :value="commit.formatted" class="text-sm *:py-0 *:my-0 [&_code]:text-xs" tag="div" />
117
- </div>
118
- </div>
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
- <Mermaid
14
- v-if="isMermaid"
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', 'mermaid']
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.12.4",
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
+ })