-
Notifications
You must be signed in to change notification settings - Fork 91
Expand file tree
/
Copy pathindex.ts
More file actions
156 lines (136 loc) · 5.4 KB
/
Copy pathindex.ts
File metadata and controls
156 lines (136 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { createDebug } from 'obug'
import type { Plugin } from 'vite'
import type { SiteConfig, DefaultTheme } from 'vitepress'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
export type { MossSearchOptions } from './types.js'
const debug = createDebug('vitepress:moss-indexer')
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const charMap: Record<string, string> = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
}
function escapeUnsafeChars(str: string): string {
return str.replace(/[<>/\\\b\f\n\r\t\0\u2028\u2029]/g, x => charMap[x] ?? x)
}
export function mossIndexerPlugin(): Plugin {
const virtualModuleId = 'virtual:moss-config'
const resolvedVirtualModuleId = '\0' + virtualModuleId
let siteConfig: SiteConfig<DefaultTheme.Config>
return {
name: 'vitepress:moss-indexer',
enforce: 'pre',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
// Shadowing components
if (id.endsWith('/VPNavBarSearch.vue') || id === './VPNavBarSearch.vue') {
const replacement = path.resolve(__dirname, '..', 'Search.vue')
if (fs.existsSync(replacement)) {
return replacement
}
}
if (id.endsWith('/VPNavBarSearchButton.vue') || id === './VPNavBarSearchButton.vue') {
const replacement = path.resolve(__dirname, '..', 'SearchButton.vue')
if (fs.existsSync(replacement)) {
return replacement
}
}
},
configResolved(config) {
siteConfig = (config as any).vitepress
},
load(id) {
if (id === resolvedVirtualModuleId) {
// If siteConfig isn't available yet or search isn't moss, return empty config
const searchConfig = siteConfig?.site?.themeConfig?.search as any
if (searchConfig?.provider !== 'moss') {
return 'export default () => ({})'
}
const searchOptions = searchConfig.options || {}
return `export default () => (${escapeUnsafeChars(JSON.stringify(searchOptions))})`
}
},
async buildEnd() {
// Only run when siteConfig is available and Moss search is enabled
const searchConfig = siteConfig.site.themeConfig?.search as any
if (!siteConfig || searchConfig?.provider !== 'moss') {
return
}
// Only run for client build and only during production build
// NOTE: environment.name is used in newer Vite/VitePress
try {
debug('Starting Moss index sync...')
const searchConfig = siteConfig.site.themeConfig?.search as any
const searchOptions =
searchConfig?.provider === 'moss' ? searchConfig.options : undefined
// Get credentials from config options only
const projectId = searchOptions?.projectId
const projectKey = searchOptions?.projectKey
const indexName = searchOptions?.indexName
// Validate required credentials
if (!projectId || !projectKey || !indexName) {
throw new Error(
'Missing Moss configuration: projectId, projectKey, and indexName must be provided in themeConfig.search.options. ' +
'Example: search: { provider: "moss", options: { projectId: "...", projectKey: "...", indexName: "..." } }'
)
}
const creds = {
projectId,
projectKey,
indexName
}
const mdIndexer = await import('@moss-tools/md-indexer')
const buildJsonDocs = mdIndexer.buildJsonDocs
const uploadDocuments = mdIndexer.uploadDocuments
// Build chunks
const allDocs = await buildJsonDocs(siteConfig.root) as any[]
debug(`Built ${allDocs.length} chunks`)
// Filter: remove chunks with 3 or fewer words
const wordFiltered = allDocs.filter((doc: any) => {
const wordCount = doc.text?.trim().split(/\s+/).filter(Boolean).length ?? 0
return wordCount > 3
})
// Filter: remove structural-only section headings
const STRUCTURAL_TITLES = new Set(['Parameters', 'Constructors', 'Methods', 'Properties', 'Type declaration'])
const structuralFiltered = wordFiltered.filter((doc: any) => {
const title: string = (doc.metadata?.title ?? '').trim().replace(/\u200b/g, '').trim()
return !STRUCTURAL_TITLES.has(title)
})
// Deduplicate: keep only the first chunk per (groupId, title)
const seen = new Set<string>()
const filtered = structuralFiltered.filter((doc: any) => {
const key = `${doc.metadata?.groupId ?? ''}||${doc.metadata?.title ?? ''}`
if (seen.has(key)) return false
seen.add(key)
return true
})
debug(`After filtering: ${filtered.length} chunks (removed ${allDocs.length - filtered.length})`)
await uploadDocuments(filtered, { ...creds, modelName: 'moss-minilm' })
debug('Moss index sync completed.')
} catch (error) {
const err = error as Error
siteConfig.logger.error(
`Moss index sync failed: ${err.message}\n` +
'The documentation build will continue without an updated Moss index.'
)
debug('Moss index sync error', err)
}
},
async closeBundle() {
await (this as any).buildEnd?.()
}
}
}