cocowiki 0.2.2

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/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "cocowiki",
3
+ "version": "0.2.2",
4
+ "type": "module",
5
+ "description": "Search-first static wiki framework powered by VitePress and Vue.",
6
+ "author": "Mueo",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/coconi-dev/cocowiki#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/coconi-dev/cocowiki.git",
12
+ "directory": "packages/cocowiki"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/coconi-dev/cocowiki/issues"
16
+ },
17
+ "keywords": [
18
+ "wiki",
19
+ "vitepress",
20
+ "vue",
21
+ "markdown",
22
+ "static-site"
23
+ ],
24
+ "bin": {
25
+ "cocowiki": "./bin/cocowiki.mjs"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./src/config.ts",
30
+ "import": "./dist/config.js"
31
+ },
32
+ "./runtime": {
33
+ "types": "./src/node/vitepress.ts",
34
+ "import": "./dist/node/vitepress.js"
35
+ },
36
+ "./theme": "./src/theme/index.ts",
37
+ "./theme/customization": "./src/theme/customization.ts",
38
+ "./theme/components": "./src/theme/components/index.ts"
39
+ },
40
+ "files": [
41
+ "bin",
42
+ "dist",
43
+ "src"
44
+ ],
45
+ "scripts": {
46
+ "build:package": "esbuild src/config.ts src/node/vitepress.ts --bundle --platform=node --format=esm --packages=external --outdir=dist",
47
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
48
+ "prepack": "esbuild src/config.ts src/node/vitepress.ts --bundle --platform=node --format=esm --packages=external --outdir=dist",
49
+ "prepublishOnly": "vue-tsc --noEmit -p tsconfig.json"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "dependencies": {
55
+ "fast-glob": "^3.3.3",
56
+ "gray-matter": "^4.0.3",
57
+ "markdown-it-container": "^4.0.0",
58
+ "minisearch": "^7.2.0",
59
+ "vite": "^5.4.14",
60
+ "vitepress": "^1.6.4",
61
+ "vue": "^3.5.42"
62
+ },
63
+ "devDependencies": {
64
+ "@types/markdown-it-container": "^2.0.12",
65
+ "@types/node": "^22.20.3",
66
+ "esbuild": "^0.21.3",
67
+ "typescript": "^5.7.3",
68
+ "vue-tsc": "^2.2.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=20"
72
+ }
73
+ }
package/src/config.ts ADDED
@@ -0,0 +1,102 @@
1
+ export interface CocoWikiNavigationItem {
2
+ text: string
3
+ link?: string
4
+ items?: CocoWikiNavigationItem[]
5
+ }
6
+
7
+ export interface CocoWikiDirectories {
8
+ content?: string
9
+ components?: string
10
+ data?: string
11
+ public?: string
12
+ out?: string
13
+ }
14
+
15
+ export interface CocoWikiThemeComponents {
16
+ Header?: string
17
+ Home?: string
18
+ Search?: string
19
+ Archive?: string
20
+ Contributors?: string
21
+ PageMeta?: string
22
+ PageOutline?: string
23
+ ContentSidebar?: string
24
+ PageNavigation?: string
25
+ PageFooter?: string
26
+ SearchOverlay?: string
27
+ Loading?: string
28
+ NotFound?: string
29
+ }
30
+
31
+ export interface CocoWikiThemeConfig {
32
+ /** A complete VitePress-compatible theme entry, relative to the project root. */
33
+ entry?: string
34
+ /** One or more style sheets, relative to the project root. */
35
+ styles?: string | string[]
36
+ /** Replace individual components while keeping the default CoCoWiki layout. */
37
+ components?: CocoWikiThemeComponents
38
+ }
39
+
40
+ export interface CocoWikiConfig {
41
+ title: string
42
+ description?: string
43
+ lang?: string
44
+ logo?: string
45
+ favicon?: string
46
+ base?: string
47
+ navigation?: CocoWikiNavigationItem[]
48
+ directories?: CocoWikiDirectories
49
+ theme?: CocoWikiThemeConfig
50
+ search?: {
51
+ enabled?: boolean
52
+ include?: string[]
53
+ exclude?: string[]
54
+ }
55
+ markdown?: { spoiler?: boolean; wikiLink?: boolean }
56
+ content?: {
57
+ outline?: boolean
58
+ sidebar?: 'none' | 'light' | 'outline'
59
+ showPrevNext?: boolean
60
+ showLastUpdated?: boolean
61
+ }
62
+ home?: {
63
+ name?: string
64
+ text?: string
65
+ tagline?: string
66
+ eyebrow?: string
67
+ note?: string
68
+ search?: { placeholder?: string }
69
+ visual?: {
70
+ image?: string
71
+ labels?: string[]
72
+ }
73
+ actions?: Array<{
74
+ text: string
75
+ link: string
76
+ theme?: 'brand' | 'alt'
77
+ }>
78
+ }
79
+ }
80
+
81
+ export interface CocoWikiPageMeta {
82
+ title: string
83
+ layout?: string
84
+ description?: string
85
+ type?: string
86
+ category?: string
87
+ tags?: string[]
88
+ aliases?: string[]
89
+ contributors?: string[]
90
+ image?: string
91
+ updated?: string
92
+ search?: boolean
93
+ archive?: boolean
94
+ outline?: boolean
95
+ sidebar?: 'none' | 'light' | 'outline'
96
+ showPrevNext?: boolean
97
+ showLastUpdated?: boolean
98
+ }
99
+
100
+ export function defineCocoWikiConfig(config: CocoWikiConfig): CocoWikiConfig {
101
+ return config
102
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ declare module 'virtual:cocowiki-config' {
2
+ import type { CocoWikiConfig } from './config'
3
+ const config: CocoWikiConfig
4
+ export default config
5
+ }
6
+
7
+ declare module 'virtual:cocowiki-contributors' {
8
+ const contributors: Record<string, {
9
+ name?: string
10
+ initials?: string
11
+ role?: string
12
+ avatar?: string
13
+ url?: string
14
+ github?: string
15
+ }>
16
+ export default contributors
17
+ }
@@ -0,0 +1,86 @@
1
+ import fg from 'fast-glob'
2
+ import matter from 'gray-matter'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { relative, sep } from 'node:path'
5
+ import type { CocoWikiPageMeta } from '../config.ts'
6
+
7
+ export interface SearchRecord extends CocoWikiPageMeta {
8
+ id: string
9
+ route: string
10
+ content: string
11
+ excerpt: string
12
+ }
13
+
14
+ function plainText(markdown: string) {
15
+ return markdown
16
+ .replace(/```[\s\S]*?```/g, ' ')
17
+ .replace(/<[^>]+>/g, ' ')
18
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
19
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
20
+ .replace(/\[\[([^\]]+)\]\]/g, '$1')
21
+ .replace(/[#>*_`~|:-]/g, ' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim()
24
+ }
25
+
26
+ function routeFromFile(contentRoot: string, filename: string) {
27
+ const path = relative(contentRoot, filename).split(sep).join('/').replace(/(?:\.page\.json|\.md)$/, '')
28
+ return path === 'index' ? '/' : `/${path.replace(/\/index$/, '')}`
29
+ }
30
+
31
+ function recordFromMeta(filename: string, contentRoot: string, meta: Partial<CocoWikiPageMeta>, content = '') {
32
+ const route = routeFromFile(contentRoot, filename)
33
+ return {
34
+ id: route,
35
+ route,
36
+ title: meta.title || route.split('/').pop() || 'Untitled',
37
+ layout: meta.layout,
38
+ description: meta.description || '',
39
+ type: meta.type || '',
40
+ category: meta.category || '',
41
+ tags: meta.tags || [],
42
+ aliases: meta.aliases || [],
43
+ contributors: meta.contributors || [],
44
+ image: meta.image,
45
+ updated: meta.updated,
46
+ search: meta.search,
47
+ archive: meta.archive,
48
+ content,
49
+ excerpt: meta.description || content.slice(0, 120)
50
+ } satisfies SearchRecord
51
+ }
52
+
53
+ export async function scanContent(
54
+ contentRoot: string,
55
+ options: { include?: string[]; exclude?: string[] } = {}
56
+ ): Promise<SearchRecord[]> {
57
+ const filenames = await fg(options.include || ['**/*.md'], {
58
+ cwd: contentRoot,
59
+ absolute: true,
60
+ ignore: options.exclude || []
61
+ })
62
+
63
+ const pageMetadata = await fg('**/*.page.json', {
64
+ cwd: contentRoot,
65
+ absolute: true,
66
+ ignore: options.exclude || []
67
+ })
68
+
69
+ const records = await Promise.all(filenames.map(async (filename) => {
70
+ const source = await readFile(filename, 'utf8')
71
+ const parsed = matter(source)
72
+ const content = plainText(parsed.content)
73
+ const firstHeading = parsed.content.match(/^#\s+(.+)$/m)?.[1]?.trim()
74
+ const meta = parsed.data as Partial<CocoWikiPageMeta>
75
+
76
+ return recordFromMeta(filename, contentRoot, { ...meta, title: meta.title || firstHeading }, content)
77
+ }))
78
+
79
+ const vuePageRecords = await Promise.all(pageMetadata.map(async (filename) => {
80
+ const meta = JSON.parse(await readFile(filename, 'utf8')) as Partial<CocoWikiPageMeta>
81
+ return recordFromMeta(filename, contentRoot, meta, meta.description || '')
82
+ }))
83
+
84
+ return [...records, ...vuePageRecords]
85
+ .sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'))
86
+ }
@@ -0,0 +1,115 @@
1
+ import container from 'markdown-it-container'
2
+ import type { SearchRecord } from './content.ts'
3
+
4
+ function addSpoilerRule(md: any) {
5
+ md.core.ruler.after('inline', 'cocowiki-spoiler', (state: any) => {
6
+ for (const blockToken of state.tokens) {
7
+ if (blockToken.type !== 'inline' || !blockToken.children) continue
8
+ const children = []
9
+
10
+ for (const token of blockToken.children) {
11
+ if (token.type !== 'text' || !token.content.includes('||')) {
12
+ children.push(token)
13
+ continue
14
+ }
15
+
16
+ const pattern = /\|\|([^|]+?)\|\|/g
17
+ let cursor = 0
18
+ let match: RegExpExecArray | null
19
+ while ((match = pattern.exec(token.content))) {
20
+ if (match.index > cursor) {
21
+ const text = new state.Token('text', '', 0)
22
+ text.content = token.content.slice(cursor, match.index)
23
+ children.push(text)
24
+ }
25
+ const spoiler = new state.Token('html_inline', '', 0)
26
+ spoiler.content = `<button class="cw-spoiler" type="button" aria-expanded="false" aria-label="剧透内容,点击显示"><span>${md.utils.escapeHtml(match[1])}</span></button>`
27
+ children.push(spoiler)
28
+ cursor = pattern.lastIndex
29
+ }
30
+
31
+ if (cursor === 0) {
32
+ children.push(token)
33
+ } else if (cursor < token.content.length) {
34
+ const text = new state.Token('text', '', 0)
35
+ text.content = token.content.slice(cursor)
36
+ children.push(text)
37
+ }
38
+ }
39
+
40
+ blockToken.children = children
41
+ }
42
+ })
43
+ }
44
+
45
+ function addWikiLinkRule(md: any, records: SearchRecord[], base: string) {
46
+ const prefix = base === '/' ? '' : base.replace(/\/$/, '')
47
+ const routes = new Map<string, string>()
48
+ for (const record of records) {
49
+ routes.set(record.title, record.route)
50
+ for (const alias of record.aliases || []) routes.set(alias, record.route)
51
+ }
52
+
53
+ md.inline.ruler.before('link', 'cocowiki-wikilink', (state: any, silent: boolean) => {
54
+ const start = state.pos
55
+ if (state.src.slice(start, start + 2) !== '[[') return false
56
+ const end = state.src.indexOf(']]', start + 2)
57
+ if (end < 0) return false
58
+ const name = state.src.slice(start + 2, end).trim()
59
+ if (!name) return false
60
+ if (!silent) {
61
+ const route = routes.get(name) || `/search?q=${encodeURIComponent(name)}`
62
+ const token = state.push('html_inline', '', 0)
63
+ token.content = `<a class="cw-wikilink" href="${prefix}${route}">${md.utils.escapeHtml(name)}</a>`
64
+ }
65
+ state.pos = end + 2
66
+ return true
67
+ })
68
+ }
69
+
70
+ function addTableWrapper(md: any) {
71
+ const open = md.renderer.rules.table_open || ((tokens: any[], index: number, options: any, _env: any, renderer: any) => renderer.renderToken(tokens, index, options))
72
+ const close = md.renderer.rules.table_close || ((tokens: any[], index: number, options: any, _env: any, renderer: any) => renderer.renderToken(tokens, index, options))
73
+ md.renderer.rules.table_open = (...args: any[]) => `<div class="cw-table-scroll">${open(...args)}`
74
+ md.renderer.rules.table_close = (...args: any[]) => `${close(...args)}</div>`
75
+ }
76
+
77
+ const calloutPresets = {
78
+ info: { title: '信息', icon: 'i' },
79
+ tip: { title: '提示', icon: '✓' },
80
+ warning: { title: '注意', icon: '!' },
81
+ danger: { title: '警告', icon: '!' }
82
+ } as const
83
+
84
+ function addCalloutContainers(md: any) {
85
+ for (const [name, preset] of Object.entries(calloutPresets)) {
86
+ md.use(container, name, {
87
+ render(tokens: Array<{ nesting: number; info: string }>, index: number) {
88
+ if (tokens[index].nesting !== 1) return '</aside>\n'
89
+ const customTitle = tokens[index].info.trim().slice(name.length).trim()
90
+ const title = md.utils.escapeHtml(customTitle || preset.title)
91
+ return `<aside class="cw-callout cw-callout--${name}"><span class="cw-callout__icon" aria-hidden="true">${preset.icon}</span><p class="cw-callout__title">${title}</p>\n`
92
+ }
93
+ })
94
+ }
95
+
96
+ md.use(container, 'details', {
97
+ render(tokens: Array<{ nesting: number; info: string }>, index: number) {
98
+ if (tokens[index].nesting !== 1) return '</details>\n'
99
+ const customTitle = tokens[index].info.trim().slice('details'.length).trim()
100
+ const title = md.utils.escapeHtml(customTitle || '详细信息')
101
+ return `<details class="cw-callout cw-callout--details"><summary>${title}</summary>\n`
102
+ }
103
+ })
104
+ }
105
+
106
+ export function applyCocoWikiMarkdown(
107
+ md: any,
108
+ records: SearchRecord[],
109
+ options: { spoiler?: boolean; wikiLink?: boolean; base?: string }
110
+ ) {
111
+ if (options.spoiler !== false) addSpoilerRule(md)
112
+ if (options.wikiLink !== false) addWikiLinkRule(md, records, options.base || '/')
113
+ addTableWrapper(md)
114
+ addCalloutContainers(md)
115
+ }
@@ -0,0 +1,25 @@
1
+ import { resolve } from 'node:path'
2
+ import type { CocoWikiConfig } from '../config.ts'
3
+
4
+ export interface CocoWikiProjectPaths {
5
+ projectRoot: string
6
+ runtimeRoot: string
7
+ contentDir: string
8
+ componentsDir: string
9
+ dataDir: string
10
+ publicDir: string
11
+ outDir: string
12
+ }
13
+
14
+ export function resolveProjectPaths(projectRoot: string, config: CocoWikiConfig): CocoWikiProjectPaths {
15
+ const directories = config.directories || {}
16
+ return {
17
+ projectRoot,
18
+ runtimeRoot: resolve(projectRoot, '.cocowiki/site'),
19
+ contentDir: resolve(projectRoot, directories.content || 'content'),
20
+ componentsDir: resolve(projectRoot, directories.components || 'components'),
21
+ dataDir: resolve(projectRoot, directories.data || 'data'),
22
+ publicDir: resolve(projectRoot, directories.public || 'public'),
23
+ outDir: resolve(projectRoot, directories.out || 'dist')
24
+ }
25
+ }
@@ -0,0 +1,160 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { relative, resolve } from 'node:path'
4
+ import { defineConfig, type UserConfig } from 'vitepress'
5
+ import { createLogger, type Plugin } from 'vite'
6
+ import type { CocoWikiConfig } from '../config.ts'
7
+ import { scanContent, type SearchRecord } from './content.ts'
8
+ import { applyCocoWikiMarkdown } from './markdown.ts'
9
+ import { resolveProjectPaths } from './project.ts'
10
+
11
+ const CONFIG_ID = 'virtual:cocowiki-config'
12
+ const CONTRIBUTORS_ID = 'virtual:cocowiki-contributors'
13
+ const ANSI = { reset: '\x1b[0m', dim: '\x1b[2m', cyan: '\x1b[36m', yellow: '\x1b[33m', red: '\x1b[31m' }
14
+
15
+ function createCocoWikiLogger() {
16
+ const logger = createLogger()
17
+ const label = '[cocowiki]'
18
+ const supportsColor = Boolean(process.stdout.isTTY && !process.env.NO_COLOR)
19
+ const paint = (text: string, color: string) => supportsColor ? `${color}${text}${ANSI.reset}` : text
20
+ const branded = (message: string) => {
21
+ const normalized = message.replace(/\[(?:vitepress|vite)\]/gi, label)
22
+ return normalized.includes(label) ? normalized : `${label} ${normalized}`
23
+ }
24
+ const optionsWithoutViteLabel = (options?: any) => options ? { ...options, timestamp: false } : options
25
+ const timestamp = (options?: any) => options?.timestamp
26
+ ? paint(`${new Date().toLocaleTimeString('en-GB', { hour12: false })} `, ANSI.dim)
27
+ : ''
28
+ const format = (message: string, color: string, options?: any) => `${timestamp(options)}${paint(branded(message), color)}`
29
+ const warned = new Set<string>()
30
+ return {
31
+ ...logger,
32
+ info: (message: string, options?: any) => logger.info(format(message, ANSI.cyan, options), optionsWithoutViteLabel(options)),
33
+ warn: (message: string, options?: any) => logger.warn(format(message, ANSI.yellow, options), optionsWithoutViteLabel(options)),
34
+ warnOnce: (message: string, options?: any) => {
35
+ if (warned.has(message)) return
36
+ warned.add(message)
37
+ logger.warn(format(message, ANSI.yellow, options), optionsWithoutViteLabel(options))
38
+ },
39
+ error: (message: string, options?: any) => logger.error(format(message, ANSI.red, options), optionsWithoutViteLabel(options))
40
+ }
41
+ }
42
+
43
+ function contentPlugin(
44
+ config: CocoWikiConfig,
45
+ contentDir: string,
46
+ dataDir: string,
47
+ initialRecords: SearchRecord[]
48
+ ): Plugin {
49
+ const records = initialRecords
50
+ const resolvedConfigId = `\0${CONFIG_ID}`
51
+ const resolvedContributorsId = `\0${CONTRIBUTORS_ID}`
52
+ const contributorsFile = resolve(dataDir, 'contributors.json')
53
+
54
+ async function refresh() {
55
+ const nextRecords = await scanContent(contentDir, config.search)
56
+ records.splice(0, records.length, ...nextRecords)
57
+ }
58
+
59
+ return {
60
+ name: 'cocowiki-content-index',
61
+ resolveId(id) {
62
+ if (id === CONFIG_ID) return resolvedConfigId
63
+ if (id === CONTRIBUTORS_ID) return resolvedContributorsId
64
+ },
65
+ async load(id) {
66
+ if (id === resolvedConfigId) return `export default ${JSON.stringify(config)}`
67
+ if (id === resolvedContributorsId) {
68
+ const contributors = existsSync(contributorsFile) ? JSON.parse(await readFile(contributorsFile, 'utf8')) : {}
69
+ return `export default ${JSON.stringify(contributors)}`
70
+ }
71
+ },
72
+ configureServer(server) {
73
+ server.middlewares.use((request, response, next) => {
74
+ if (!request.url?.split('?')[0]?.endsWith('/search-index.json')) return next()
75
+ response.statusCode = 200
76
+ response.setHeader('Content-Type', 'application/json; charset=utf-8')
77
+ response.setHeader('Cache-Control', 'no-store')
78
+ response.end(JSON.stringify(records))
79
+ })
80
+ server.watcher.add([contentDir, contributorsFile])
81
+ let refreshTimer: ReturnType<typeof setTimeout> | undefined
82
+ const refreshContent = (file: string) => {
83
+ const path = relative(contentDir, file)
84
+ if (!path || path.startsWith('..') || (!path.endsWith('.md') && !path.endsWith('.page.json'))) return
85
+ clearTimeout(refreshTimer)
86
+ refreshTimer = setTimeout(async () => {
87
+ try {
88
+ await refresh()
89
+ server.ws.send({ type: 'custom', event: 'cocowiki:content-updated' })
90
+ } catch (error) {
91
+ server.config.logger.error(`刷新内容索引失败:${error instanceof Error ? error.message : String(error)}`)
92
+ }
93
+ }, 60)
94
+ }
95
+ const refreshContributors = (file: string) => {
96
+ if (file !== contributorsFile) return
97
+ const module = server.moduleGraph.getModuleById(resolvedContributorsId)
98
+ if (module) server.moduleGraph.invalidateModule(module)
99
+ server.ws.send({ type: 'full-reload' })
100
+ }
101
+ server.watcher.on('add', refreshContent)
102
+ server.watcher.on('change', refreshContent)
103
+ server.watcher.on('unlink', refreshContent)
104
+ server.watcher.on('add', refreshContributors)
105
+ server.watcher.on('change', refreshContributors)
106
+ server.watcher.on('unlink', refreshContributors)
107
+ },
108
+ generateBundle() {
109
+ this.emitFile({ type: 'asset', fileName: 'search-index.json', source: JSON.stringify(records) })
110
+ }
111
+ }
112
+ }
113
+
114
+ export async function createVitePressConfig(
115
+ config: CocoWikiConfig,
116
+ projectRoot: string
117
+ ): Promise<UserConfig> {
118
+ const paths = resolveProjectPaths(projectRoot, config)
119
+ const records = await scanContent(paths.contentDir, config.search)
120
+ const requestedBase = config.base || '/'
121
+ const base = `/${requestedBase.replace(/^\/+|\/+$/g, '')}${requestedBase === '/' ? '' : '/'}`
122
+ const favicon = config.favicon || config.logo || '/favicon.svg'
123
+ const faviconType = favicon.endsWith('.svg') ? 'image/svg+xml' : favicon.endsWith('.ico') ? 'image/x-icon' : 'image/png'
124
+ const customLogger = createCocoWikiLogger()
125
+
126
+ return defineConfig({
127
+ title: config.title,
128
+ description: config.description,
129
+ lang: config.lang || 'zh-CN',
130
+ base,
131
+ srcDir: paths.runtimeRoot,
132
+ cleanUrls: true,
133
+ outDir: paths.outDir,
134
+ cacheDir: resolve(projectRoot, '.cocowiki/cache'),
135
+ lastUpdated: true,
136
+ head: [
137
+ ['link', { rel: 'icon', type: faviconType, href: `${base}${favicon.replace(/^\//, '')}` }],
138
+ ['meta', { name: 'theme-color', content: '#ffffff', media: '(prefers-color-scheme: light)' }],
139
+ ['meta', { name: 'theme-color', content: '#0f1013', media: '(prefers-color-scheme: dark)' }]
140
+ ],
141
+ vite: {
142
+ customLogger,
143
+ publicDir: paths.publicDir,
144
+ plugins: [contentPlugin(config, paths.contentDir, paths.dataDir, records)],
145
+ server: { fs: { allow: [projectRoot, resolve(import.meta.dirname, '../..')] } },
146
+ resolve: {
147
+ alias: {
148
+ '@components': paths.componentsDir,
149
+ '@data': paths.dataDir
150
+ }
151
+ }
152
+ },
153
+ markdown: {
154
+ lineNumbers: false,
155
+ config(md) {
156
+ applyCocoWikiMarkdown(md, records, { ...config.markdown, base })
157
+ }
158
+ }
159
+ })
160
+ }