@life-and-dev/mdsite 0.5.3 → 0.7.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 (70) hide show
  1. package/README.md +29 -37
  2. package/dist/commands/clean.d.ts +1 -0
  3. package/dist/commands/clean.js +70 -0
  4. package/dist/commands/clean.js.map +1 -0
  5. package/dist/commands/commands.test.js +157 -75
  6. package/dist/commands/commands.test.js.map +1 -1
  7. package/dist/commands/generate.js +5 -4
  8. package/dist/commands/generate.js.map +1 -1
  9. package/dist/commands/init.js +5 -64
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/prepare.js +2 -14
  12. package/dist/commands/prepare.js.map +1 -1
  13. package/dist/commands/prepare.test.js +26 -24
  14. package/dist/commands/prepare.test.js.map +1 -1
  15. package/dist/commands/preview.js +21 -21
  16. package/dist/commands/preview.js.map +1 -1
  17. package/dist/commands/start.js +13 -11
  18. package/dist/commands/start.js.map +1 -1
  19. package/dist/commands/stop.js +7 -4
  20. package/dist/commands/stop.js.map +1 -1
  21. package/dist/commands/workflows.test.js +42 -56
  22. package/dist/commands/workflows.test.js.map +1 -1
  23. package/dist/config/default-mdsite-config.js +7 -8
  24. package/dist/config/default-mdsite-config.js.map +1 -1
  25. package/dist/config/default-mdsite-config.test.js +7 -8
  26. package/dist/config/default-mdsite-config.test.js.map +1 -1
  27. package/dist/config/mdsite-config.d.ts +46 -10
  28. package/dist/config/mdsite-config.js +46 -24
  29. package/dist/config/mdsite-config.js.map +1 -1
  30. package/dist/config/mdsite-config.test.js +55 -50
  31. package/dist/config/mdsite-config.test.js.map +1 -1
  32. package/dist/index.js +8 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.test.js +13 -0
  35. package/dist/index.test.js.map +1 -1
  36. package/dist/process/child-process.d.ts +4 -0
  37. package/dist/process/child-process.js +33 -1
  38. package/dist/process/child-process.js.map +1 -1
  39. package/dist/process/child-process.test.js +41 -5
  40. package/dist/process/child-process.test.js.map +1 -1
  41. package/dist/process/runtime-state.d.ts +13 -5
  42. package/dist/process/runtime-state.js +25 -13
  43. package/dist/process/runtime-state.js.map +1 -1
  44. package/dist/process/runtime-state.test.js +10 -10
  45. package/dist/process/runtime-state.test.js.map +1 -1
  46. package/dist/renderer/mdsite-nuxt.d.ts +28 -3
  47. package/dist/renderer/mdsite-nuxt.js +32 -27
  48. package/dist/renderer/mdsite-nuxt.js.map +1 -1
  49. package/dist/renderer/mdsite-nuxt.test.js +40 -39
  50. package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
  51. package/mdsite-nuxt/app/components/AppFooter.vue +84 -22
  52. package/mdsite-nuxt/app/composables/useFooter.test.ts +54 -0
  53. package/mdsite-nuxt/app/composables/useFooter.ts +48 -31
  54. package/mdsite-nuxt/app/composables/useSiteConfig.test.ts +13 -87
  55. package/mdsite-nuxt/app/composables/useSiteConfig.ts +7 -26
  56. package/mdsite-nuxt/app/composables/useSourceEdit.test.ts +103 -0
  57. package/mdsite-nuxt/app/composables/useSourceEdit.ts +39 -51
  58. package/mdsite-nuxt/app/layouts/default.vue +10 -3
  59. package/mdsite-nuxt/nuxt.config.ts +22 -15
  60. package/mdsite-nuxt/scripts/generate-favicons.test.ts +3 -3
  61. package/mdsite-nuxt/scripts/generate-favicons.ts +4 -4
  62. package/mdsite-nuxt/scripts/generate-indices.test.ts +71 -10
  63. package/mdsite-nuxt/scripts/generate-indices.ts +161 -27
  64. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -91
  65. package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -50
  66. package/mdsite-nuxt/scripts/start.test.ts +0 -1
  67. package/mdsite-nuxt/scripts/start.ts +0 -1
  68. package/mdsite-nuxt/utils/mdsite-config.ts +86 -41
  69. package/package.json +1 -1
  70. package/mdsite-nuxt/example.config.yml +0 -67
@@ -244,7 +244,7 @@ describe('generated content indices', () => {
244
244
  return JSON.parse(await fs.readFile(path.join(publicDir, '_navigation.json'), 'utf8'))
245
245
  }
246
246
 
247
- it('generates _footer.json with paths and titles from mdsite.yml footer section', async () => {
247
+ it('generates _footer.json with paths and titles from mdsite.yml features.footer section', async () => {
248
248
  await fs.writeFile(path.join(contentDir, 'index.md'), '# Home\n\nWelcome home.', 'utf8')
249
249
  await fs.writeFile(path.join(contentDir, 'about.md'), '# About\n\nAbout us.', 'utf8')
250
250
  await fs.writeFile(path.join(contentDir, 'contacts.md'), '# Contacts\n\nContact us.', 'utf8')
@@ -253,9 +253,10 @@ describe('generated content indices', () => {
253
253
  await fs.writeFile(mdsitePath, [
254
254
  'site:',
255
255
  ' name: Test Site',
256
- 'footer:',
257
- ' - about',
258
- ' - contacts',
256
+ 'features:',
257
+ ' footer:',
258
+ ' - about',
259
+ ' - contacts',
259
260
  '',
260
261
  ].join('\n'), 'utf8')
261
262
  process.env.MDSITE_CONFIG_PATH = mdsitePath
@@ -264,8 +265,37 @@ describe('generated content indices', () => {
264
265
 
265
266
  const footer = await readFooter()
266
267
  expect(footer).toEqual([
267
- { path: '/about', title: 'About' },
268
- { path: '/contacts', title: 'Contacts' },
268
+ { path: '/about', title: 'About', type: 'link', isExternal: false },
269
+ { path: '/contacts', title: 'Contacts', type: 'link', isExternal: false },
270
+ ])
271
+ })
272
+
273
+ it('generates _footer.json with custom labels, external URLs, and separators', async () => {
274
+ await fs.writeFile(path.join(contentDir, 'index.md'), '# Home', 'utf8')
275
+ await fs.writeFile(path.join(contentDir, 'about.md'), '# About', 'utf8')
276
+
277
+ const mdsitePath = path.join(tempDir, 'mdsite.yml')
278
+ await fs.writeFile(mdsitePath, [
279
+ 'site:',
280
+ ' name: Test Site',
281
+ 'features:',
282
+ ' footer:',
283
+ ' - about',
284
+ ' - "About Page": about',
285
+ ' - "GitHub Repo": https://github.com/life-and-dev/mdsite',
286
+ ' - null',
287
+ '',
288
+ ].join('\n'), 'utf8')
289
+ process.env.MDSITE_CONFIG_PATH = mdsitePath
290
+
291
+ await generateFooterJson()
292
+
293
+ const footer = await readFooter()
294
+ expect(footer).toEqual([
295
+ { path: '/about', title: 'About', type: 'link', isExternal: false },
296
+ { path: '/about', title: 'About Page', type: 'link', isExternal: false },
297
+ { path: 'https://github.com/life-and-dev/mdsite', title: 'GitHub Repo', type: 'link', isExternal: true },
298
+ { path: '', title: '', type: 'separator', isExternal: false },
269
299
  ])
270
300
  })
271
301
 
@@ -297,8 +327,9 @@ describe('generated content indices', () => {
297
327
  'menu:',
298
328
  ' - index',
299
329
  ' - guide',
300
- 'footer:',
301
- ' - contacts',
330
+ 'features:',
331
+ ' footer:',
332
+ ' - contacts',
302
333
  '',
303
334
  ].join('\n'), 'utf8')
304
335
  process.env.MDSITE_CONFIG_PATH = mdsitePath
@@ -314,10 +345,39 @@ describe('generated content indices', () => {
314
345
 
315
346
  const footer = await readFooter()
316
347
  expect(footer).toEqual([
317
- { path: '/contacts', title: 'Contacts' },
348
+ { path: '/contacts', title: 'Contacts', type: 'link', isExternal: false },
318
349
  ])
319
350
  })
320
351
 
352
+ it('excludes custom-labelled internal footer entries from the navigation tree', async () => {
353
+ await fs.writeFile(path.join(contentDir, 'index.md'), '# Home', 'utf8')
354
+ await fs.writeFile(path.join(contentDir, 'guide.md'), '# Guide', 'utf8')
355
+ await fs.writeFile(path.join(contentDir, 'about.md'), '# About', 'utf8')
356
+
357
+ const mdsitePath = path.join(tempDir, 'mdsite.yml')
358
+ await fs.writeFile(mdsitePath, [
359
+ 'site:',
360
+ ' name: Test Site',
361
+ 'menu:',
362
+ ' - index',
363
+ ' - guide',
364
+ 'features:',
365
+ ' footer:',
366
+ ' - "About Page": about',
367
+ '',
368
+ ].join('\n'), 'utf8')
369
+ process.env.MDSITE_CONFIG_PATH = mdsitePath
370
+
371
+ await generateNavigationJson()
372
+ await generateFooterJson()
373
+
374
+ const navigation = await readNavigation()
375
+ const paths = navigation.map((n: { path: string }) => n.path)
376
+ expect(paths).toContain('/')
377
+ expect(paths).toContain('/guide')
378
+ expect(paths).not.toContain('/about')
379
+ })
380
+
321
381
  it('excludes footer entries from the fallback navigation tree', async () => {
322
382
  await fs.writeFile(path.join(contentDir, 'index.md'), '# Home\n\nWelcome home.', 'utf8')
323
383
  await fs.writeFile(path.join(contentDir, 'about.md'), '# About\n\nAbout us.', 'utf8')
@@ -327,7 +387,8 @@ describe('generated content indices', () => {
327
387
  await fs.writeFile(mdsitePath, [
328
388
  'site:',
329
389
  ' name: Test Site',
330
- 'footer: [contacts]',
390
+ 'features:',
391
+ ' footer: [contacts]',
331
392
  '',
332
393
  ].join('\n'), 'utf8')
333
394
  delete process.env.MDSITE_CONFIG_PATH
@@ -4,7 +4,7 @@ import fs from 'fs-extra'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
6
  import { parse as parseYaml } from 'yaml'
7
- import type { MdsiteMenuItem } from '../utils/mdsite-config'
7
+ import type { MdsiteFooterItem, MdsiteMenuItem } from '../utils/mdsite-config'
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url)
10
10
  const __dirname = path.dirname(__filename)
@@ -452,25 +452,60 @@ async function loadMenuConfig(sourceDir: string): Promise<MdsiteMenuItem[] | nul
452
452
  // FOOTER LOGIC
453
453
  // ----------------------------------------------------------------------------
454
454
 
455
+ /**
456
+ * Runtime-validated footer entry. `null` is rendered as a vertical separator
457
+ * in the bar; external URLs keep their raw `path` and render in a new tab;
458
+ * internal links use the normalized path with the H1 title (or custom title).
459
+ */
455
460
  export interface FooterLink {
456
461
  path: string
457
462
  title: string
463
+ type: 'link' | 'separator'
464
+ isExternal: boolean
465
+ }
466
+
467
+ /**
468
+ * Same filter the CLI uses, but expressed against `MdsiteFooterItem`. Keeps
469
+ * CLI and renderer parsers in lockstep so both drop malformed entries the
470
+ * same way.
471
+ */
472
+ function isValidFooterItem(item: unknown): item is MdsiteFooterItem {
473
+ if (item === null) return true
474
+ if (typeof item === 'string') return item.trim().length > 0
475
+ if (typeof item === 'object') {
476
+ const keys = Object.keys(item as Record<string, unknown>)
477
+ if (keys.length !== 1) return false
478
+ const value = (item as Record<string, unknown>)[keys[0]]
479
+ return value === null || typeof value === 'string'
480
+ }
481
+ return false
482
+ }
483
+
484
+ /**
485
+ * True when the string is an absolute http(s) URL. Used to keep external
486
+ * links out of the menu-exclusion set (no markdown file to exclude) and to
487
+ * open them in a new tab in the footer.
488
+ */
489
+ function isExternalUrl(value: string): boolean {
490
+ return value.startsWith('http://') || value.startsWith('https://')
458
491
  }
459
492
 
460
493
  /**
461
494
  * Read the footer array from a candidate mdsite.yml config file.
462
495
  * Returns null when the file is missing, unreadable, or has no footer key.
496
+ * Footer lives under `features.footer` in mdsite.yml.
463
497
  */
464
- async function tryReadFooterFromConfig(configPath: string): Promise<string[] | null> {
498
+ async function tryReadFooterFromConfig(configPath: string): Promise<MdsiteFooterItem[] | null> {
465
499
  if (!await fs.pathExists(configPath)) {
466
500
  return null
467
501
  }
468
502
 
469
503
  try {
470
504
  const content = await fs.readFile(configPath, 'utf-8')
471
- const parsed = parseYaml(content) as { footer?: unknown } | null
472
- if (parsed && Array.isArray(parsed.footer) && parsed.footer.length > 0) {
473
- return parsed.footer.filter((item): item is string => typeof item === 'string')
505
+ const parsed = parseYaml(content) as { features?: { footer?: unknown } } | null
506
+ const footerItems = parsed?.features?.footer
507
+ if (parsed && Array.isArray(footerItems) && footerItems.length > 0) {
508
+ return footerItems.filter(isValidFooterItem)
474
509
  }
475
510
  } catch (e) {
476
511
  // Ignore parse errors - they shouldn't abort the whole lookup chain
@@ -486,7 +521,7 @@ async function tryReadFooterFromConfig(configPath: string): Promise<string[] | n
486
521
  * 3. <sourceDir>/mdsite.yml
487
522
  * Returns the first non-empty footer array, or null if none resolve.
488
523
  */
489
- async function loadFooterConfig(sourceDir: string): Promise<string[] | null> {
524
+ async function loadFooterConfig(sourceDir: string): Promise<MdsiteFooterItem[] | null> {
490
525
  const candidates: string[] = []
491
526
  if (process.env.MDSITE_CONFIG_PATH) {
492
527
  candidates.push(process.env.MDSITE_CONFIG_PATH)
@@ -505,10 +540,13 @@ async function loadFooterConfig(sourceDir: string): Promise<string[] | null> {
505
540
  }
506
541
 
507
542
  /**
508
- * Resolve a raw footer entry to its normalized URL path. Returns null when the
509
- * path is empty after resolution.
543
+ * Resolve a raw footer entry to its normalized URL path. Returns null when
544
+ * the path is empty after resolution or when the entry is not a usable string.
510
545
  */
511
546
  function resolveFooterPath(item: string): string | null {
547
+ if (isExternalUrl(item)) {
548
+ return null
549
+ }
512
550
  const resolvedPath = resolvePath(item, '/')
513
551
  if (!resolvedPath || resolvedPath === '/') {
514
552
  return null
@@ -517,34 +555,112 @@ function resolveFooterPath(item: string): string | null {
517
555
  }
518
556
 
519
557
  /**
520
- * Process footer items into a flat list of { path, title } links.
521
- * Title falls back to the raw entry when the markdown file is missing or has no H1.
558
+ * Pull the normalized internal path out of a footer item, if any. Returns
559
+ * null for external URLs and `null`/empty items so they never get added to
560
+ * the menu-exclusion set.
561
+ */
562
+ function extractInternalPath(item: MdsiteFooterItem): string | null {
563
+ if (typeof item === 'string') {
564
+ return resolveFooterPath(item)
565
+ }
566
+ if (item && typeof item === 'object') {
567
+ const keys = Object.keys(item)
568
+ if (keys.length === 1) {
569
+ const value = item[keys[0]]
570
+ if (typeof value === 'string') {
571
+ return resolveFooterPath(value)
572
+ }
573
+ }
574
+ }
575
+ return null
576
+ }
577
+
578
+ /**
579
+ * Process footer items into a flat list of FooterLink entries. Supports:
580
+ * - `null` → separator
581
+ * - string (file name) → internal link, title from H1
582
+ * - `{ title: path }` → internal link with custom title
583
+ * - `{ title: https://... }` → external link, opens in a new tab
584
+ * Title falls back to the raw entry / key when the markdown file is missing
585
+ * or has no H1.
522
586
  */
523
- async function processFooterItems(items: string[]): Promise<FooterLink[]> {
587
+ async function processFooterItems(items: MdsiteFooterItem[]): Promise<FooterLink[]> {
524
588
  const links: FooterLink[] = []
525
589
 
526
590
  for (const item of items) {
527
- const normalizedPath = resolveFooterPath(item)
528
- if (!normalizedPath) {
591
+ if (item === null) {
592
+ links.push({
593
+ path: '',
594
+ title: '',
595
+ type: 'separator',
596
+ isExternal: false
597
+ })
529
598
  continue
530
599
  }
531
600
 
532
- const markdownPath = getMarkdownPath(normalizedPath)
533
- let title: string | null = null
534
-
535
- try {
536
- if (await fs.pathExists(markdownPath)) {
537
- const content = await fs.readFile(markdownPath, 'utf-8')
538
- const metadata = extractMarkdownMetadata(content)
539
- title = metadata.title
601
+ if (typeof item === 'string') {
602
+ const normalizedPath = resolveFooterPath(item)
603
+ if (!normalizedPath) {
604
+ continue
540
605
  }
541
- } catch (e) {
542
- // Ignore missing files
606
+ const title = await readH1Title(normalizedPath) ?? item
607
+ links.push({
608
+ path: normalizedPath,
609
+ title,
610
+ type: 'link',
611
+ isExternal: false
612
+ })
613
+ continue
614
+ }
615
+
616
+ // Object form: { title: path-or-url }
617
+ const keys = Object.keys(item)
618
+ if (keys.length !== 1) continue
619
+ const displayTitle = keys[0]
620
+ const value = item[displayTitle]
621
+
622
+ if (value === null) {
623
+ links.push({
624
+ path: '',
625
+ title: displayTitle,
626
+ type: 'separator',
627
+ isExternal: false
628
+ })
629
+ continue
543
630
  }
544
631
 
632
+ if (typeof value !== 'string') continue
633
+
634
+ if (isExternalUrl(value)) {
635
+ links.push({
636
+ path: value,
637
+ title: displayTitle,
638
+ type: 'link',
639
+ isExternal: true
640
+ })
641
+ continue
642
+ }
643
+
644
+ const normalizedPath = resolveFooterPath(value)
645
+ if (!normalizedPath) {
646
+ // Custom title pointing at a non-existent page — still render it
647
+ // using the raw value as the title so the user sees their label.
648
+ links.push({
649
+ path: value,
650
+ title: displayTitle,
651
+ type: 'link',
652
+ isExternal: false
653
+ })
654
+ continue
655
+ }
656
+
657
+ // Object form always wins for the title, mirroring how the `menu`
658
+ // parser treats custom labels as overrides of the file's H1.
545
659
  links.push({
546
660
  path: normalizedPath,
547
- title: title || item
661
+ title: displayTitle,
662
+ type: 'link',
663
+ isExternal: false
548
664
  })
549
665
  }
550
666
 
@@ -552,8 +668,26 @@ async function processFooterItems(items: string[]): Promise<FooterLink[]> {
552
668
  }
553
669
 
554
670
  /**
555
- * Build the set of normalized footer paths used to exclude entries from the nav tree.
556
- * Returns an empty set when no footer section is configured.
671
+ * Read the H1 from a markdown file at the given normalized path. Returns
672
+ * null when the file is missing or has no H1 heading.
673
+ */
674
+ async function readH1Title(normalizedPath: string): Promise<string | null> {
675
+ const markdownPath = getMarkdownPath(normalizedPath)
676
+ try {
677
+ if (await fs.pathExists(markdownPath)) {
678
+ const content = await fs.readFile(markdownPath, 'utf-8')
679
+ const metadata = extractMarkdownMetadata(content)
680
+ return metadata.title
681
+ }
682
+ } catch (e) {
683
+ // Ignore missing files
684
+ }
685
+ return null
686
+ }
687
+
688
+ /**
689
+ * Build the set of normalized footer paths used to exclude entries from the
690
+ * nav tree. Returns an empty set when no footer section is configured.
557
691
  */
558
692
  async function getFooterExcludedPaths(sourceDir: string): Promise<Set<string>> {
559
693
  const items = await loadFooterConfig(sourceDir)
@@ -563,7 +697,7 @@ async function getFooterExcludedPaths(sourceDir: string): Promise<Set<string>> {
563
697
 
564
698
  const excluded = new Set<string>()
565
699
  for (const item of items) {
566
- const normalizedPath = resolveFooterPath(item)
700
+ const normalizedPath = extractInternalPath(item)
567
701
  if (normalizedPath) {
568
702
  excluded.add(normalizedPath)
569
703
  }
@@ -8,51 +8,31 @@ const {
8
8
  generateFaviconsMock,
9
9
  generateWebManifestMock,
10
10
  loadMdsiteConfigSyncMock,
11
- parseYamlMock,
12
- readFileSyncMock,
13
11
  resolveMdsiteConfigPathMock,
14
12
  rmMock,
15
13
  startWatcherMock,
16
- statSyncMock,
17
- stringifyYamlMock,
18
14
  syncContentMock,
19
- writeFileSyncMock,
20
15
  } = vi.hoisted(() => ({
21
16
  buildContentDataMock: vi.fn(),
22
17
  existsSyncMock: vi.fn(),
23
18
  generateFaviconsMock: vi.fn(),
24
19
  generateWebManifestMock: vi.fn(),
25
20
  loadMdsiteConfigSyncMock: vi.fn(),
26
- parseYamlMock: vi.fn(),
27
- readFileSyncMock: vi.fn(),
28
21
  resolveMdsiteConfigPathMock: vi.fn(),
29
22
  rmMock: vi.fn(),
30
23
  startWatcherMock: vi.fn(),
31
- statSyncMock: vi.fn(),
32
- stringifyYamlMock: vi.fn(),
33
24
  syncContentMock: vi.fn(),
34
- writeFileSyncMock: vi.fn(),
35
25
  }))
36
26
 
37
27
  vi.mock('fs', () => ({
38
28
  default: {
39
29
  existsSync: existsSyncMock,
40
- statSync: statSyncMock,
41
- readFileSync: readFileSyncMock,
42
- writeFileSync: writeFileSyncMock,
43
30
  promises: {
44
31
  rm: rmMock,
45
32
  },
46
33
  },
47
34
  }))
48
35
 
49
- vi.mock('yaml', () => ({
50
- default: {
51
- parse: parseYamlMock,
52
- stringify: stringifyYamlMock,
53
- },
54
- }))
55
-
56
36
  vi.mock('./generate-indices.js', () => ({
57
37
  buildContentData: buildContentDataMock,
58
38
  }))
@@ -105,8 +85,6 @@ describe('renderer hooks orchestration', () => {
105
85
  contentDir: '/renderer/docs',
106
86
  })
107
87
  existsSyncMock.mockImplementation((target: string) => target === '/renderer/docs')
108
- statSyncMock.mockReturnValue({ isFile: () => true })
109
- stringifyYamlMock.mockReturnValue('compat-config')
110
88
  generateFaviconsMock.mockResolvedValue(true)
111
89
  buildContentDataMock.mockResolvedValue(undefined)
112
90
  generateWebManifestMock.mockResolvedValue(undefined)
@@ -146,70 +124,6 @@ describe('renderer hooks orchestration', () => {
146
124
  expect(process.env.MDSITE_CONFIG_PATH).toBe('/renderer/mdsite.yml')
147
125
  })
148
126
 
149
- it('falls back to the legacy compatibility config when no explicit mdsite config resolves', () => {
150
- resolveMdsiteConfigPathMock.mockReturnValue(undefined)
151
- existsSyncMock.mockImplementation((target: string) => (
152
- target === '/renderer/content.config.yml' || target === path.join('/renderer', 'legacy-docs')
153
- ))
154
- parseYamlMock.mockReturnValue({
155
- content: {
156
- path: 'legacy-docs',
157
- },
158
- siteName: 'Legacy Docs',
159
- })
160
- loadMdsiteConfigSyncMock.mockReturnValue({
161
- config: { site: { name: 'Legacy Docs' } },
162
- configPath: '/renderer/.mdsite-compat.yml',
163
- contentDir: path.join('/renderer', 'legacy-docs'),
164
- })
165
-
166
- const runtime = prepareRendererRuntime('/renderer')
167
-
168
- expect(readFileSyncMock).toHaveBeenCalledWith('/renderer/content.config.yml', 'utf8')
169
- expect(writeFileSyncMock).toHaveBeenCalledWith('/renderer/.mdsite-compat.yml', 'compat-config', 'utf8')
170
- expect(loadMdsiteConfigSyncMock).toHaveBeenCalledWith({
171
- configPath: '/renderer/.mdsite-compat.yml',
172
- contentPath: path.join('/renderer', 'legacy-docs'),
173
- })
174
- expect(runtime.configPath).toBe('/renderer/.mdsite-compat.yml')
175
- expect(process.env.MDSITE_CONFIG_PATH).toBe('/renderer/.mdsite-compat.yml')
176
- expect(process.env.NUXT_CONTENT_PATH).toBe(path.join('/renderer', 'legacy-docs'))
177
- })
178
-
179
- it('supports the checked-in legacy renderer config keys', () => {
180
- resolveMdsiteConfigPathMock.mockReturnValue(undefined)
181
- existsSyncMock.mockImplementation((target: string) => (
182
- target === '/renderer/content.config.yml' || target === '/content/docs'
183
- ))
184
- parseYamlMock.mockReturnValue({
185
- contentPath: '/content/docs',
186
- contentGitRepo: 'https://example.test/docs.git',
187
- siteCanonical: 'https://example.test',
188
- siteName: 'Legacy Site',
189
- })
190
- loadMdsiteConfigSyncMock.mockReturnValue({
191
- config: { site: { name: 'Legacy Site' } },
192
- configPath: '/renderer/.mdsite-compat.yml',
193
- contentDir: '/content/docs',
194
- })
195
-
196
- prepareRendererRuntime('/renderer')
197
-
198
- expect(stringifyYamlMock).toHaveBeenCalledWith(expect.objectContaining({
199
- server: expect.objectContaining({
200
- repo: 'https://example.test/docs.git',
201
- }),
202
- site: expect.objectContaining({
203
- canonical: 'https://example.test',
204
- name: 'Legacy Site',
205
- }),
206
- }))
207
- expect(loadMdsiteConfigSyncMock).toHaveBeenCalledWith({
208
- configPath: '/renderer/.mdsite-compat.yml',
209
- contentPath: '/content/docs',
210
- })
211
- })
212
-
213
127
  it('exits when no renderer config can be resolved', () => {
214
128
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
215
129
  const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number) => {
@@ -261,7 +175,6 @@ describe('renderer hooks orchestration', () => {
261
175
  }),
262
176
  }),
263
177
  }))
264
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
265
178
  expect(runtime.contentDir).toBe('/renderer/docs')
266
179
  })
267
180
 
@@ -290,7 +203,6 @@ describe('renderer hooks orchestration', () => {
290
203
  }))
291
204
  expect(syncContentMock.mock.invocationCallOrder[0]).toBeLessThan(buildContentDataMock.mock.invocationCallOrder[0])
292
205
  expect(buildContentDataMock.mock.invocationCallOrder[0]).toBeLessThan(generateFaviconsMock.mock.invocationCallOrder[0])
293
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
294
206
  })
295
207
 
296
208
  it('runs setup mode through the same non-dev orchestration path as build and generate', async () => {
@@ -309,7 +221,6 @@ describe('renderer hooks orchestration', () => {
309
221
  }))
310
222
  expect(startWatcherMock).not.toHaveBeenCalled()
311
223
  expect(rmMock).not.toHaveBeenCalled()
312
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
313
224
  })
314
225
 
315
226
  it('marks orchestration complete for build mode on the same shared non-dev path', async () => {
@@ -319,7 +230,6 @@ describe('renderer hooks orchestration', () => {
319
230
  expect(buildContentDataMock).toHaveBeenCalledTimes(1)
320
231
  expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
321
232
  expect(startWatcherMock).not.toHaveBeenCalled()
322
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
323
233
  })
324
234
 
325
235
  it('runs build fallback hooks without publishing favicon assets when generation reports no output', async () => {
@@ -341,6 +251,5 @@ describe('renderer hooks orchestration', () => {
341
251
  expect(buildContentDataMock).toHaveBeenCalledTimes(1)
342
252
  expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
343
253
  expect(generateWebManifestMock).not.toHaveBeenCalled()
344
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
345
254
  })
346
255
  })
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
- import YAML from 'yaml'
4
3
 
5
4
  import { buildContentData } from './generate-indices.js'
6
5
  import { generateFavicons, generateWebManifest } from './generate-favicons.js'
@@ -18,15 +17,11 @@ export function prepareRendererRuntime(rootDir: string, options: {
18
17
  configPath?: string
19
18
  contentPath?: string
20
19
  } = {}): RendererRuntime {
21
- let configPath = resolveMdsiteConfigPath({
20
+ const configPath = resolveMdsiteConfigPath({
22
21
  configPath: options.configPath,
23
22
  contentPath: options.contentPath ?? process.env.NUXT_CONTENT_PATH
24
23
  })
25
24
 
26
- if (!configPath) {
27
- configPath = ensureLegacyCompatibilityConfig(rootDir)
28
- }
29
-
30
25
  if (!configPath) {
31
26
  console.error('❌ No mdsite.yml configuration found. Set MDSITE_CONFIG_PATH or pass a mdsite.yml path.')
32
27
  process.exit(1)
@@ -70,7 +65,6 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
70
65
  await fs.promises.rm(path.join(rootDir, '.data'), { recursive: true, force: true })
71
66
  }
72
67
  await generateDevManifestAssets(runtime.config)
73
- process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
74
68
  await startWatcher()
75
69
  return runtime
76
70
  }
@@ -79,7 +73,6 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
79
73
  console.log(`\n🔨 Generating navigation and search index...`)
80
74
  await buildContentData()
81
75
  await generateFaviconAssets(runtime.config)
82
- process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
83
76
 
84
77
  return runtime
85
78
  }
@@ -103,45 +96,3 @@ async function generateFaviconAssets(config: MdsiteConfig): Promise<void> {
103
96
  async function generateDevManifestAssets(config: MdsiteConfig): Promise<void> {
104
97
  await generateWebManifest({ name: config.site.name, themes: config.themes })
105
98
  }
106
-
107
- function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
108
- const legacyConfigPath = path.join(rootDir, 'content.config.yml')
109
-
110
- if (!fs.existsSync(legacyConfigPath) || !fs.statSync(legacyConfigPath).isFile()) {
111
- return undefined
112
- }
113
-
114
- const legacyConfig = YAML.parse(fs.readFileSync(legacyConfigPath, 'utf8')) ?? {}
115
- const legacyContentPath = legacyConfig.content?.path || legacyConfig.content?.git?.path || legacyConfig.contentPath || 'docs'
116
- const contentDir = path.resolve(rootDir, legacyContentPath)
117
- const compatibilityConfigPath = path.join(rootDir, '.mdsite-compat.yml')
118
- const compatibilityConfig = {
119
- favicon: '',
120
- features: {
121
- bibleTooltips: legacyConfig.features?.bibleTooltips ?? true,
122
- sourceEdit: legacyConfig.features?.sourceEdit ?? true
123
- },
124
- menu: [],
125
- footer: [],
126
- server: {
127
- output: '.output',
128
- path: '.mdsite',
129
- repo: legacyConfig.content?.git?.repo || legacyConfig.contentGitRepo || '',
130
- gitBranch: legacyConfig.server?.['git-branch'] || 'main'
131
- },
132
- site: {
133
- canonical: legacyConfig.site?.canonical || legacyConfig.siteCanonical || '',
134
- name: legacyConfig.site?.name || legacyConfig.siteName || path.basename(contentDir) || 'Site'
135
- },
136
- themes: legacyConfig.themes || {}
137
- }
138
-
139
- fs.writeFileSync(compatibilityConfigPath, YAML.stringify(compatibilityConfig), 'utf8')
140
- process.env.NUXT_CONTENT_PATH = contentDir
141
- process.env.CONTENT_DIR = contentDir
142
- process.env.MDSITE_CONFIG_PATH = compatibilityConfigPath
143
-
144
- console.warn(`⚠️ Using legacy compatibility fallback from ${legacyConfigPath}`)
145
-
146
- return compatibilityConfigPath
147
- }
@@ -109,7 +109,6 @@ describe('renderer start wrapper', () => {
109
109
  configPath: 'config/site.yml',
110
110
  })
111
111
  expect(runSetupHooksMock).not.toHaveBeenCalled()
112
- expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
113
112
  expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'preview'], {
114
113
  cwd: rootDir,
115
114
  env: process.env,
@@ -39,7 +39,6 @@ if (nuxtCommand.startsWith('dev')) {
39
39
  await runSetupHooks(nuxtCommand, rootDir, { configPath: configArg })
40
40
  } else {
41
41
  prepareRendererRuntime(rootDir, { configPath: configArg })
42
- process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
43
42
  }
44
43
 
45
44
  console.log(`✨ Starting Nuxt ${nuxtCommand.toUpperCase()}...`)