@life-and-dev/mdsite 0.0.8 → 0.0.15
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 +23 -1
- package/dist/commands/prepare.js +10 -12
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/prepare.test.js +9 -6
- package/dist/commands/prepare.test.js.map +1 -1
- package/mdsite-nuxt/app/components/AppFooter.vue +21 -6
- package/mdsite-nuxt/app/composables/useNavigationTree.ts +3 -1
- package/mdsite-nuxt/app/composables/useSearchIndex.ts +3 -1
- package/mdsite-nuxt/example.config.yml +1 -1
- package/mdsite-nuxt/nuxt.config.ts +9 -4
- package/mdsite-nuxt/package-lock.json +4414 -4615
- package/mdsite-nuxt/package.json +19 -19
- package/mdsite-nuxt/scripts/base-url.test.ts +22 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +2 -2
- package/mdsite-nuxt/scripts/generate-indices.test.ts +57 -0
- package/mdsite-nuxt/scripts/generate-indices.ts +33 -3
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +34 -0
- package/mdsite-nuxt/scripts/renderer-hooks.ts +5 -4
- package/mdsite-nuxt/utils/base-url.ts +25 -0
- package/package.json +5 -5
package/mdsite-nuxt/package.json
CHANGED
|
@@ -23,28 +23,28 @@
|
|
|
23
23
|
"test": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@nuxt/content": "^3.
|
|
27
|
-
"@types/node": "^
|
|
28
|
-
"better-sqlite3": "^12.
|
|
29
|
-
"fs-extra": "^11.3.
|
|
26
|
+
"@nuxt/content": "^3.14.0",
|
|
27
|
+
"@types/node": "^26.0.1",
|
|
28
|
+
"better-sqlite3": "^12.11.1",
|
|
29
|
+
"fs-extra": "^11.3.5",
|
|
30
30
|
"gray-matter": "^4.0.3",
|
|
31
|
-
"mermaid": "^11.
|
|
32
|
-
"nuxt": "^4.
|
|
33
|
-
"typescript": "^
|
|
34
|
-
"vue": "^3.5.
|
|
35
|
-
"vue-router": "^
|
|
36
|
-
"vue-tsc": "^3.
|
|
37
|
-
"vuetify-nuxt-module": "^0.
|
|
38
|
-
"yaml": "^2.
|
|
31
|
+
"mermaid": "^11.16.0",
|
|
32
|
+
"nuxt": "^4.4.8",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"vue": "^3.5.39",
|
|
35
|
+
"vue-router": "^5.1.0",
|
|
36
|
+
"vue-tsc": "^3.3.5",
|
|
37
|
+
"vuetify-nuxt-module": "^1.0.0-rc.1",
|
|
38
|
+
"yaml": "^2.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@playwright/test": "^1.
|
|
41
|
+
"@playwright/test": "^1.61.1",
|
|
42
42
|
"@types/fs-extra": "^11.0.4",
|
|
43
|
-
"@vitest/ui": "^4.
|
|
44
|
-
"playwright": "^1.
|
|
45
|
-
"remark-github-blockquote-alert": "^2.0
|
|
46
|
-
"sharp": "^0.
|
|
47
|
-
"tsx": "^4.
|
|
48
|
-
"vitest": "^4.
|
|
43
|
+
"@vitest/ui": "^4.1.9",
|
|
44
|
+
"playwright": "^1.61.1",
|
|
45
|
+
"remark-github-blockquote-alert": "^2.1.0",
|
|
46
|
+
"sharp": "^0.35.2",
|
|
47
|
+
"tsx": "^4.22.4",
|
|
48
|
+
"vitest": "^4.1.9"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { withBasePath } from '../utils/base-url'
|
|
3
|
+
|
|
4
|
+
describe('withBasePath', () => {
|
|
5
|
+
it('keeps root base URLs unchanged', () => {
|
|
6
|
+
expect(withBasePath('/_navigation.json', '/')).toBe('/_navigation.json')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('prefixes internal URLs with configured app base URL', () => {
|
|
10
|
+
expect(withBasePath('/_search-index.json', '/mdsite/')).toBe('/mdsite/_search-index.json')
|
|
11
|
+
expect(withBasePath('/about', '/mdsite')).toBe('/mdsite/about')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('does not prefix URLs that already include the base URL', () => {
|
|
15
|
+
expect(withBasePath('/mdsite/about', '/mdsite/')).toBe('/mdsite/about')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('leaves external and hash URLs unchanged', () => {
|
|
19
|
+
expect(withBasePath('https://example.com/icon.png', '/mdsite/')).toBe('https://example.com/icon.png')
|
|
20
|
+
expect(withBasePath('#section', '/mdsite/')).toBe('#section')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -143,12 +143,12 @@ export async function generateWebManifest(name: string) {
|
|
|
143
143
|
short_name: name,
|
|
144
144
|
icons: [
|
|
145
145
|
{
|
|
146
|
-
src: '
|
|
146
|
+
src: 'icon-192.png',
|
|
147
147
|
sizes: '192x192',
|
|
148
148
|
type: 'image/png'
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
|
-
src: '
|
|
151
|
+
src: 'icon-512.png',
|
|
152
152
|
sizes: '512x512',
|
|
153
153
|
type: 'image/png'
|
|
154
154
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { generateNavigationJson, generateSearchIndexJson } from './generate-indices.js'
|
|
8
|
+
|
|
9
|
+
describe('generated content indices', () => {
|
|
10
|
+
const originalEnv = { ...process.env }
|
|
11
|
+
let tempDir: string
|
|
12
|
+
let contentDir: string
|
|
13
|
+
let publicDir: string
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
process.env = { ...originalEnv }
|
|
17
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mdsite-indices-'))
|
|
18
|
+
contentDir = path.join(tempDir, 'content')
|
|
19
|
+
publicDir = path.join(tempDir, 'public')
|
|
20
|
+
await fs.mkdir(contentDir, { recursive: true })
|
|
21
|
+
process.env.CONTENT_DIR = contentDir
|
|
22
|
+
process.env.MDSITE_PUBLIC_DIR = publicDir
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
process.env = { ...originalEnv }
|
|
27
|
+
await fs.rm(tempDir, { force: true, recursive: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('falls back to markdown files when no menu exists', async () => {
|
|
31
|
+
await fs.writeFile(path.join(contentDir, 'index.md'), '# Home\n\nWelcome home.', 'utf8')
|
|
32
|
+
await fs.writeFile(path.join(contentDir, 'guide.md'), '# Guide\n\nUseful guide.', 'utf8')
|
|
33
|
+
|
|
34
|
+
await generateNavigationJson()
|
|
35
|
+
|
|
36
|
+
const navigation = JSON.parse(await fs.readFile(path.join(publicDir, '_navigation.json'), 'utf8'))
|
|
37
|
+
expect(navigation).toEqual(expect.arrayContaining([
|
|
38
|
+
expect.objectContaining({ path: '/', title: 'Home' }),
|
|
39
|
+
expect.objectContaining({ path: '/guide', title: 'Guide' }),
|
|
40
|
+
]))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('writes searchable markdown pages with excerpts', async () => {
|
|
44
|
+
await fs.writeFile(path.join(contentDir, 'guide.md'), '# Guide\n\nUseful guide content for search.', 'utf8')
|
|
45
|
+
|
|
46
|
+
await generateSearchIndexJson()
|
|
47
|
+
|
|
48
|
+
const searchIndex = JSON.parse(await fs.readFile(path.join(publicDir, '_search-index.json'), 'utf8'))
|
|
49
|
+
expect(searchIndex).toEqual([
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
excerpt: 'Useful guide content for search.',
|
|
52
|
+
path: '/guide',
|
|
53
|
+
title: 'Guide',
|
|
54
|
+
}),
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -31,6 +31,7 @@ function getSourceDir(): string {
|
|
|
31
31
|
* Get target public directory
|
|
32
32
|
*/
|
|
33
33
|
function getTargetDir(): string {
|
|
34
|
+
if (process.env.MDSITE_PUBLIC_DIR) return process.env.MDSITE_PUBLIC_DIR
|
|
34
35
|
return path.resolve(__dirname, '..', 'public')
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -325,6 +326,29 @@ function countNodes(nodes: MinimalTreeNode[]): number {
|
|
|
325
326
|
return count
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
async function buildFallbackNavigationTree(sourceDir: string): Promise<MinimalTreeNode[]> {
|
|
330
|
+
const markdownFiles = await getAllMarkdownFiles(sourceDir)
|
|
331
|
+
const nodes: MinimalTreeNode[] = []
|
|
332
|
+
|
|
333
|
+
for (const [order, filePath] of markdownFiles.sort().entries()) {
|
|
334
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
335
|
+
const metadata = extractMarkdownMetadata(content)
|
|
336
|
+
const urlPath = filePathToUrlPath(filePath, sourceDir)
|
|
337
|
+
const title = metadata.title ?? path.basename(filePath, '.md')
|
|
338
|
+
|
|
339
|
+
nodes.push({
|
|
340
|
+
id: `${urlPath.split('/').filter(Boolean).pop() || 'home'}-${order}`,
|
|
341
|
+
title,
|
|
342
|
+
path: urlPath,
|
|
343
|
+
type: 'link',
|
|
344
|
+
description: metadata.description,
|
|
345
|
+
isPrimary: true
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return nodes
|
|
350
|
+
}
|
|
351
|
+
|
|
328
352
|
/**
|
|
329
353
|
* Generate navigation JSON file
|
|
330
354
|
*/
|
|
@@ -343,9 +367,11 @@ export async function generateNavigationJson() {
|
|
|
343
367
|
try {
|
|
344
368
|
if (await fs.pathExists(menuPath)) {
|
|
345
369
|
const menuContent = await fs.readFile(menuPath, 'utf-8')
|
|
346
|
-
const menuItems = parseYaml(menuContent) as MenuItemType[]
|
|
347
|
-
|
|
348
|
-
|
|
370
|
+
const menuItems = parseYaml(menuContent) as MenuItemType[] | null
|
|
371
|
+
if (Array.isArray(menuItems) && menuItems.length > 0) {
|
|
372
|
+
const result = await processMenuItems(menuItems, '/')
|
|
373
|
+
tree = result.nodes
|
|
374
|
+
}
|
|
349
375
|
} else {
|
|
350
376
|
console.warn('⚠️ No _menu.yml or _menu.yaml found at:', menuPath)
|
|
351
377
|
}
|
|
@@ -353,6 +379,10 @@ export async function generateNavigationJson() {
|
|
|
353
379
|
console.error('Error building navigation tree:', error)
|
|
354
380
|
}
|
|
355
381
|
|
|
382
|
+
if (tree.length === 0) {
|
|
383
|
+
tree = await buildFallbackNavigationTree(sourceDir)
|
|
384
|
+
}
|
|
385
|
+
|
|
356
386
|
const targetDir = getTargetDir()
|
|
357
387
|
const outputPath = path.join(targetDir, '_navigation.json')
|
|
358
388
|
|
|
@@ -161,6 +161,40 @@ describe('renderer hooks orchestration', () => {
|
|
|
161
161
|
expect(process.env.NUXT_CONTENT_PATH).toBe(path.join('/renderer', 'legacy-docs'))
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
+
it('supports the checked-in legacy renderer config keys', () => {
|
|
165
|
+
resolveMdsiteConfigPathMock.mockReturnValue(undefined)
|
|
166
|
+
existsSyncMock.mockImplementation((target: string) => (
|
|
167
|
+
target === '/renderer/content.config.yml' || target === '/content/docs'
|
|
168
|
+
))
|
|
169
|
+
parseYamlMock.mockReturnValue({
|
|
170
|
+
contentPath: '/content/docs',
|
|
171
|
+
contentGitRepo: 'https://example.test/docs.git',
|
|
172
|
+
siteCanonical: 'https://example.test',
|
|
173
|
+
siteName: 'Legacy Site',
|
|
174
|
+
})
|
|
175
|
+
loadMdsiteConfigSyncMock.mockReturnValue({
|
|
176
|
+
config: { site: { name: 'Legacy Site' } },
|
|
177
|
+
configPath: '/renderer/.mdsite-compat.yml',
|
|
178
|
+
contentDir: '/content/docs',
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
prepareRendererRuntime('/renderer')
|
|
182
|
+
|
|
183
|
+
expect(stringifyYamlMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
184
|
+
server: expect.objectContaining({
|
|
185
|
+
repo: 'https://example.test/docs.git',
|
|
186
|
+
}),
|
|
187
|
+
site: expect.objectContaining({
|
|
188
|
+
canonical: 'https://example.test',
|
|
189
|
+
name: 'Legacy Site',
|
|
190
|
+
}),
|
|
191
|
+
}))
|
|
192
|
+
expect(loadMdsiteConfigSyncMock).toHaveBeenCalledWith({
|
|
193
|
+
configPath: '/renderer/.mdsite-compat.yml',
|
|
194
|
+
contentPath: '/content/docs',
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
164
198
|
it('exits when no renderer config can be resolved', () => {
|
|
165
199
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
166
200
|
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number) => {
|
|
@@ -109,7 +109,8 @@ function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const legacyConfig = YAML.parse(fs.readFileSync(legacyConfigPath, 'utf8')) ?? {}
|
|
112
|
-
const
|
|
112
|
+
const legacyContentPath = legacyConfig.content?.path || legacyConfig.content?.git?.path || legacyConfig.contentPath || 'docs'
|
|
113
|
+
const contentDir = path.resolve(rootDir, legacyContentPath)
|
|
113
114
|
const compatibilityConfigPath = path.join(rootDir, '.mdsite-compat.yml')
|
|
114
115
|
const compatibilityConfig = {
|
|
115
116
|
favicon: '',
|
|
@@ -121,11 +122,11 @@ function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
|
|
|
121
122
|
server: {
|
|
122
123
|
output: '.output',
|
|
123
124
|
path: '.mdsite',
|
|
124
|
-
repo: legacyConfig.content?.git?.repo || ''
|
|
125
|
+
repo: legacyConfig.content?.git?.repo || legacyConfig.contentGitRepo || ''
|
|
125
126
|
},
|
|
126
127
|
site: {
|
|
127
|
-
canonical: legacyConfig.site?.canonical || '',
|
|
128
|
-
name: legacyConfig.site?.name || path.basename(contentDir) || 'Site'
|
|
128
|
+
canonical: legacyConfig.site?.canonical || legacyConfig.siteCanonical || '',
|
|
129
|
+
name: legacyConfig.site?.name || legacyConfig.siteName || path.basename(contentDir) || 'Site'
|
|
129
130
|
},
|
|
130
131
|
themes: legacyConfig.themes || {}
|
|
131
132
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function withBasePath(pathname: string, baseURL = '/'): string {
|
|
2
|
+
if (isExternalOrHash(pathname)) return pathname
|
|
3
|
+
|
|
4
|
+
const normalizedBase = normalizeBaseURL(baseURL)
|
|
5
|
+
const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`
|
|
6
|
+
|
|
7
|
+
if (normalizedBase === '/') return normalizedPath
|
|
8
|
+
if (normalizedPath === normalizedBase.slice(0, -1) || normalizedPath.startsWith(normalizedBase)) return normalizedPath
|
|
9
|
+
|
|
10
|
+
return `${normalizedBase.slice(0, -1)}${normalizedPath}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeBaseURL(baseURL: string): string {
|
|
14
|
+
if (!baseURL || baseURL === '/') return '/'
|
|
15
|
+
|
|
16
|
+
const withLeadingSlash = baseURL.startsWith('/') ? baseURL : `/${baseURL}`
|
|
17
|
+
return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isExternalOrHash(pathname: string): boolean {
|
|
21
|
+
return pathname.startsWith('http://') ||
|
|
22
|
+
pathname.startsWith('https://') ||
|
|
23
|
+
pathname.startsWith('//') ||
|
|
24
|
+
pathname.startsWith('#')
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@life-and-dev/mdsite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Local-first CLI that orchestrates mdsite-nuxt",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"prepublishOnly": "npm test && npm run typecheck && npm run build && npm run verify:package"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"yaml": "^2.
|
|
35
|
+
"yaml": "^2.9.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^
|
|
39
|
-
"typescript": "^
|
|
40
|
-
"vitest": "^4.
|
|
38
|
+
"@types/node": "^26.0.1",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.9"
|
|
41
41
|
}
|
|
42
42
|
}
|