@life-and-dev/mdsite 0.4.0 → 0.5.1
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 +4 -0
- package/dist/commands/commands.test.js +1 -0
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/workflows.test.js +1 -0
- package/dist/commands/workflows.test.js.map +1 -1
- package/dist/config/default-mdsite-config.js +1 -0
- package/dist/config/default-mdsite-config.js.map +1 -1
- package/dist/config/default-mdsite-config.test.js +1 -0
- package/dist/config/default-mdsite-config.test.js.map +1 -1
- package/dist/config/mdsite-config.d.ts +1 -0
- package/dist/config/mdsite-config.js +1 -0
- package/dist/config/mdsite-config.js.map +1 -1
- package/dist/config/mdsite-config.test.js +17 -0
- package/dist/config/mdsite-config.test.js.map +1 -1
- package/dist/process/runtime-state.test.js +1 -0
- package/dist/process/runtime-state.test.js.map +1 -1
- package/dist/renderer/mdsite-nuxt.test.js +1 -0
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
- package/mdsite-nuxt/app/components/AppBar.vue +4 -2
- package/mdsite-nuxt/app/components/AppFooter.vue +31 -35
- package/mdsite-nuxt/app/components/AppTableOfContents.vue +11 -17
- package/mdsite-nuxt/app/composables/useFooter.ts +46 -0
- package/mdsite-nuxt/app/composables/useNavigationTree.test.ts +78 -0
- package/mdsite-nuxt/app/composables/useNavigationTree.ts +36 -0
- package/mdsite-nuxt/app/composables/useTableOfContents.test.ts +35 -0
- package/mdsite-nuxt/app/composables/useTableOfContents.ts +32 -4
- package/mdsite-nuxt/app/layouts/default.vue +15 -0
- package/mdsite-nuxt/nuxt.config.ts +19 -1
- package/mdsite-nuxt/scripts/generate-indices.test.ts +108 -1
- package/mdsite-nuxt/scripts/generate-indices.ts +181 -8
- package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -0
- package/mdsite-nuxt/scripts/sync-content.ts +31 -0
- package/mdsite-nuxt/utils/mdsite-config.ts +28 -1
- package/package.json +1 -1
|
@@ -4,6 +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
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
10
|
const __dirname = path.dirname(__filename)
|
|
@@ -108,8 +109,10 @@ export interface MinimalTreeNode {
|
|
|
108
109
|
isPrimary?: boolean
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// `MdsiteMenuItem` is imported from `~/utils/mdsite-config` (see top of file).
|
|
113
|
+
// It is the same recursive shape that was previously defined locally as
|
|
114
|
+
// `MdsiteMenuItem`. Single source of truth lives in `utils/mdsite-config.ts`
|
|
115
|
+
// so the Nuxt runtime-config type generator can infer it correctly.
|
|
113
116
|
/**
|
|
114
117
|
* Normalize URL path so a trailing /index resolves to its parent.
|
|
115
118
|
* Mirrors filePathToUrlPath behavior so menu paths match content routes.
|
|
@@ -161,7 +164,7 @@ function resolvePath(menuPath: string, contextPath: string): string {
|
|
|
161
164
|
* Process menu items recursively and build minimal tree structure
|
|
162
165
|
*/
|
|
163
166
|
async function processMenuItems(
|
|
164
|
-
items:
|
|
167
|
+
items: MdsiteMenuItem[],
|
|
165
168
|
contextPath: string,
|
|
166
169
|
order: number = 0
|
|
167
170
|
): Promise<{ nodes: MinimalTreeNode[], nextOrder: number }> {
|
|
@@ -371,14 +374,14 @@ async function buildFallbackNavigationTree(sourceDir: string): Promise<MinimalTr
|
|
|
371
374
|
* Load the menu array from a candidate config file (legacy _menu.yml/yaml or mdsite.yml).
|
|
372
375
|
* Returns null if the file is missing, unreadable, has no menu key, or has an empty menu.
|
|
373
376
|
*/
|
|
374
|
-
async function tryReadMenuFromConfig(configPath: string): Promise<
|
|
377
|
+
async function tryReadMenuFromConfig(configPath: string): Promise<MdsiteMenuItem[] | null> {
|
|
375
378
|
if (!await fs.pathExists(configPath)) {
|
|
376
379
|
return null
|
|
377
380
|
}
|
|
378
381
|
|
|
379
382
|
try {
|
|
380
383
|
const content = await fs.readFile(configPath, 'utf-8')
|
|
381
|
-
const parsed = parseYaml(content) as { menu?:
|
|
384
|
+
const parsed = parseYaml(content) as { menu?: MdsiteMenuItem[] } | null
|
|
382
385
|
if (parsed && Array.isArray(parsed.menu) && parsed.menu.length > 0) {
|
|
383
386
|
return parsed.menu
|
|
384
387
|
}
|
|
@@ -393,14 +396,14 @@ async function tryReadMenuFromConfig(configPath: string): Promise<MenuItemType[]
|
|
|
393
396
|
* Try to read menu items from a plain (non-wrapped) legacy _menu.yml/yaml file.
|
|
394
397
|
* Returns null if the file is missing, unreadable, or doesn't contain a non-empty array.
|
|
395
398
|
*/
|
|
396
|
-
async function tryReadLegacyMenuFile(menuPath: string): Promise<
|
|
399
|
+
async function tryReadLegacyMenuFile(menuPath: string): Promise<MdsiteMenuItem[] | null> {
|
|
397
400
|
if (!await fs.pathExists(menuPath)) {
|
|
398
401
|
return null
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
try {
|
|
402
405
|
const content = await fs.readFile(menuPath, 'utf-8')
|
|
403
|
-
const parsed = parseYaml(content) as
|
|
406
|
+
const parsed = parseYaml(content) as MdsiteMenuItem[] | null
|
|
404
407
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
405
408
|
return parsed
|
|
406
409
|
}
|
|
@@ -420,7 +423,7 @@ async function tryReadLegacyMenuFile(menuPath: string): Promise<MenuItemType[] |
|
|
|
420
423
|
* 5. <sourceDir>/mdsite.yml
|
|
421
424
|
* Returns the first non-empty menu array, or null if none resolve to a menu.
|
|
422
425
|
*/
|
|
423
|
-
async function loadMenuConfig(sourceDir: string): Promise<
|
|
426
|
+
async function loadMenuConfig(sourceDir: string): Promise<MdsiteMenuItem[] | null> {
|
|
424
427
|
const candidates: { path: string, isLegacy: boolean }[] = [
|
|
425
428
|
{ path: path.join(sourceDir, '_menu.yml'), isLegacy: true },
|
|
426
429
|
{ path: path.join(sourceDir, '_menu.yaml'), isLegacy: true }
|
|
@@ -445,6 +448,166 @@ async function loadMenuConfig(sourceDir: string): Promise<MenuItemType[] | null>
|
|
|
445
448
|
return null
|
|
446
449
|
}
|
|
447
450
|
|
|
451
|
+
// ----------------------------------------------------------------------------
|
|
452
|
+
// FOOTER LOGIC
|
|
453
|
+
// ----------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
export interface FooterLink {
|
|
456
|
+
path: string
|
|
457
|
+
title: string
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Read the footer array from a candidate mdsite.yml config file.
|
|
462
|
+
* Returns null when the file is missing, unreadable, or has no footer key.
|
|
463
|
+
*/
|
|
464
|
+
async function tryReadFooterFromConfig(configPath: string): Promise<string[] | null> {
|
|
465
|
+
if (!await fs.pathExists(configPath)) {
|
|
466
|
+
return null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
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')
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// Ignore parse errors - they shouldn't abort the whole lookup chain
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return null
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Layered footer lookup. Tries, in order:
|
|
484
|
+
* 1. MDSITE_CONFIG_PATH env var
|
|
485
|
+
* 2. <sourceDir>/../mdsite.yml
|
|
486
|
+
* 3. <sourceDir>/mdsite.yml
|
|
487
|
+
* Returns the first non-empty footer array, or null if none resolve.
|
|
488
|
+
*/
|
|
489
|
+
async function loadFooterConfig(sourceDir: string): Promise<string[] | null> {
|
|
490
|
+
const candidates: string[] = []
|
|
491
|
+
if (process.env.MDSITE_CONFIG_PATH) {
|
|
492
|
+
candidates.push(process.env.MDSITE_CONFIG_PATH)
|
|
493
|
+
}
|
|
494
|
+
candidates.push(path.join(sourceDir, '..', 'mdsite.yml'))
|
|
495
|
+
candidates.push(path.join(sourceDir, 'mdsite.yml'))
|
|
496
|
+
|
|
497
|
+
for (const candidate of candidates) {
|
|
498
|
+
const footer = await tryReadFooterFromConfig(candidate)
|
|
499
|
+
if (footer) {
|
|
500
|
+
return footer
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Resolve a raw footer entry to its normalized URL path. Returns null when the
|
|
509
|
+
* path is empty after resolution.
|
|
510
|
+
*/
|
|
511
|
+
function resolveFooterPath(item: string): string | null {
|
|
512
|
+
const resolvedPath = resolvePath(item, '/')
|
|
513
|
+
if (!resolvedPath || resolvedPath === '/') {
|
|
514
|
+
return null
|
|
515
|
+
}
|
|
516
|
+
return normalizeIndexPath(resolvedPath)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
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.
|
|
522
|
+
*/
|
|
523
|
+
async function processFooterItems(items: string[]): Promise<FooterLink[]> {
|
|
524
|
+
const links: FooterLink[] = []
|
|
525
|
+
|
|
526
|
+
for (const item of items) {
|
|
527
|
+
const normalizedPath = resolveFooterPath(item)
|
|
528
|
+
if (!normalizedPath) {
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
|
|
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
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
// Ignore missing files
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
links.push({
|
|
546
|
+
path: normalizedPath,
|
|
547
|
+
title: title || item
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return links
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
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.
|
|
557
|
+
*/
|
|
558
|
+
async function getFooterExcludedPaths(sourceDir: string): Promise<Set<string>> {
|
|
559
|
+
const items = await loadFooterConfig(sourceDir)
|
|
560
|
+
if (!items) {
|
|
561
|
+
return new Set()
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const excluded = new Set<string>()
|
|
565
|
+
for (const item of items) {
|
|
566
|
+
const normalizedPath = resolveFooterPath(item)
|
|
567
|
+
if (normalizedPath) {
|
|
568
|
+
excluded.add(normalizedPath)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return excluded
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Recursively drop any node whose path matches the excluded set. Subtree children
|
|
576
|
+
* of a dropped node are removed along with the parent.
|
|
577
|
+
*/
|
|
578
|
+
function filterTreeByExcludedPaths(nodes: MinimalTreeNode[], excluded: Set<string>): MinimalTreeNode[] {
|
|
579
|
+
return nodes
|
|
580
|
+
.filter(node => !excluded.has(node.path))
|
|
581
|
+
.map(node => ({
|
|
582
|
+
...node,
|
|
583
|
+
children: node.children ? filterTreeByExcludedPaths(node.children, excluded) : undefined
|
|
584
|
+
}))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate footer links JSON file
|
|
589
|
+
*/
|
|
590
|
+
export async function generateFooterJson() {
|
|
591
|
+
const domain = getContentDomain()
|
|
592
|
+
console.log(`📎 Building footer links for: ${domain}`)
|
|
593
|
+
|
|
594
|
+
const sourceDir = getSourceDir()
|
|
595
|
+
const items = await loadFooterConfig(sourceDir)
|
|
596
|
+
const links = items ? await processFooterItems(items) : []
|
|
597
|
+
|
|
598
|
+
const targetDir = getTargetDir()
|
|
599
|
+
const outputPath = path.join(targetDir, '_footer.json')
|
|
600
|
+
|
|
601
|
+
await fs.ensureDir(targetDir)
|
|
602
|
+
await fs.writeJson(outputPath, links, { spaces: 2 })
|
|
603
|
+
|
|
604
|
+
const fileSize = (await fs.stat(outputPath)).size
|
|
605
|
+
const fileSizeKB = (fileSize / 1024).toFixed(2)
|
|
606
|
+
|
|
607
|
+
console.log(`✓ Footer links generated: ${outputPath} (${fileSizeKB} KB)`)
|
|
608
|
+
console.log(`✓ Total footer links: ${links.length}\n`)
|
|
609
|
+
}
|
|
610
|
+
|
|
448
611
|
/**
|
|
449
612
|
* Generate navigation JSON file
|
|
450
613
|
*/
|
|
@@ -466,8 +629,17 @@ export async function generateNavigationJson() {
|
|
|
466
629
|
console.error('Error building navigation tree:', error)
|
|
467
630
|
}
|
|
468
631
|
|
|
632
|
+
// Deduplicate footer entries from the menu tree (if any footer section is configured)
|
|
633
|
+
const excludedPaths = await getFooterExcludedPaths(sourceDir)
|
|
634
|
+
if (excludedPaths.size > 0) {
|
|
635
|
+
tree = filterTreeByExcludedPaths(tree, excludedPaths)
|
|
636
|
+
}
|
|
637
|
+
|
|
469
638
|
if (tree.length === 0) {
|
|
470
639
|
tree = await buildFallbackNavigationTree(sourceDir)
|
|
640
|
+
if (excludedPaths.size > 0) {
|
|
641
|
+
tree = filterTreeByExcludedPaths(tree, excludedPaths)
|
|
642
|
+
}
|
|
471
643
|
}
|
|
472
644
|
|
|
473
645
|
const targetDir = getTargetDir()
|
|
@@ -614,6 +786,7 @@ export async function generateSearchIndexJson() {
|
|
|
614
786
|
export async function buildContentData() {
|
|
615
787
|
await generateNavigationJson()
|
|
616
788
|
await generateSearchIndexJson()
|
|
789
|
+
await generateFooterJson()
|
|
617
790
|
}
|
|
618
791
|
|
|
619
792
|
// Run if called directly
|
|
@@ -22,6 +22,7 @@ const STATIC_FILES = [
|
|
|
22
22
|
// Debounce timers for JSON regeneration (5 second delay)
|
|
23
23
|
let navigationDebounceTimer: NodeJS.Timeout | null = null
|
|
24
24
|
let searchDebounceTimer: NodeJS.Timeout | null = null
|
|
25
|
+
let footerDebounceTimer: NodeJS.Timeout | null = null
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Get content domain from environment variable (read at runtime, not import time)
|
|
@@ -87,6 +88,26 @@ function regenerateSearchIndex() {
|
|
|
87
88
|
}, 5000)
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Regenerate footer links JSON with debouncing (5 second delay)
|
|
93
|
+
*/
|
|
94
|
+
function regenerateFooter() {
|
|
95
|
+
if (footerDebounceTimer) {
|
|
96
|
+
clearTimeout(footerDebounceTimer)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
footerDebounceTimer = setTimeout(async () => {
|
|
100
|
+
console.log('🔄 Regenerating footer links...')
|
|
101
|
+
try {
|
|
102
|
+
const { generateFooterJson } = await import('./generate-indices.js')
|
|
103
|
+
await generateFooterJson()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('❌ Failed to regenerate footer links:', error)
|
|
106
|
+
}
|
|
107
|
+
footerDebounceTimer = null
|
|
108
|
+
}, 5000)
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
/**
|
|
91
112
|
* Generate navigation and search JSON files (one-time on startup)
|
|
92
113
|
*/
|
|
@@ -106,6 +127,13 @@ export async function generateJsonFiles() {
|
|
|
106
127
|
} catch (error) {
|
|
107
128
|
console.error('❌ Failed to generate search index:', error)
|
|
108
129
|
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const { generateFooterJson } = await import('./generate-indices.js')
|
|
133
|
+
await generateFooterJson()
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('❌ Failed to generate footer links:', error)
|
|
136
|
+
}
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
/**
|
|
@@ -198,6 +226,7 @@ export async function startWatcher() {
|
|
|
198
226
|
console.log(`📝 Markdown added: ${fileName}`)
|
|
199
227
|
regenerateNavigation()
|
|
200
228
|
regenerateSearchIndex()
|
|
229
|
+
regenerateFooter()
|
|
201
230
|
} else {
|
|
202
231
|
copyImage(filePath, true, 'added')
|
|
203
232
|
}
|
|
@@ -209,6 +238,7 @@ export async function startWatcher() {
|
|
|
209
238
|
console.log(`📝 Markdown updated: ${fileName}`)
|
|
210
239
|
regenerateNavigation()
|
|
211
240
|
regenerateSearchIndex()
|
|
241
|
+
regenerateFooter()
|
|
212
242
|
} else {
|
|
213
243
|
copyImage(filePath, true, 'updated')
|
|
214
244
|
}
|
|
@@ -220,6 +250,7 @@ export async function startWatcher() {
|
|
|
220
250
|
console.log(`📝 Markdown deleted: ${fileName}`)
|
|
221
251
|
regenerateNavigation()
|
|
222
252
|
regenerateSearchIndex()
|
|
253
|
+
regenerateFooter()
|
|
223
254
|
} else {
|
|
224
255
|
deleteImage(filePath)
|
|
225
256
|
}
|
|
@@ -2,6 +2,30 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Recursive shape of a single `mdsite.yml` `menu:` entry.
|
|
7
|
+
*
|
|
8
|
+
* - `string` → flat link to a markdown slug
|
|
9
|
+
* (e.g. `- genesis`, `- resurrections`)
|
|
10
|
+
* - `null` → visual separator (`===` in YAML)
|
|
11
|
+
* - `Record<string, MdsiteMenuValue>` → group: object whose keys are group
|
|
12
|
+
* labels (e.g. `"Members of the Trinity":`) and whose
|
|
13
|
+
* values are `MdsiteMenuValue` items
|
|
14
|
+
* - `MdsiteMenuValue` (the value side of a group) can be:
|
|
15
|
+
* - `string` → alias link with custom title
|
|
16
|
+
* (e.g. `"Job": https://...`, `"Homepage": index`)
|
|
17
|
+
* - `null` → group label only (renders as a heading, no link)
|
|
18
|
+
* - `MdsiteMenuItem[]` → nested submenu (recursive)
|
|
19
|
+
*
|
|
20
|
+
* Keeping this a `type` alias (not an `interface`) and avoiding `any` lets
|
|
21
|
+
* Nuxt's runtime-config type generator infer `runtimeConfig.public.siteConfig`
|
|
22
|
+
* without collapsing `menu` to `{}[]`. See `nuxt.config.ts` for the cast
|
|
23
|
+
* history this replaces.
|
|
24
|
+
*/
|
|
25
|
+
export type MdsiteMenuItem = string | null | MdsiteMenuGroup
|
|
26
|
+
export type MdsiteMenuGroup = { [key: string]: MdsiteMenuValue }
|
|
27
|
+
export type MdsiteMenuValue = string | null | MdsiteMenuItem[]
|
|
28
|
+
|
|
5
29
|
export interface MdsiteConfig {
|
|
6
30
|
content?: {
|
|
7
31
|
path?: string
|
|
@@ -11,7 +35,8 @@ export interface MdsiteConfig {
|
|
|
11
35
|
bibleTooltips: boolean
|
|
12
36
|
sourceEdit: boolean
|
|
13
37
|
}
|
|
14
|
-
menu:
|
|
38
|
+
menu: MdsiteMenuItem[]
|
|
39
|
+
footer: string[]
|
|
15
40
|
server: {
|
|
16
41
|
output: string
|
|
17
42
|
path: string
|
|
@@ -201,6 +226,7 @@ function createDefaultMdsiteConfig(siteName: string): MdsiteConfig {
|
|
|
201
226
|
sourceEdit: true
|
|
202
227
|
},
|
|
203
228
|
menu: [],
|
|
229
|
+
footer: [],
|
|
204
230
|
server: {
|
|
205
231
|
output: '.output',
|
|
206
232
|
path: '.mdsite',
|
|
@@ -233,6 +259,7 @@ function normalizeMdsiteConfig(rawConfig: Record<string, any>, contentDir: strin
|
|
|
233
259
|
},
|
|
234
260
|
content: contentPath ? { path: contentPath } : fallbackConfig.content,
|
|
235
261
|
menu: Array.isArray(rawConfig.menu) ? rawConfig.menu : fallbackConfig.menu,
|
|
262
|
+
footer: Array.isArray(rawConfig.footer) ? rawConfig.footer.filter((item): item is string => typeof item === 'string') : [],
|
|
236
263
|
server: {
|
|
237
264
|
output: typeof rawConfig.server?.output === 'string' ? rawConfig.server.output : fallbackConfig.server.output,
|
|
238
265
|
path: typeof rawConfig.server?.path === 'string' ? rawConfig.server.path : fallbackConfig.server.path,
|