@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.
@@ -23,28 +23,28 @@
23
23
  "test": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@nuxt/content": "^3.8.0",
27
- "@types/node": "^24.10.0",
28
- "better-sqlite3": "^12.4.1",
29
- "fs-extra": "^11.3.2",
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.12.2",
32
- "nuxt": "^4.2.1",
33
- "typescript": "^5.9.3",
34
- "vue": "^3.5.24",
35
- "vue-router": "^4.6.3",
36
- "vue-tsc": "^3.1.3",
37
- "vuetify-nuxt-module": "^0.19.2",
38
- "yaml": "^2.8.1"
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.56.1",
41
+ "@playwright/test": "^1.61.1",
42
42
  "@types/fs-extra": "^11.0.4",
43
- "@vitest/ui": "^4.0.8",
44
- "playwright": "^1.56.1",
45
- "remark-github-blockquote-alert": "^2.0.1",
46
- "sharp": "^0.34.5",
47
- "tsx": "^4.20.6",
48
- "vitest": "^4.0.8"
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: '/icon-192.png',
146
+ src: 'icon-192.png',
147
147
  sizes: '192x192',
148
148
  type: 'image/png'
149
149
  },
150
150
  {
151
- src: '/icon-512.png',
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
- const result = await processMenuItems(menuItems, '/')
348
- tree = result.nodes
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 contentDir = path.resolve(rootDir, legacyConfig.content?.path || legacyConfig.content?.git?.path || 'docs')
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.8",
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.8.1"
35
+ "yaml": "^2.9.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^24.10.0",
39
- "typescript": "^5.9.3",
40
- "vitest": "^4.0.8"
38
+ "@types/node": "^26.0.1",
39
+ "typescript": "^6.0.3",
40
+ "vitest": "^4.1.9"
41
41
  }
42
42
  }