Skip to content
Merged
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
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"cac": "^6.7.14",
"diff": "^8.0.3",
"type-fest": "^5.4.1"
},
"devDependencies": {
Expand Down
10 changes: 7 additions & 3 deletions src/cli/generate-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ export const generateMarkdown = (

for (const envvar of extractedEnvvars) {
const key = envvar.path.length > 0 ? envvar.path.join('.') : '';
if (!envvarsByPath.has(key)) {
envvarsByPath.set(key, []);

const envvars = envvarsByPath.get(key);

if (envvars) {
envvars.push(envvar);
} else {
envvarsByPath.set(key, [envvar]);
}
envvarsByPath.get(key)?.push(envvar);
}

for (const [path, envvars] of envvarsByPath.entries()) {
Expand Down
7 changes: 4 additions & 3 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ cli
}

console.error('Validation failed! Found differences:\n');
for (const diff of result.differences) {
console.error(diff);
}
console.error(result.differences);

console.error('\nTo regenerate documentation:');
console.error(` envase generate ${schemaPath} -o ${markdownPath}`);
process.exit(1);
} catch (error) {
console.error(
Expand Down
83 changes: 16 additions & 67 deletions src/cli/validate-markdown.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { stripVTControlCharacters } from 'node:util';
import { describe, expect, it } from 'vitest';
import { validateMarkdown } from './validate-markdown.ts';

Expand All @@ -16,7 +17,6 @@ describe('validateMarkdown', () => {
const result = validateMarkdown(markdown, markdown);

expect(result.isValid).toBe(true);
expect(result.differences).toEqual([]);
});

it('normalizes line endings and multiple newlines', () => {
Expand All @@ -26,29 +26,9 @@ describe('validateMarkdown', () => {
const result = validateMarkdown(actual, expected);

expect(result.isValid).toBe(true);
expect(result.differences).toEqual([]);
});

it('detects missing lines in actual file', () => {
const actual = `# Environment variables

## App`;
const expected = `# Environment variables

## App

- \`PORT\` (required)`;

const result = validateMarkdown(actual, expected);

expect(result.isValid).toBe(false);
expect(result.differences.length).toBeGreaterThan(0);
expect(
result.differences.some((d) => d.includes('Missing in actual file')),
).toBe(true);
});

it('detects extra lines in actual file', () => {
it('shows mixed changes with all prefix types', () => {
const actual = `# Environment variables

## App
Expand All @@ -59,54 +39,23 @@ describe('validateMarkdown', () => {

## App

- \`PORT\` (required)`;

const result = validateMarkdown(actual, expected);

expect(result.isValid).toBe(false);
expect(result.differences).toContain('Line 6: Extra line in actual file');
});

it('detects content mismatches', () => {
const actual = `# Environment variables

- \`PORT\` (optional)`;
const expected = `# Environment variables

- \`PORT\` (required)`;

const result = validateMarkdown(actual, expected);

expect(result.isValid).toBe(false);
expect(result.differences).toContain('Line 3: Content mismatch');
expect(result.differences).toContain(' Expected: - `PORT` (required)');
expect(result.differences).toContain(' Actual: - `PORT` (optional)');
});

it('handles empty strings', () => {
const result = validateMarkdown('', '');

expect(result.isValid).toBe(true);
expect(result.differences).toEqual([]);
});

it('reports all differences in a complex mismatch', () => {
const actual = `# Environment variables

## Database

- \`DB_URL\` (optional)`;
const expected = `# Environment variables

## App

- \`PORT\` (required)`;
- \`PORT\` (required)
- \`URL\` (required)`;

const result = validateMarkdown(actual, expected);

expect(result.isValid).toBe(false);
expect(result.differences.length).toBeGreaterThan(0);
expect(result.differences).toContain('Line 3: Content mismatch');
expect(result.differences).toContain('Line 5: Content mismatch');
if (!result.isValid) {
const plain = stripVTControlCharacters(result.differences);

// Should contain unchanged lines
expect(plain).toContain(' # Environment variables');
expect(plain).toContain(' ## App');
expect(plain).toContain(' - `PORT` (required)');
// Should contain removed line
expect(plain).toContain('- - `HOST` (optional)');
// Should contain added line
expect(plain).toContain('+ - `URL` (required)');
}
});
});
59 changes: 32 additions & 27 deletions src/cli/validate-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,55 @@
import { styleText } from 'node:util';
import { diffLines } from 'diff';

const normalizeMarkdown = (content: string): string => {
return content
.trim()
.replace(/\r\n/g, '\n')
.replace(/\n{3,}/g, '\n\n');
};

type ValidateMarkdownOutput =
| { isValid: true }
| { isValid: false; differences: string };

/**
* Validates if a markdown file matches the expected documentation
* generated from the environment schema.
*/
export const validateMarkdown = (
actualMarkdown: string,
expectedMarkdown: string,
): { isValid: boolean; differences: string[] } => {
const differences: string[] = [];

): ValidateMarkdownOutput => {
const actual = normalizeMarkdown(actualMarkdown);
const expected = normalizeMarkdown(expectedMarkdown);

if (actual === expected) {
return { isValid: true, differences: [] };
return { isValid: true };
}

const actualLines = actual.split('\n');
const expectedLines = expected.split('\n');

const maxLines = Math.max(actualLines.length, expectedLines.length);

for (let i = 0; i < maxLines; i++) {
const actualLine = actualLines[i] ?? '';
const expectedLine = expectedLines[i] ?? '';

if (actualLine !== expectedLine) {
if (i >= actualLines.length) {
differences.push(`Line ${i + 1}: Missing in actual file`);
differences.push(` Expected: ${expectedLine}`);
} else if (i >= expectedLines.length) {
differences.push(`Line ${i + 1}: Extra line in actual file`);
differences.push(` Actual: ${actualLine}`);
} else {
differences.push(`Line ${i + 1}: Content mismatch`);
differences.push(` Expected: ${expectedLine}`);
differences.push(` Actual: ${actualLine}`);
}
}
}
const differences = diffLines(actual, expected)
.flatMap((change) => {
const lines = change.value.split('\n');

// Only remove last element if it's empty (due to trailing newline)
const linesToProcess =
lines.length > 0 && lines[lines.length - 1] === ''
? lines.slice(0, -1)
: lines;

return linesToProcess.map((line) => {
if (change.added) {
return styleText('green', `+ ${line}`);
}
if (change.removed) {
return styleText('red', `- ${line}`);
}
return styleText('gray', ` ${line}`);
});
})
.join('\n');

console.log(differences);

return {
isValid: false,
Expand Down