@movk/nuxt-docs 1.13.1 → 1.14.0

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.
Files changed (43) hide show
  1. package/app/app.config.ts +1 -1
  2. package/app/assets/css/main.css +16 -0
  3. package/app/assets/icons/LICENSE +14 -0
  4. package/app/assets/icons/ai.svg +1 -0
  5. package/app/components/OgImage/Nuxt.vue +2 -4
  6. package/app/components/content/CommitChangelog.vue +8 -3
  7. package/app/components/content/ComponentEmits.vue +1 -1
  8. package/app/components/content/ComponentExample.vue +98 -72
  9. package/app/components/content/ComponentProps.vue +3 -3
  10. package/app/components/content/ComponentPropsSchema.vue +1 -1
  11. package/app/components/content/ComponentSlots.vue +1 -1
  12. package/app/components/content/HighlightInlineType.vue +1 -1
  13. package/app/components/content/PageLastCommit.vue +6 -5
  14. package/app/components/header/HeaderLogo.vue +1 -1
  15. package/app/composables/cachedParseMarkdown.ts +12 -0
  16. package/app/composables/fetchComponentExample.ts +5 -22
  17. package/app/composables/fetchComponentMeta.ts +5 -22
  18. package/app/mdc.config.ts +12 -0
  19. package/app/pages/docs/[...slug].vue +8 -2
  20. package/app/templates/releases.vue +3 -1
  21. package/app/types/index.d.ts +1 -1
  22. package/app/utils/shiki-transformer-icon-highlight.ts +89 -0
  23. package/app/workers/prettier.js +26 -17
  24. package/modules/ai-chat/index.ts +1 -1
  25. package/modules/component-example.ts +65 -30
  26. package/modules/config.ts +24 -1
  27. package/modules/css.ts +1 -1
  28. package/nuxt.config.ts +40 -2
  29. package/nuxt.schema.ts +4 -4
  30. package/package.json +17 -17
  31. package/server/api/component-example.get.ts +5 -5
  32. package/server/api/github/{commits.get.ts → commits.json.get.ts} +7 -4
  33. package/server/api/github/{last-commit.get.ts → last-commit.json.get.ts} +12 -9
  34. package/server/api/github/releases.json.get.ts +2 -2
  35. package/server/mcp/resources/documentation-pages.ts +26 -0
  36. package/server/mcp/resources/examples.ts +17 -0
  37. package/server/mcp/tools/get-example.ts +1 -1
  38. package/server/mcp/tools/list-examples.ts +4 -8
  39. package/server/mcp/tools/list-getting-started-guides.ts +29 -0
  40. package/server/routes/raw/[...slug].md.get.ts +3 -5
  41. package/server/utils/stringifyMinimark.ts +345 -0
  42. package/server/utils/transformMDC.ts +14 -5
  43. package/utils/meta.ts +1 -1
@@ -0,0 +1,89 @@
1
+ import type { ShikiTransformer } from '@shikijs/core'
2
+
3
+ export interface TransformerIconHighlightOptions {
4
+ /**
5
+ * Custom function to render the icon HTML
6
+ * @default Uses Iconify API with mask mode
7
+ */
8
+ htmlIcon?: (icon: string) => string
9
+ }
10
+
11
+ // Common icon collections to validate against (sorted by length descending for proper matching)
12
+ const ICON_COLLECTIONS = [
13
+ 'simple-icons',
14
+ 'vscode-icons',
15
+ 'tabler',
16
+ 'lucide',
17
+ 'logos',
18
+ 'ph'
19
+ ]
20
+
21
+ function parseIconName(text: string): { collection: string, name: string, format: 'i' | 'colon' } | null {
22
+ // Strip quotes if present (single, double, or backticks)
23
+ let cleanText = text
24
+ if (/^['"`].*['"`]$/.test(text)) {
25
+ cleanText = text.slice(1, -1)
26
+ }
27
+
28
+ // Try i-{collection}-{name} format
29
+ if (cleanText.startsWith('i-')) {
30
+ const rest = cleanText.slice(2)
31
+ for (const collection of ICON_COLLECTIONS) {
32
+ if (rest.startsWith(`${collection}-`)) {
33
+ const name = rest.slice(collection.length + 1)
34
+ if (name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
35
+ return { collection, name, format: 'i' }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ // Try {collection}:{name} format
42
+ const colonIndex = cleanText.indexOf(':')
43
+ if (colonIndex > 0) {
44
+ const collection = cleanText.slice(0, colonIndex)
45
+ const name = cleanText.slice(colonIndex + 1)
46
+ if (ICON_COLLECTIONS.includes(collection) && name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
47
+ return { collection, name, format: 'colon' }
48
+ }
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ export function transformerIconHighlight(options: TransformerIconHighlightOptions = {}): ShikiTransformer {
55
+ const { htmlIcon } = options
56
+
57
+ return {
58
+ name: 'shiki-transformer-icon-highlight',
59
+ span(hast, _line, _col, _lineElement, token) {
60
+ const text = token.content
61
+
62
+ // Try to parse as an icon
63
+ const parsed = parseIconName(text)
64
+ if (!parsed) return
65
+
66
+ const iconIdentifier = `${parsed.collection}:${parsed.name}`
67
+ // Add color=black for mask-image to work properly (mask uses luminance)
68
+ const iconUrl = `https://api.iconify.design/${iconIdentifier}.svg?color=%23000`
69
+
70
+ // Create the icon element as a proper HAST element
71
+ const iconElement = htmlIcon
72
+ ? { type: 'raw' as const, value: htmlIcon(iconIdentifier) }
73
+ : {
74
+ type: 'element' as const,
75
+ tagName: 'i',
76
+ properties: {
77
+ class: 'shiki-icon-highlight',
78
+ style: `--shiki-icon-url: url(${iconUrl})`
79
+ },
80
+ children: []
81
+ }
82
+
83
+ // Prepend the icon to the span content
84
+ if (hast.children) {
85
+ hast.children.unshift(iconElement)
86
+ }
87
+ }
88
+ }
89
+ }
@@ -1,9 +1,18 @@
1
- /* eslint-disable no-undef */
1
+ let _prettier
2
+ let _plugins
3
+
2
4
  self.onmessage = async function (event) {
3
- self.postMessage({
4
- uid: event.data.uid,
5
- message: await handleMessage(event.data.message)
6
- })
5
+ try {
6
+ self.postMessage({
7
+ uid: event.data.uid,
8
+ message: await handleMessage(event.data.message)
9
+ })
10
+ } catch (error) {
11
+ self.postMessage({
12
+ uid: event.data.uid,
13
+ error: error.message || String(error)
14
+ })
15
+ }
7
16
  }
8
17
 
9
18
  function handleMessage(message) {
@@ -14,23 +23,23 @@ function handleMessage(message) {
14
23
  }
15
24
 
16
25
  async function handleFormatMessage(message) {
17
- if (!globalThis.prettier) {
18
- await Promise.all([
19
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/standalone.js'),
20
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/babel.js'),
21
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/estree.js'),
22
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/html.js'),
23
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/markdown.js'),
24
- import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/typescript.js')
26
+ if (!_prettier) {
27
+ const [prettierModule, ...plugins] = await Promise.all([
28
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/standalone.mjs'),
29
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/babel.mjs'),
30
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/estree.mjs'),
31
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/html.mjs'),
32
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/markdown.mjs'),
33
+ import('https://cdn.jsdelivr.net/npm/prettier@3.7.4/plugins/typescript.mjs')
25
34
  ])
35
+ _prettier = prettierModule
36
+ _plugins = plugins
26
37
  }
27
38
 
28
39
  const { options, source } = message
29
- const formatted = await prettier.format(source, {
40
+ return _prettier.format(source, {
30
41
  parser: 'markdown',
31
- plugins: prettierPlugins,
42
+ plugins: _plugins,
32
43
  ...options
33
44
  })
34
-
35
- return formatted
36
45
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { join } from 'pathe'
3
3
  import {
4
4
  addComponent,
5
5
  addComponentsDir,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import fsp from 'node:fs/promises'
3
- import { dirname, join } from 'pathe'
4
- import { defineNuxtModule, addTemplate, addServerHandler, createResolver } from '@nuxt/kit'
3
+ import { join } from 'pathe'
4
+ import { defineNuxtModule, addServerHandler, createResolver } from '@nuxt/kit'
5
5
 
6
6
  export default defineNuxtModule({
7
7
  meta: {
@@ -11,13 +11,20 @@ export default defineNuxtModule({
11
11
  const resolver = createResolver(import.meta.url)
12
12
  let _configResolved: any
13
13
  let components: Record<string, any>
14
- const outputPath = join(nuxt.options.buildDir, 'component-example')
14
+ const outputDir = join(nuxt.options.buildDir, 'component-examples')
15
+
16
+ async function ensureOutputDir() {
17
+ if (!existsSync(outputDir)) {
18
+ await fsp.mkdir(outputDir, { recursive: true })
19
+ }
20
+ }
15
21
 
16
22
  async function stubOutput() {
17
- if (existsSync(outputPath + '.mjs')) {
18
- return
23
+ await ensureOutputDir()
24
+ const indexPath = join(outputDir, '_index.json')
25
+ if (!existsSync(indexPath)) {
26
+ await fsp.writeFile(indexPath, '[]', 'utf-8')
19
27
  }
20
- await updateOutput('export default {}')
21
28
  }
22
29
 
23
30
  async function fetchComponent(component: string | any) {
@@ -46,20 +53,26 @@ export default defineNuxtModule({
46
53
  pascalName: component.pascalName
47
54
  }
48
55
  }
49
- const getStringifiedComponents = () => JSON.stringify(components, null, 2)
50
56
 
51
- const getVirtualModuleContent = () =>
52
- `export default ${getStringifiedComponents()}`
57
+ async function writeComponentFile(name: string) {
58
+ const comp = components[name]
59
+ if (!comp?.code) return
60
+ await fsp.writeFile(
61
+ join(outputDir, `${name}.json`),
62
+ JSON.stringify({ code: comp.code, filePath: comp.filePath, pascalName: comp.pascalName }),
63
+ 'utf-8'
64
+ )
65
+ }
66
+
67
+ async function writeIndex() {
68
+ const names = Object.keys(components).filter(k => components[k]?.code)
69
+ await fsp.writeFile(join(outputDir, '_index.json'), JSON.stringify(names), 'utf-8')
70
+ }
53
71
 
54
- async function updateOutput(content?: string) {
55
- const path = outputPath + '.mjs'
56
- if (!existsSync(dirname(path))) {
57
- await fsp.mkdir(dirname(path), { recursive: true })
58
- }
59
- if (existsSync(path)) {
60
- await fsp.unlink(path)
61
- }
62
- await fsp.writeFile(path, content || getVirtualModuleContent(), 'utf-8')
72
+ async function updateOutput() {
73
+ await ensureOutputDir()
74
+ await Promise.all(Object.keys(components).map(writeComponentFile))
75
+ await writeIndex()
63
76
  }
64
77
 
65
78
  async function fetchComponents() {
@@ -76,12 +89,6 @@ export default defineNuxtModule({
76
89
  await stubOutput()
77
90
  })
78
91
 
79
- addTemplate({
80
- filename: 'component-example.mjs',
81
- getContents: () => 'export default {}',
82
- write: true
83
- })
84
-
85
92
  nuxt.hook('vite:extend', (vite: any) => {
86
93
  vite.config.plugins = vite.config.plugins || []
87
94
  vite.config.plugins.push({
@@ -104,19 +111,47 @@ export default defineNuxtModule({
104
111
  )
105
112
  ) {
106
113
  await fetchComponent(file)
107
- await updateOutput()
114
+ const entry = Object.entries(components).find(
115
+ ([, comp]: any) => comp.filePath === file
116
+ )
117
+ if (entry) {
118
+ await ensureOutputDir()
119
+ await writeComponentFile(entry[0])
120
+ await writeIndex()
121
+ }
108
122
  }
109
123
  }
110
124
  })
111
125
  })
112
126
 
113
127
  nuxt.hook('nitro:config', (nitroConfig) => {
128
+ nitroConfig.serverAssets = nitroConfig.serverAssets || []
129
+ nitroConfig.serverAssets.push({
130
+ baseName: 'component-examples',
131
+ dir: outputDir
132
+ })
133
+
114
134
  nitroConfig.virtual = nitroConfig.virtual || {}
115
- nitroConfig.virtual['#component-example/nitro'] = () =>
116
- readFileSync(
117
- join(nuxt.options.buildDir, '/component-example.mjs'),
118
- 'utf-8'
119
- )
135
+ nitroConfig.virtual['#component-example/nitro'] = () => {
136
+ const indexPath = join(outputDir, '_index.json')
137
+ const names: string[] = existsSync(indexPath)
138
+ ? JSON.parse(readFileSync(indexPath, 'utf-8'))
139
+ : []
140
+
141
+ return `import { useStorage } from '#imports'
142
+
143
+ const names = ${JSON.stringify(names)}
144
+ const _storage = () => useStorage('assets:component-examples')
145
+
146
+ export async function getComponentExample(name) {
147
+ return await _storage().getItem(name + '.json')
148
+ }
149
+
150
+ export async function listComponentExamples() {
151
+ return names
152
+ }
153
+ `
154
+ }
120
155
  })
121
156
 
122
157
  addServerHandler({
package/modules/config.ts CHANGED
@@ -1,9 +1,27 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { createResolver, defineNuxtModule } from '@nuxt/kit'
2
3
  import { defu } from 'defu'
4
+ import { join } from 'pathe'
3
5
  import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
4
6
  import { getPackageJsonMetadata, inferSiteURL } from '../utils/meta'
5
7
  import { createComponentMetaExcludeFilters } from '../utils/component-meta'
6
8
 
9
+ function getMdcConfigSources(nuxt: any): string[] {
10
+ return nuxt.options._layers.flatMap((layer: any) => {
11
+ const tsConfigPath = join(layer.config.srcDir, 'mdc.config.ts')
12
+ if (existsSync(tsConfigPath)) {
13
+ return [tsConfigPath]
14
+ }
15
+
16
+ const jsConfigPath = join(layer.config.srcDir, 'mdc.config.js')
17
+ if (existsSync(jsConfigPath)) {
18
+ return [jsConfigPath]
19
+ }
20
+
21
+ return []
22
+ })
23
+ }
24
+
7
25
  export default defineNuxtModule({
8
26
  meta: {
9
27
  name: 'config'
@@ -31,7 +49,6 @@ export default defineNuxtModule({
31
49
  domain: url || 'https://example.com',
32
50
  title: siteName,
33
51
  description: meta.description || '',
34
- contentRawMarkdown: false as const,
35
52
  full: {
36
53
  title: siteName,
37
54
  description: meta.description || ''
@@ -73,5 +90,11 @@ export default defineNuxtModule({
73
90
  ...createComponentMetaExcludeFilters(resolve, dir, layerPath, userInclude)
74
91
  ]
75
92
  })
93
+
94
+ // Load mdc.config from all layers
95
+ const mdcConfigSources = getMdcConfigSources(nuxt)
96
+ if (mdcConfigSources.length > 0) {
97
+ await nuxt.callHook('mdc:configSources', mdcConfigSources)
98
+ }
76
99
  }
77
100
  })
package/modules/css.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { addTemplate, defineNuxtModule } from '@nuxt/kit'
2
2
  import { fileURLToPath } from 'node:url'
3
- import { dirname, join } from 'node:path'
3
+ import { dirname, join } from 'pathe'
4
4
  import { joinURL } from 'ufo'
5
5
  import { resolveModulePath } from 'exsolve'
6
6
 
package/nuxt.config.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  import { defineNuxtConfig } from 'nuxt/config'
2
2
  import pkg from './package.json'
3
+ import { createResolver, useNuxt } from '@nuxt/kit'
4
+ import { join } from 'pathe'
5
+
6
+ const { resolve } = createResolver(import.meta.url)
3
7
 
4
8
  export default defineNuxtConfig({
5
9
  modules: [
10
+ () => {
11
+ const nuxt = useNuxt()
12
+ nuxt.options.icon ||= {}
13
+ nuxt.options.icon.customCollections ||= []
14
+ nuxt.options.icon.customCollections.push({
15
+ prefix: 'custom',
16
+ dir: join(nuxt.options.srcDir, 'assets/icons')
17
+ })
18
+ },
6
19
  '@nuxt/ui',
7
20
  '@nuxt/content',
8
21
  '@nuxt/image',
@@ -24,6 +37,7 @@ export default defineNuxtConfig({
24
37
  },
25
38
 
26
39
  content: {
40
+ experimental: { sqliteConnector: 'native' },
27
41
  build: {
28
42
  markdown: {
29
43
  highlight: {
@@ -46,6 +60,15 @@ export default defineNuxtConfig({
46
60
  }
47
61
  },
48
62
 
63
+ ui: {
64
+ theme: {
65
+ colors: ['primary', 'secondary', 'info', 'success', 'warning', 'error', 'important']
66
+ },
67
+ experimental: {
68
+ componentDetection: true
69
+ }
70
+ },
71
+
49
72
  runtimeConfig: {
50
73
  public: {
51
74
  version: pkg.version
@@ -61,7 +84,7 @@ export default defineNuxtConfig({
61
84
  }
62
85
  },
63
86
 
64
- compatibilityDate: 'latest',
87
+ compatibilityDate: '2026-01-14',
65
88
 
66
89
  nitro: {
67
90
  prerender: {
@@ -143,7 +166,22 @@ export default defineNuxtConfig({
143
166
  },
144
167
 
145
168
  icon: {
146
- provider: 'iconify'
169
+ customCollections: [
170
+ {
171
+ prefix: 'custom',
172
+ dir: resolve('./app/assets/icons')
173
+ }
174
+ ],
175
+ clientBundle: {
176
+ scan: true,
177
+ includeCustomCollections: true
178
+ }
179
+ },
180
+
181
+ llms: {
182
+ // Must be defined before @nuxt/content setup,
183
+ // otherwise Content LLMS module will overwrite it in modules:done.
184
+ contentRawMarkdown: false
147
185
  },
148
186
 
149
187
  ogImage: {
package/nuxt.schema.ts CHANGED
@@ -315,7 +315,7 @@ export default defineNuxtSchema({
315
315
  aiChat: group({
316
316
  title: 'AI Chat',
317
317
  description: 'AI 聊天助手配置',
318
- icon: 'i-lucide-sparkles',
318
+ icon: 'i-custom-ai',
319
319
  fields: {
320
320
  floatingInput: field({
321
321
  type: 'boolean',
@@ -440,7 +440,7 @@ export default defineNuxtSchema({
440
440
  type: 'string',
441
441
  title: '触发按钮',
442
442
  description: 'AI 聊天面板触发按钮的提示文本',
443
- icon: 'i-lucide-sparkles',
443
+ icon: 'i-custom-ai',
444
444
  default: '与 AI 聊天'
445
445
  }),
446
446
  streaming: field({
@@ -483,8 +483,8 @@ export default defineNuxtSchema({
483
483
  type: 'string',
484
484
  title: '触发图标',
485
485
  description: 'AI 聊天触发按钮的图标',
486
- icon: 'i-lucide-sparkles',
487
- default: 'i-lucide-sparkles'
486
+ icon: 'i-custom-ai',
487
+ default: 'i-custom-ai'
488
488
  }),
489
489
  explain: field({
490
490
  type: 'string',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@movk/nuxt-docs",
3
3
  "type": "module",
4
- "version": "1.13.1",
4
+ "version": "1.14.0",
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>",
@@ -39,36 +39,35 @@
39
39
  "README.md"
40
40
  ],
41
41
  "dependencies": {
42
- "@ai-sdk/gateway": "^3.0.55",
43
- "@ai-sdk/mcp": "^1.0.21",
44
- "@ai-sdk/vue": "^3.0.99",
45
- "@iconify-json/lucide": "^1.2.94",
46
- "@iconify-json/simple-icons": "^1.2.71",
42
+ "@ai-sdk/gateway": "^3.0.66",
43
+ "@ai-sdk/mcp": "^1.0.25",
44
+ "@ai-sdk/vue": "^3.0.116",
45
+ "@iconify-json/lucide": "^1.2.95",
46
+ "@iconify-json/simple-icons": "^1.2.72",
47
47
  "@iconify-json/vscode-icons": "^1.2.44",
48
- "@movk/core": "^1.1.0",
48
+ "@movk/core": "^1.2.0",
49
49
  "@nuxt/a11y": "^1.0.0-alpha.1",
50
- "@nuxt/content": "^3.11.2",
50
+ "@nuxt/content": "^3.12.0",
51
51
  "@nuxt/image": "^2.0.0",
52
52
  "@nuxt/kit": "^4.3.1",
53
- "@nuxt/ui": "^4.5.0",
53
+ "@nuxt/ui": "^4.5.1",
54
54
  "@nuxtjs/mcp-toolkit": "^0.7.0",
55
- "@nuxtjs/mdc": "^0.20.1",
56
- "@nuxtjs/robots": "^5.7.0",
55
+ "@nuxtjs/mdc": "^0.20.2",
56
+ "@nuxtjs/robots": "^5.7.1",
57
57
  "@octokit/rest": "^22.0.1",
58
58
  "@openrouter/ai-sdk-provider": "^2.2.3",
59
- "@shikijs/core": "^3.23.0",
60
- "@shikijs/engine-javascript": "^3.23.0",
61
- "@shikijs/langs": "^3.23.0",
62
- "@shikijs/themes": "^3.23.0",
59
+ "@shikijs/core": "^4.0.1",
60
+ "@shikijs/engine-javascript": "^4.0.1",
61
+ "@shikijs/langs": "^4.0.1",
62
+ "@shikijs/themes": "^4.0.1",
63
63
  "@vercel/analytics": "^1.6.1",
64
64
  "@vercel/speed-insights": "^1.3.1",
65
65
  "@vueuse/core": "^14.2.1",
66
66
  "@vueuse/nuxt": "^14.2.1",
67
- "ai": "^6.0.99",
67
+ "ai": "^6.0.111",
68
68
  "defu": "^6.1.4",
69
69
  "exsolve": "^1.0.8",
70
70
  "git-url-parse": "^16.1.0",
71
- "minimark": "^1.0.0",
72
71
  "motion-v": "^2.0.0",
73
72
  "nuxt": "^4.3.1",
74
73
  "nuxt-component-meta": "^0.17.2",
@@ -79,6 +78,7 @@
79
78
  "pkg-types": "^2.3.0",
80
79
  "prettier": "^3.8.1",
81
80
  "scule": "^1.3.0",
81
+ "shiki-transformer-color-highlight": "^1.1.0",
82
82
  "shiki-stream": "^0.1.4",
83
83
  "tailwind-merge": "^3.5.0",
84
84
  "tailwindcss": "^4.2.1",
@@ -1,17 +1,17 @@
1
1
  import { defineEventHandler, createError, appendHeader } from 'h3'
2
2
  import { pascalCase } from 'scule'
3
3
  // @ts-expect-error - no types available
4
- import components from '#component-example/nitro'
4
+ import { getComponentExample } from '#component-example/nitro'
5
5
 
6
- export default defineEventHandler((event) => {
6
+ export default defineEventHandler(async (event) => {
7
7
  appendHeader(event, 'Access-Control-Allow-Origin', '*')
8
8
  const componentName = (event.context.params?.['component?'] || '').replace(/\.json$/, '')
9
9
  if (componentName) {
10
- const component = components[pascalCase(componentName)]
10
+ const component = await getComponentExample(pascalCase(componentName))
11
11
  if (!component) {
12
12
  throw createError({
13
- statusText: 'Example not found!',
14
- status: 404
13
+ statusMessage: 'Example not found!',
14
+ statusCode: 404
15
15
  })
16
16
  }
17
17
  return component
@@ -24,20 +24,23 @@ export default defineCachedEventHandler(async (event) => {
24
24
  })
25
25
  }
26
26
 
27
- const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
27
+ const octokit = new Octokit({
28
+ auth: process.env.NUXT_GITHUB_TOKEN,
29
+ request: { timeout: 10_000 }
30
+ })
28
31
 
29
32
  const allCommits = await Promise.all(
30
33
  paths.map(path =>
31
- octokit.paginate(octokit.rest.repos.listCommits, {
34
+ octokit.rest.repos.listCommits({
32
35
  sha: github.branch,
33
36
  owner: github.owner,
34
37
  repo: github.name,
35
38
  path,
36
39
  since: github.since,
37
- per_page: github.per_page,
40
+ per_page: github.per_page || 100,
38
41
  until: github.until,
39
42
  author
40
- })
43
+ }).then(res => res.data).catch(() => [])
41
44
  )
42
45
  )
43
46
 
@@ -22,16 +22,19 @@ export default defineCachedEventHandler(async (event) => {
22
22
  })
23
23
  }
24
24
 
25
- const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
25
+ const octokit = new Octokit({
26
+ auth: process.env.NUXT_GITHUB_TOKEN,
27
+ request: { timeout: 10_000 }
28
+ })
26
29
 
27
30
  try {
28
- const { data: commits } = await octokit.rest.repos.listCommits({
31
+ const commits = await octokit.rest.repos.listCommits({
29
32
  sha: github.branch,
30
33
  owner: github.owner,
31
34
  repo: github.name,
32
35
  path,
33
36
  per_page: 1
34
- })
37
+ }).then(res => res.data).catch(() => [])
35
38
 
36
39
  if (!commits.length) {
37
40
  return null
@@ -54,16 +57,16 @@ export default defineCachedEventHandler(async (event) => {
54
57
  // 从 squash commit message 中提取 PR 编号 (#166)
55
58
  const prMatch = commit.commit.message.match(/#(\d+)/)
56
59
  if (prMatch?.[1]) {
57
- const { data: prData } = await octokit.rest.pulls.get({
60
+ const prData = await octokit.rest.pulls.get({
58
61
  owner: github.owner,
59
62
  repo: github.name,
60
63
  pull_number: Number.parseInt(prMatch[1])
61
- })
64
+ }).then(res => res.data).catch(() => null)
62
65
 
63
- authorLogin = prData.user?.login ?? authorLogin
64
- authorAvatar = prData.user?.avatar_url ?? authorAvatar
65
- authorName = prData.user?.name || authorLogin
66
- commitUrl = prData.html_url
66
+ authorLogin = prData?.user?.login ?? authorLogin
67
+ authorAvatar = prData?.user?.avatar_url ?? authorAvatar
68
+ authorName = prData?.user?.name || authorLogin
69
+ commitUrl = prData?.html_url ?? commitUrl
67
70
  }
68
71
  } catch {
69
72
  // 获取 PR 信息失败时忽略,使用原始提交者信息
@@ -16,10 +16,10 @@ export default defineCachedEventHandler(async () => {
16
16
 
17
17
  const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
18
18
 
19
- const { data: releases } = await octokit.rest.repos.listReleases({
19
+ const releases = await octokit.rest.repos.listReleases({
20
20
  owner: github.owner,
21
21
  repo: github.name
22
- })
22
+ }).then(res => res.data).catch(() => [])
23
23
 
24
24
  return releases
25
25
  }, {
@@ -0,0 +1,26 @@
1
+ import { queryCollection } from '@nuxt/content/server'
2
+
3
+ export default defineMcpResource({
4
+ uri: 'resource://docs/documentation-pages',
5
+ description: '所有可用文档页面的完整列表',
6
+ cache: '1h',
7
+ async handler(uri: URL) {
8
+ const event = useEvent()
9
+
10
+ const pages = await queryCollection(event, 'docs').all()
11
+
12
+ const result = pages.map(doc => ({
13
+ title: doc.title,
14
+ description: doc.description,
15
+ path: doc.path
16
+ }))
17
+
18
+ return {
19
+ contents: [{
20
+ uri: uri.toString(),
21
+ mimeType: 'application/json',
22
+ text: JSON.stringify(result, null, 2)
23
+ }]
24
+ }
25
+ }
26
+ })