@life-and-dev/mdsite 0.7.0 → 0.7.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.
|
@@ -3,6 +3,26 @@ import { loadMdsiteConfigSync } from './utils/mdsite-config.js'
|
|
|
3
3
|
|
|
4
4
|
const { contentDir } = loadMdsiteConfigSync()
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Build/dependency directories that should never be crawled as content.
|
|
8
|
+
*
|
|
9
|
+
* @nuxt/content v3 does not exclude `node_modules` or hidden directories
|
|
10
|
+
* by default (it passes `dot: true` to all glob/match calls), so a
|
|
11
|
+
* content directory that happens to be the project root (i.e. `mdsite.yml`
|
|
12
|
+
* lives at the repo root and `paths.input` is unset) would otherwise
|
|
13
|
+
* walk into the renderer working dir (`.mdsite/`), its `node_modules`,
|
|
14
|
+
* and other build artifacts.
|
|
15
|
+
*
|
|
16
|
+
* The rule mirrors `isExcludedSourceDir` in `scripts/generate-indices.ts`
|
|
17
|
+
* and `scripts/sync-content.ts`: any hidden directory (name starts with
|
|
18
|
+
* `.`) plus `node_modules` and `dist`. Keep the three lists in sync.
|
|
19
|
+
*/
|
|
20
|
+
const excludedSourcePatterns: readonly string[] = [
|
|
21
|
+
'**/node_modules/**',
|
|
22
|
+
'**/dist/**',
|
|
23
|
+
'**/.*/**'
|
|
24
|
+
]
|
|
25
|
+
|
|
6
26
|
export default defineContentConfig({
|
|
7
27
|
collections: {
|
|
8
28
|
content: defineCollection({
|
|
@@ -10,7 +30,7 @@ export default defineContentConfig({
|
|
|
10
30
|
source: {
|
|
11
31
|
cwd: contentDir,
|
|
12
32
|
include: '**/*.md',
|
|
13
|
-
exclude: ['**/*.draft.md'],
|
|
33
|
+
exclude: [...excludedSourcePatterns, '**/*.draft.md'],
|
|
14
34
|
prefix: '/'
|
|
15
35
|
}
|
|
16
36
|
})
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
6
|
|
|
7
7
|
import { generateNavigationJson, generateSearchIndexJson, generateFooterJson } from './generate-indices.js'
|
|
8
8
|
|
|
@@ -402,4 +402,153 @@ describe('generated content indices', () => {
|
|
|
402
402
|
expect(paths).not.toContain('/contacts')
|
|
403
403
|
})
|
|
404
404
|
})
|
|
405
|
+
|
|
406
|
+
describe('build/dependency directory exclusion', () => {
|
|
407
|
+
async function readSearchIndex() {
|
|
408
|
+
return JSON.parse(await fs.readFile(path.join(publicDir, '_search-index.json'), 'utf8'))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function readNavigation() {
|
|
412
|
+
return JSON.parse(await fs.readFile(path.join(publicDir, '_navigation.json'), 'utf8'))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Build a fake content tree that mimics a project root used as the
|
|
417
|
+
* content directory: real user pages alongside build artifacts, hidden
|
|
418
|
+
* editor dirs, and a top-level `node_modules`/`dist`. Returns the set
|
|
419
|
+
* of "expected to be indexed" markdown paths.
|
|
420
|
+
*/
|
|
421
|
+
async function plantMixedTree() {
|
|
422
|
+
// User-authored content at the content root
|
|
423
|
+
await fs.writeFile(path.join(contentDir, 'index.md'), '# Home\n\nWelcome home.', 'utf8')
|
|
424
|
+
await fs.writeFile(path.join(contentDir, 'guide.md'), '# Guide\n\nUseful guide.', 'utf8')
|
|
425
|
+
await fs.mkdir(path.join(contentDir, 'features'), { recursive: true })
|
|
426
|
+
await fs.writeFile(path.join(contentDir, 'features', 'theme.md'), '# Theme\n\nContent.', 'utf8')
|
|
427
|
+
|
|
428
|
+
// Renderer working dir with nested node_modules containing a README
|
|
429
|
+
// — mimics the layout the CLI materializes on first run.
|
|
430
|
+
await fs.mkdir(path.join(contentDir, '.mdsite', 'node_modules', 'some-pkg'), { recursive: true })
|
|
431
|
+
await fs.writeFile(
|
|
432
|
+
path.join(contentDir, '.mdsite', 'node_modules', 'some-pkg', 'README.md'),
|
|
433
|
+
'# some-pkg\n\nThis is a dependency README and should be ignored.'
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
// A top-level node_modules dir (e.g. when the content root IS the
|
|
437
|
+
// project root) should also be skipped.
|
|
438
|
+
await fs.mkdir(path.join(contentDir, 'node_modules', 'transitive'), { recursive: true })
|
|
439
|
+
await fs.writeFile(
|
|
440
|
+
path.join(contentDir, 'node_modules', 'transitive', 'README.md'),
|
|
441
|
+
'# transitive\n\nShould also be ignored.'
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// A `dist` directory (top-level build output) should also be skipped.
|
|
445
|
+
await fs.mkdir(path.join(contentDir, 'dist', 'legacy'), { recursive: true })
|
|
446
|
+
await fs.writeFile(
|
|
447
|
+
path.join(contentDir, 'dist', 'legacy', 'stale.md'),
|
|
448
|
+
'# Stale\n\nShould be ignored.'
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
// Any hidden directory at any depth should be skipped — editor state,
|
|
452
|
+
// tooling caches, etc. `.vscode`, `.idea`, `.history`, `.cache` are
|
|
453
|
+
// all common offenders when the content root is the project root.
|
|
454
|
+
for (const hidden of ['.vscode', '.idea', '.history', '.cache']) {
|
|
455
|
+
await fs.mkdir(path.join(contentDir, hidden, 'notes'), { recursive: true })
|
|
456
|
+
await fs.writeFile(
|
|
457
|
+
path.join(contentDir, hidden, 'notes', 'scratch.md'),
|
|
458
|
+
`# ${hidden}\n\nShould be ignored.`
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Nuxt/Nitro/CMS output dirs that may sit next to the content.
|
|
463
|
+
for (const hidden of ['.nuxt', '.output', '.nitro', '.data', '.git']) {
|
|
464
|
+
await fs.mkdir(path.join(contentDir, hidden), { recursive: true })
|
|
465
|
+
await fs.writeFile(
|
|
466
|
+
path.join(contentDir, hidden, 'scratch.md'),
|
|
467
|
+
`# ${hidden}\n\nShould be ignored.`
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
it('does not index .md files under any hidden directory, node_modules, or dist', async () => {
|
|
473
|
+
await plantMixedTree()
|
|
474
|
+
|
|
475
|
+
// console.warn would log a "No H1 found" notice for any file the
|
|
476
|
+
// walker still visited — silence it so the test output stays clean.
|
|
477
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
await generateSearchIndexJson()
|
|
481
|
+
} finally {
|
|
482
|
+
warnSpy.mockRestore()
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const searchIndex = await readSearchIndex()
|
|
486
|
+
const paths = searchIndex.map((entry: { path: string }) => entry.path)
|
|
487
|
+
// User-authored pages are still indexed
|
|
488
|
+
expect(paths).toContain('/')
|
|
489
|
+
expect(paths).toContain('/guide')
|
|
490
|
+
expect(paths).toContain('/features/theme')
|
|
491
|
+
// No entry leaks from a build/dependency or hidden directory
|
|
492
|
+
for (const excluded of [
|
|
493
|
+
'.mdsite',
|
|
494
|
+
'node_modules',
|
|
495
|
+
'dist',
|
|
496
|
+
'.nuxt',
|
|
497
|
+
'.output',
|
|
498
|
+
'.nitro',
|
|
499
|
+
'.data',
|
|
500
|
+
'.git',
|
|
501
|
+
'.vscode',
|
|
502
|
+
'.idea',
|
|
503
|
+
'.history',
|
|
504
|
+
'.cache'
|
|
505
|
+
]) {
|
|
506
|
+
expect(paths.some((p: string) => p.split('/').includes(excluded))).toBe(false)
|
|
507
|
+
}
|
|
508
|
+
const noH1Warnings = warnSpy.mock.calls
|
|
509
|
+
.map((call) => String(call[0] ?? ''))
|
|
510
|
+
.filter((line) => line.startsWith('⚠️'))
|
|
511
|
+
expect(noH1Warnings).toEqual([])
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('excludes build/dependency and hidden directories from the fallback navigation tree', async () => {
|
|
515
|
+
await plantMixedTree()
|
|
516
|
+
|
|
517
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
await generateNavigationJson()
|
|
521
|
+
} finally {
|
|
522
|
+
warnSpy.mockRestore()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const navigation = await readNavigation()
|
|
526
|
+
const paths = navigation.map((n: { path: string }) => n.path)
|
|
527
|
+
expect(paths).toContain('/')
|
|
528
|
+
expect(paths).toContain('/guide')
|
|
529
|
+
expect(paths).toContain('/features/theme')
|
|
530
|
+
// No node should be derived from a build/dependency or hidden directory
|
|
531
|
+
for (const excluded of [
|
|
532
|
+
'.mdsite',
|
|
533
|
+
'node_modules',
|
|
534
|
+
'dist',
|
|
535
|
+
'.nuxt',
|
|
536
|
+
'.output',
|
|
537
|
+
'.nitro',
|
|
538
|
+
'.data',
|
|
539
|
+
'.git',
|
|
540
|
+
'.vscode',
|
|
541
|
+
'.idea',
|
|
542
|
+
'.history',
|
|
543
|
+
'.cache'
|
|
544
|
+
]) {
|
|
545
|
+
expect(paths.some((p: string) => p.split('/').includes(excluded))).toBe(false)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const noH1Warnings = warnSpy.mock.calls
|
|
549
|
+
.map((call) => String(call[0] ?? ''))
|
|
550
|
+
.filter((line) => line.startsWith('⚠️'))
|
|
551
|
+
expect(noH1Warnings).toEqual([])
|
|
552
|
+
})
|
|
553
|
+
})
|
|
405
554
|
})
|
|
@@ -836,7 +836,29 @@ function filePathToUrlPath(filePath: string, sourceDir: string): string {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
/**
|
|
839
|
-
*
|
|
839
|
+
* Directory names that are never user-authored content. The recursive
|
|
840
|
+
* markdown walker skips these so a content directory that happens to be
|
|
841
|
+
* the project root (i.e. `mdsite.yml` lives at the repo root and
|
|
842
|
+
* `paths.input` is unset) does not crawl into the renderer working dir
|
|
843
|
+
* (`.mdsite/`), its `node_modules`, or other build/dependency artifacts.
|
|
844
|
+
*
|
|
845
|
+
* The rule is broad on purpose: any directory whose name starts with `.`
|
|
846
|
+
* (hidden dirs like `.git`, `.mdsite`, `.nuxt`, `.vscode`, `.idea`,
|
|
847
|
+
* `.history`, `.data`, `.output`, …) plus the two non-hidden directories
|
|
848
|
+
* that are always tooling artifacts (`node_modules`, `dist`). Keeping the
|
|
849
|
+
* list narrow would require enumerating every CI/editor/build tool that
|
|
850
|
+
* might leave a hidden directory next to the content.
|
|
851
|
+
*
|
|
852
|
+
* Keep this in sync with the Nuxt Content collection `exclude` list in
|
|
853
|
+
* `content.config.ts` — both are the same safety net at two layers.
|
|
854
|
+
*/
|
|
855
|
+
function isExcludedSourceDir(name: string): boolean {
|
|
856
|
+
return name.startsWith('.') || name === 'node_modules' || name === 'dist'
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Get all markdown files recursively, skipping build/dependency directories
|
|
861
|
+
* (see `isExcludedSourceDir`).
|
|
840
862
|
*/
|
|
841
863
|
async function getAllMarkdownFiles(dir: string): Promise<string[]> {
|
|
842
864
|
const files: string[] = []
|
|
@@ -852,6 +874,9 @@ async function getAllMarkdownFiles(dir: string): Promise<string[]> {
|
|
|
852
874
|
const stat = await fs.stat(itemPath)
|
|
853
875
|
|
|
854
876
|
if (stat.isDirectory()) {
|
|
877
|
+
if (isExcludedSourceDir(item)) {
|
|
878
|
+
continue
|
|
879
|
+
}
|
|
855
880
|
const subFiles = await getAllMarkdownFiles(itemPath)
|
|
856
881
|
files.push(...subFiles)
|
|
857
882
|
} else if (item.endsWith('.md') && !item.endsWith('.draft.md')) {
|
|
@@ -209,9 +209,22 @@ export async function startWatcher() {
|
|
|
209
209
|
`${sourceDir}/**/*.md` // Watch all markdown files
|
|
210
210
|
]
|
|
211
211
|
|
|
212
|
+
// Skip build/dependency directories (see `isExcludedSourceDir`). The
|
|
213
|
+
// matcher inspects every path segment so a hidden dir or
|
|
214
|
+
// `node_modules`/`dist` nested anywhere under the content dir is
|
|
215
|
+
// ignored, regardless of depth.
|
|
216
|
+
const ignored = (filePath: string, stats?: { isDirectory?: () => boolean }) => {
|
|
217
|
+
if (stats?.isDirectory && stats.isDirectory()) {
|
|
218
|
+
return isExcludedSourceDir(path.basename(filePath))
|
|
219
|
+
}
|
|
220
|
+
const segments = filePath.split(path.sep)
|
|
221
|
+
return segments.some((segment) => isExcludedSourceDir(segment))
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
const watcher = chokidar.watch(patterns, {
|
|
213
225
|
persistent: true,
|
|
214
226
|
ignoreInitial: true, // Files already copied above
|
|
227
|
+
ignored,
|
|
215
228
|
awaitWriteFinish: {
|
|
216
229
|
stabilityThreshold: 500,
|
|
217
230
|
pollInterval: 100
|
|
@@ -361,7 +374,29 @@ async function isDraftOnlyImage(imagePath: string): Promise<boolean> {
|
|
|
361
374
|
}
|
|
362
375
|
|
|
363
376
|
/**
|
|
364
|
-
*
|
|
377
|
+
* Directory names that are never user-authored content. The recursive image
|
|
378
|
+
* walker and the dev-mode file watcher both skip these so a content
|
|
379
|
+
* directory that happens to be the project root (i.e. `mdsite.yml` lives
|
|
380
|
+
* at the repo root and `paths.input` is unset) does not crawl into the
|
|
381
|
+
* renderer working dir (`.mdsite/`), its `node_modules`, or other
|
|
382
|
+
* build/dependency artifacts.
|
|
383
|
+
*
|
|
384
|
+
* The rule is broad on purpose: any directory whose name starts with `.`
|
|
385
|
+
* (hidden dirs like `.git`, `.mdsite`, `.nuxt`, `.vscode`, `.idea`,
|
|
386
|
+
* `.history`, `.data`, `.output`, …) plus the two non-hidden directories
|
|
387
|
+
* that are always tooling artifacts (`node_modules`, `dist`).
|
|
388
|
+
*
|
|
389
|
+
* Keep this in sync with the same predicate in
|
|
390
|
+
* `scripts/generate-indices.ts` and the Nuxt Content collection `exclude`
|
|
391
|
+
* list in `content.config.ts`.
|
|
392
|
+
*/
|
|
393
|
+
function isExcludedSourceDir(name: string): boolean {
|
|
394
|
+
return name.startsWith('.') || name === 'node_modules' || name === 'dist'
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get all files with specific extension recursively, skipping
|
|
399
|
+
* build/dependency directories (see `isExcludedSourceDir`).
|
|
365
400
|
*/
|
|
366
401
|
async function getAllFiles(dir: string, ext: string): Promise<string[]> {
|
|
367
402
|
const files: string[] = []
|
|
@@ -377,6 +412,9 @@ async function getAllFiles(dir: string, ext: string): Promise<string[]> {
|
|
|
377
412
|
const stat = await fs.stat(itemPath)
|
|
378
413
|
|
|
379
414
|
if (stat.isDirectory()) {
|
|
415
|
+
if (isExcludedSourceDir(item)) {
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
380
418
|
const subFiles = await getAllFiles(itemPath, ext)
|
|
381
419
|
files.push(...subFiles)
|
|
382
420
|
} else if (item.toLowerCase().endsWith(`.${ext}`)) {
|