From 8c3696b034c5285fc06c8767bc514b99812b5c12 Mon Sep 17 00:00:00 2001 From: Mateusz Tkacz Date: Fri, 23 Jan 2026 12:51:09 +0100 Subject: [PATCH 1/3] feat: add verify cli command --- README.md | 22 ++++++ src/cli/main.ts | 50 ++++++++++++- src/cli/validate-markdown.test.ts | 112 ++++++++++++++++++++++++++++++ src/cli/validate-markdown.ts | 53 ++++++++++++++ 4 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/cli/validate-markdown.test.ts create mode 100644 src/cli/validate-markdown.ts diff --git a/README.md b/README.md index a13fbc8..2b631fa 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,28 @@ Generates markdown documentation from an environment schema. **Options:** - `-o, --output ` - Output file path (default: `./env-docs.md`) +#### `envase validate ` + +Validates if a markdown file matches the documentation that would be generated from the environment schema. + +**Arguments:** +- `` - Path to a file containing default export of env schema +- `` - Path to the markdown file to validate + +**Example:** +```bash +envase validate ./config.ts ./docs/env.md +``` + +This command is useful for: +- CI/CD pipelines to ensure documentation stays in sync with code +- Pre-commit hooks to verify documentation changes +- Detecting manual edits to generated documentation + +**Exit codes:** +- `0` - Validation passed (markdown matches schema) +- `1` - Validation failed (differences found) or error occurred + ### Example Output The CLI generates readable markdown documentation with: diff --git a/src/cli/main.ts b/src/cli/main.ts index 99eeef2..02866e3 100755 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -1,11 +1,12 @@ #!/usr/bin/env node -import { writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { cac } from 'cac'; import { extractEnvvars } from './extract-envvars.ts'; import { generateMarkdown } from './generate-markdown.ts'; import { loadSchema } from './load-schema.ts'; +import { validateMarkdown } from './validate-markdown.ts'; const cli = cac('envase'); @@ -26,11 +27,11 @@ cli }, ) => { try { - console.log('📖 Loading schema from:', schemaPath); + console.log('Loading schema from:', schemaPath); const schema = await loadSchema(schemaPath); const extractedEnvvars = extractEnvvars(schema); - console.log('📝 Generating markdown documentation...'); + console.log('Generating markdown documentation...'); const markdown = generateMarkdown(extractedEnvvars); const outputPath = resolve(process.cwd(), options.output); @@ -46,5 +47,48 @@ cli }, ); +cli + .command( + 'validate ', + 'Validate if markdown file matches the schema definition', + ) + .example('envase validate ./config.js ./docs/env.md') + .action(async (schemaPath: string, markdownPath: string) => { + try { + console.log('Loading schema from:', schemaPath); + const schema = await loadSchema(schemaPath); + const extractedEnvvars = extractEnvvars(schema); + + console.log('Generating expected markdown...'); + const expectedMarkdown = generateMarkdown(extractedEnvvars); + + console.log('Reading actual markdown from:', markdownPath); + const markdownFilePath = resolve(process.cwd(), markdownPath); + const actualMarkdown = await readFile(markdownFilePath, 'utf-8'); + + console.log('Validating...'); + const result = validateMarkdown(actualMarkdown, expectedMarkdown); + + if (result.isValid) { + console.log( + 'Validation passed! The markdown file matches the schema.', + ); + process.exit(0); + } + + console.error('Validation failed! Found differences:\n'); + for (const diff of result.differences) { + console.error(diff); + } + process.exit(1); + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : String(error), + ); + process.exit(1); + } + }); + cli.help(); cli.parse(); diff --git a/src/cli/validate-markdown.test.ts b/src/cli/validate-markdown.test.ts new file mode 100644 index 0000000..e5796e2 --- /dev/null +++ b/src/cli/validate-markdown.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'vitest'; +import { validateMarkdown } from './validate-markdown.ts'; + +describe('validateMarkdown', () => { + it('returns valid when markdown matches exactly', () => { + const markdown = `# Environment variables + +## App + +- \`PORT\` (required) + Type: \`number\` + Description: Application port + +`; + + const result = validateMarkdown(markdown, markdown); + + expect(result.isValid).toBe(true); + expect(result.differences).toEqual([]); + }); + + it('normalizes line endings and multiple newlines', () => { + const actual = '# Environment variables\r\n\r\n\r\n## App\r\n'; + const expected = '# Environment variables\n\n## App\n'; + + 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', () => { + const actual = `# Environment variables + +## App + +- \`PORT\` (required) +- \`HOST\` (optional)`; + const expected = `# Environment variables + +## 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)`; + + 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'); + }); +}); diff --git a/src/cli/validate-markdown.ts b/src/cli/validate-markdown.ts new file mode 100644 index 0000000..b803f6a --- /dev/null +++ b/src/cli/validate-markdown.ts @@ -0,0 +1,53 @@ +const normalizeMarkdown = (content: string): string => { + return content + .trim() + .replace(/\r\n/g, '\n') + .replace(/\n{3,}/g, '\n\n'); +}; + +/** + * 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[] = []; + + const actual = normalizeMarkdown(actualMarkdown); + const expected = normalizeMarkdown(expectedMarkdown); + + if (actual === expected) { + return { isValid: true, differences: [] }; + } + + 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}`); + } + } + } + + return { + isValid: false, + differences, + }; +}; From d0438cd3143888d9bb901707bb6dae63bd58c53e Mon Sep 17 00:00:00 2001 From: Mateusz Tkacz Date: Fri, 23 Jan 2026 13:07:16 +0100 Subject: [PATCH 2/3] chore: adjust README --- README.md | 90 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 2b631fa..44547df 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,9 @@ type Config = InferEnv; // { apiKey: string; db: { host: string } } ``` -## CLI Documentation Generator +## CLI -Automatically generate markdown documentation from your environment variable schemas. +Automatically generate and validate markdown documentation from your environment variable schemas. ### Quick Start @@ -168,15 +168,14 @@ export default envSchema **2. Generate documentation:** ```bash -# Using TypeScript directly with Node.js type stripping feature envase generate ./config.ts -o ./docs/env.md +``` -# Or use tsx (recommended for older Node versions) -tsx node_modules/.bin/envase generate ./config.ts -o ./docs/env.md +**3. Validate documentation (optional):** -# Or compile first, then generate -tsc config.ts -envase generate ./config.js -o ./docs/env.md +```bash +# Verify the documentation matches your schema +envase validate ./config.ts ./docs/env.md ``` ### Command Reference @@ -186,34 +185,23 @@ envase generate ./config.js -o ./docs/env.md Generates markdown documentation from an environment schema. **Arguments:** -- `` - Path to a file containing default export of env schema. +- `` - Path to a file containing default export of env schema **Options:** - `-o, --output ` - Output file path (default: `./env-docs.md`) -#### `envase validate ` - -Validates if a markdown file matches the documentation that would be generated from the environment schema. - -**Arguments:** -- `` - Path to a file containing default export of env schema -- `` - Path to the markdown file to validate - -**Example:** +**Usage:** ```bash -envase validate ./config.ts ./docs/env.md -``` +envase generate ./config.ts -o ./docs/env.md -This command is useful for: -- CI/CD pipelines to ensure documentation stays in sync with code -- Pre-commit hooks to verify documentation changes -- Detecting manual edits to generated documentation +# Or use tsx for TypeScript files (recommended for older Node versions) +tsx node_modules/.bin/envase generate ./config.ts -o ./docs/env.md -**Exit codes:** -- `0` - Validation passed (markdown matches schema) -- `1` - Validation failed (differences found) or error occurred +# Or compile first, then generate +tsc config.ts && envase generate ./config.js -o ./docs/env.md +``` -### Example Output +**Generated output:** The CLI generates readable markdown documentation with: - Type information for each environment variable @@ -224,31 +212,55 @@ The CLI generates readable markdown documentation with: - Enum values (for enum types) - Grouped by nested configuration structure -**Sample generated markdown:** +
+Sample generated markdown ```markdown # Environment variables ## App / Listen -- \`PORT\` (optional) - Type: \`number\` +- `PORT` (optional) + Type: `number` Description: Application listening port - Min value: \`1024\` - Max value: \`65535\` + Min value: `1024` + Max value: `65535` -- \`HOST\` (optional) - Type: \`string\` +- `HOST` (optional) + Type: `string` Description: Bind host address - Default: \`0.0.0.0\` + Default: `0.0.0.0` ## Database -- \`DATABASE_URL\` (required) - Type: \`string\` +- `DATABASE_URL` (required) + Type: `string` Description: PostgreSQL connection URL - Format: \`uri\` + Format: `uri` ``` +
+ +#### `envase validate ` + +Validates if a markdown file matches the documentation that would be generated from the environment schema. + +**Arguments:** +- `` - Path to a file containing default export of env schema +- `` - Path to the markdown file to validate + +**Example:** +```bash +envase validate ./config.ts ./docs/env.md +``` + +This command is useful for: +- CI/CD pipelines to ensure documentation stays in sync with code +- Pre-commit hooks to verify documentation changes +- Detecting manual edits to generated documentation + +**Exit codes:** +- `0` - Validation passed (markdown matches schema) +- `1` - Validation failed (differences found) or error occurred ## API Reference From 82c904d1d4839e00e9e2911467ec5642bd544091 Mon Sep 17 00:00:00 2001 From: Mateusz Tkacz Date: Fri, 23 Jan 2026 13:07:44 +0100 Subject: [PATCH 3/3] chore: exec lint fix --- src/cli/main.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cli/main.ts b/src/cli/main.ts index 02866e3..f8a6a6a 100755 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -70,9 +70,7 @@ cli const result = validateMarkdown(actualMarkdown, expectedMarkdown); if (result.isValid) { - console.log( - 'Validation passed! The markdown file matches the schema.', - ); + console.log('Validation passed! The markdown file matches the schema.'); process.exit(0); }