env-sentinel
Version:
Zero-dependency tool that auto-validates .env files against schema.env, with optional fallback and secure warnings.
234 lines (233 loc) • 7.96 kB
JavaScript
import { parseRuleString } from '../validate/parse-rule-string.js';
const TAGS = {
SECTION: '@section',
DESCRIPTION: '@description',
VAR: '@var',
EXAMPLE: '@example'
};
export function parseSchemaForDocs(rawSchemaContent) {
const lines = rawSchemaContent.split(/\r?\n/);
const sections = [];
let i = 0;
while (i < lines.length) {
const line = lines[i].trim();
if (!line) {
i++;
continue;
}
if (line.startsWith('#')) {
const commentContent = line.substring(1).trim();
if (commentContent.startsWith(TAGS.SECTION)) {
const { section, nextIndex } = parseSection(lines, i);
sections.push(section);
i = nextIndex;
continue;
}
if (commentContent.startsWith(TAGS.VAR)) {
const defaultSection = ensureDefaultSection(sections);
const { variable, nextIndex } = parseVariable(lines, i);
defaultSection.variables.push(variable);
i = nextIndex;
continue;
}
i++;
continue;
}
const equalIndex = line.indexOf('=');
if (equalIndex !== -1) {
const defaultSection = ensureDefaultSection(sections);
const variable = parseVariableWithoutDocs(line, i);
defaultSection.variables.push(variable);
}
i++;
}
return sections;
}
function ensureDefaultSection(sections) {
if (sections.length === 0 || sections[sections.length - 1].name !== 'General') {
const defaultSection = {
name: 'General',
description: undefined,
variables: []
};
sections.push(defaultSection);
return defaultSection;
}
return sections[sections.length - 1];
}
function parseVariableWithoutDocs(line, lineIndex) {
const equalIndex = line.indexOf('=');
const key = line.substring(0, equalIndex).trim();
const ruleString = line.substring(equalIndex + 1).trim();
if (!key) {
throw new Error(`Invalid variable definition at line ${lineIndex + 1}. Variable name is required.`);
}
const defaultValue = extractDefault(ruleString);
const cleanRuleString = removeDefault(ruleString);
const rules = cleanRuleString ? parseRuleString(cleanRuleString) : [];
return {
key,
description: undefined,
example: undefined,
rules,
default: defaultValue
};
}
function parseSection(lines, startIndex) {
const commentContent = lines[startIndex].trim().substring(1).trim();
let sectionName = commentContent.substring(TAGS.SECTION.length).trim();
if (sectionName.startsWith(':')) {
sectionName = sectionName.substring(1).trim();
}
if (!sectionName) {
throw new Error(`Empty section name at line ${startIndex + 1}. Section name is required.`);
}
const section = {
name: sectionName,
description: undefined,
variables: []
};
let i = startIndex + 1;
if (i < lines.length) {
const line = lines[i].trim();
if (line.startsWith('#')) {
const content = line.substring(1).trim();
if (content.startsWith(TAGS.DESCRIPTION)) {
const { description, nextIndex } = parseDescriptionBlock(lines, i);
section.description = description;
i = nextIndex;
}
}
}
while (i < lines.length) {
const line = lines[i].trim();
if (!line) {
i++;
continue;
}
if (line.startsWith('#')) {
const content = line.substring(1).trim();
if (content.startsWith(TAGS.SECTION)) {
break;
}
if (content.startsWith(TAGS.VAR)) {
const { variable, nextIndex } = parseVariable(lines, i);
section.variables.push(variable);
i = nextIndex;
continue;
}
i++;
continue;
}
const equalIndex = line.indexOf('=');
if (equalIndex !== -1) {
const variable = parseVariableWithoutDocs(line, i);
section.variables.push(variable);
}
i++;
}
return { section, nextIndex: i };
}
function parseDescriptionBlock(lines, startIndex) {
const commentContent = lines[startIndex].trim().substring(1).trim();
let firstLine = commentContent.substring(TAGS.DESCRIPTION.length).trim();
if (firstLine.startsWith(':')) {
firstLine = firstLine.substring(1).trim();
}
const descLines = firstLine ? [firstLine] : [];
let i = startIndex + 1;
while (i < lines.length) {
const line = lines[i].trim();
if (!line || !line.startsWith('#')) {
break;
}
const content = line.substring(1).trim();
if (content.startsWith('@')) {
break;
}
if (isSeparatorLine(content)) {
i++;
continue;
}
descLines.push(content);
i++;
}
return { description: descLines.join('\n'), nextIndex: i };
}
function parseVariable(lines, startIndex) {
const commentContent = lines[startIndex].trim().substring(1).trim();
let firstLine = commentContent.substring(TAGS.VAR.length).trim();
if (firstLine.startsWith(':')) {
firstLine = firstLine.substring(1).trim();
}
const descLines = firstLine ? [firstLine] : [];
let i = startIndex + 1;
let example;
while (i < lines.length) {
const line = lines[i].trim();
if (!line || !line.startsWith('#')) {
break;
}
const content = line.substring(1).trim();
if (content.startsWith(TAGS.EXAMPLE)) {
let exampleValue = content.substring(TAGS.EXAMPLE.length).trim();
if (exampleValue.startsWith(':')) {
exampleValue = exampleValue.substring(1).trim();
}
example = exampleValue;
i++;
continue;
}
if (content.startsWith('@')) {
break;
}
if (isSeparatorLine(content)) {
i++;
continue;
}
descLines.push(content);
i++;
}
while (i < lines.length) {
const line = lines[i].trim();
if (!line || line.startsWith('#')) {
i++;
continue;
}
const equalIndex = line.indexOf('=');
if (equalIndex !== -1) {
const key = line.substring(0, equalIndex).trim();
const ruleString = line.substring(equalIndex + 1).trim();
if (!key) {
throw new Error(`Invalid variable definition at line ${i + 1}. Variable name is required.`);
}
const defaultValue = extractDefault(ruleString);
const cleanRuleString = removeDefault(ruleString);
const rules = cleanRuleString ? parseRuleString(cleanRuleString) : [];
const variable = {
key,
description: descLines.length > 0 ? descLines.join('\n') : undefined,
example,
rules,
default: defaultValue
};
return { variable, nextIndex: i + 1 };
}
i++;
}
throw new Error(`Variable definition not found after @var comment at line ${startIndex + 1}. Expected variable assignment (KEY=value).`);
}
function extractDefault(ruleString) {
const match = ruleString.match(/(?:^|\|)default:"([^"]*?)"/);
return match ? match[1] : undefined;
}
function removeDefault(ruleString) {
return ruleString
.replace(/(?:^|\|)default:"[^"]*?"/g, '')
.replace(/\|\|+/g, '|')
.replace(/^\|+|\|+$/g, '')
.trim();
}
function isSeparatorLine(content) {
return /^[=\-_*]+$/.test(content);
}