Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,60 @@ describe('checkFilesystem', () => {

expect(findings.find((f) => f.rule === Rule.AllowedFileTypes)).toBeUndefined();
});

it('should not report max-nesting-depth for pages at depth 3 or shallower', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'docs-test-'));
await writeFile(join(tmp, 'index.md'), '---\ntitle: Home\n---\n');
await mkdir(join(tmp, 'a'));
await writeFile(join(tmp, 'a', 'index.md'), '---\ntitle: A\n---\n');
await mkdir(join(tmp, 'a', 'b'));
await writeFile(join(tmp, 'a', 'b', 'page.md'), '---\ntitle: Page\n---\n');

const findings = await checkFilesystem({ docsPath: tmp, strict: true });

expect(findings.find((f) => f.rule === Rule.MaxNestingDepth)).toBeUndefined();
});

it('should report max-nesting-depth as error in strict mode for pages deeper than 3 levels', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'docs-test-'));
await writeFile(join(tmp, 'index.md'), '---\ntitle: Home\n---\n');
await mkdir(join(tmp, 'a', 'b', 'c'), { recursive: true });
await writeFile(join(tmp, 'a', 'b', 'c', 'page.md'), '---\ntitle: Deep\n---\n');

const findings = await checkFilesystem({ docsPath: tmp, strict: true });

const finding = findings.find((f) => f.rule === Rule.MaxNestingDepth);
expect(finding).toBeDefined();
expect(finding!.severity).toBe('error');
expect(finding!.file).toContain(join('a', 'b', 'c', 'page.md'));
expect(finding!.title).toContain('4 levels');
});

it('should report max-nesting-depth as info in non-strict mode for pages deeper than 3 levels', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'docs-test-'));
await writeFile(join(tmp, 'index.md'), '---\ntitle: Home\n---\n');
await mkdir(join(tmp, 'a', 'b', 'c'), { recursive: true });
await writeFile(join(tmp, 'a', 'b', 'c', 'page.md'), '---\ntitle: Deep\n---\n');

const findings = await checkFilesystem({ docsPath: tmp, strict: false });

const finding = findings.find((f) => f.rule === Rule.MaxNestingDepth);
expect(finding).toBeDefined();
expect(finding!.severity).toBe('info');
});

it('should report max-nesting-depth once per offending file', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'docs-test-'));
await writeFile(join(tmp, 'index.md'), '---\ntitle: Home\n---\n');
await mkdir(join(tmp, 'a', 'b', 'c'), { recursive: true });
await writeFile(join(tmp, 'a', 'b', 'c', 'one.md'), '---\ntitle: One\n---\n');
await writeFile(join(tmp, 'a', 'b', 'c', 'two.md'), '---\ntitle: Two\n---\n');
await mkdir(join(tmp, 'a', 'b', 'c', 'd'), { recursive: true });
await writeFile(join(tmp, 'a', 'b', 'c', 'd', 'three.md'), '---\ntitle: Three\n---\n');

const findings = await checkFilesystem({ docsPath: tmp, strict: true });

const depthFindings = findings.filter((f) => f.rule === Rule.MaxNestingDepth);
expect(depthFindings).toHaveLength(3);
});
});
21 changes: 20 additions & 1 deletion packages/plugin-docs-cli/src/validation/rules/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { readdir } from 'node:fs/promises';
import type { Dirent } from 'node:fs';
import { join, extname, sep } from 'node:path';
import { join, extname, relative, sep } from 'node:path';
import { type Diagnostic, type ValidationInput, Rule } from '../types.js';

// slug-safe: lowercase letters, digits and hyphens only
const SLUG_SAFE_RE = /^[a-z0-9-]+$/;

// max path segments from the docs root (e.g. `a/b/page.md` is 3)
const MAX_NESTING_DEPTH = 3;

// permitted image formats shared across filesystem and asset rules
export const ALLOWED_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);

Expand Down Expand Up @@ -38,6 +41,22 @@ export async function checkFilesystem(input: ValidationInput): Promise<Diagnosti
});
}

// max-nesting-depth: report .md pages nested deeper than MAX_NESTING_DEPTH from docsPath
for (const file of mdFiles) {
const filePath = join(file.parentPath, file.name);
const rel = relative(input.docsPath, filePath);
const depth = rel.split(sep).length;
if (depth > MAX_NESTING_DEPTH) {
diagnostics.push({
rule: Rule.MaxNestingDepth,
severity: input.strict ? 'error' : 'info',
file: filePath,
title: `Doc page nested too deeply (${depth} levels, max ${MAX_NESTING_DEPTH})`,
Comment on lines +49 to +54
detail: `"${rel}" is ${depth} levels from the docs root. Deeply nested pages are hard to discover in the sidebar nav. Flatten the structure so no page is more than ${MAX_NESTING_DEPTH} levels deep.`,
});
}
}

// allowed-file-types: non-.md files must be permitted image formats
for (const file of nonMdFiles) {
const ext = extname(file.name).toLowerCase();
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-docs-cli/src/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const Rule = {
NoEmptyDir: 'no-empty-directories',
NoSymlinks: 'no-symlinks',
AllowedFileTypes: 'allowed-file-types',
MaxNestingDepth: 'max-nesting-depth',
// frontmatter rules
BlockExists: 'frontmatter-block-exists',
ValidYaml: 'frontmatter-valid-yaml',
Expand Down
Loading