Skip to content
Draft
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
111 changes: 99 additions & 12 deletions packages/jsrepo/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,105 @@ export function searchForEnvFile(
}

export function parseEnvVariables(contents: string): Record<string, string> {
const lines = contents.split('\n').filter((line) => line.trim().length > 0);
return lines.reduce(
(acc, line) => {
const firstEqualIndex = line.indexOf('=');
if (firstEqualIndex === -1) return acc;
const name = line.slice(0, firstEqualIndex);
const value = line.slice(firstEqualIndex + 1);
acc[name] = value ?? '';
return acc;
},
{} as Record<string, string>
);
const result: Record<string, string> = {};
const lines = contents.split('\n');
let currentKey: string | null = null;
let currentValue: string[] = [];

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === undefined) continue;
const trimmedLine = line.trim();

// If we're continuing a multiline value (previous line ended with backslash)
if (currentKey !== null && currentValue.length > 0) {
// Check if this line ends with backslash (continues to next line)
if (line.trimEnd().endsWith('\\')) {
// Remove the trailing backslash and leading whitespace, then continue
const continuationLine = line.trimEnd().slice(0, -1).replace(/^\s+/, '');
currentValue.push(continuationLine);
continue;
} else {
// Check if this line looks like a new variable
// If it does, finalize the multiline value first
let looksLikeNewVar = false;
if (trimmedLine.length > 0 && !trimmedLine.startsWith('#')) {
const firstEqualIndex = line.indexOf('=');
if (firstEqualIndex > 0) {
const potentialName = line.slice(0, firstEqualIndex).trim();
// Check if it looks like a valid variable name (alphanumeric/underscore)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(potentialName)) {
looksLikeNewVar = true;
}
}
}

if (looksLikeNewVar) {
// Multiline value is complete - finalize it
// Remove any trailing empty lines
while (currentValue.length > 0 && currentValue[currentValue.length - 1] === '') {
currentValue.pop();
}
result[currentKey] = currentValue.join('\n');
currentKey = null;
currentValue = [];
// Continue processing this line as a new variable
} else {
// Add this line to the multiline value and continue
// Only add non-empty lines, or empty lines if we're explicitly continuing
const continuationLine = line.replace(/^\s+/, '');
currentValue.push(continuationLine);
continue;
}
}
}

// Skip empty lines (only if we're not in a multiline value)
if (trimmedLine.length === 0) {
continue;
}

// Check for comments
if (trimmedLine.startsWith('#')) {
continue;
}

const firstEqualIndex = line.indexOf('=');
if (firstEqualIndex === -1) {
// If we're in a multiline value, this line is a continuation
if (currentKey !== null) {
currentValue.push(line.replace(/^\s+/, ''));
continue;
}
// Otherwise, skip lines without =
continue;
}

const name = line.slice(0, firstEqualIndex).trim();
let value = line.slice(firstEqualIndex + 1);

// Check if line ends with backslash (multiline continuation)
if (line.trimEnd().endsWith('\\')) {
// Remove the trailing backslash and start building multiline value
currentKey = name;
currentValue = [value.slice(0, -1).trimEnd()];
continue;
}

// Single line value - preserve as-is (including quotes)
result[name] = value ?? '';
}

// Handle case where file ends with a multiline value
if (currentKey !== null) {
// Remove any trailing empty lines
while (currentValue.length > 0 && currentValue[currentValue.length - 1] === '') {
currentValue.pop();
}
result[currentKey] = currentValue.join('\n');
}

return result;
}

export function updateEnvFile(contents: string, envVars: Record<string, string>): string {
Expand Down
51 changes: 51 additions & 0 deletions packages/jsrepo/tests/utils/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,57 @@ describe('parseEnvVariables', () => {
SIMPLE: 'normal_value',
});
});

it('should parse multiline environment variables with backslash continuation', () => {
const contents = 'MULTILINE=line1\\\nline2\\\nline3\nSIMPLE=single_line\n';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
MULTILINE: 'line1\nline2\nline3',
SIMPLE: 'single_line',
});
});

it('should parse multiline environment variables with multiple continuation lines', () => {
const contents = 'CERTIFICATE=-----BEGIN CERTIFICATE-----\\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\\\n-----END CERTIFICATE-----\nOTHER=value\n';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
CERTIFICATE: '-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n-----END CERTIFICATE-----',
OTHER: 'value',
});
});

it('should parse multiline environment variables with indentation', () => {
const contents = 'MULTILINE=first line\\\n second line with indent\\\nthird line\n';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
MULTILINE: 'first line\nsecond line with indent\nthird line',
});
});

it('should parse multiline environment variables at end of file', () => {
const contents = 'MULTILINE=line1\\\nline2\\\nline3';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
MULTILINE: 'line1\nline2\nline3',
});
});

it('should handle multiline values with empty continuation lines', () => {
const contents = 'MULTILINE=line1\\\n\nline3\n';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
MULTILINE: 'line1\n\nline3',
});
});

it('should parse multiline environment variables with quotes preserved', () => {
const contents = 'QUOTED="value with quotes"\\\ncontinued\nREGULAR=normal\n';
const envVars = parseEnvVariables(contents);
expect(envVars).toEqual({
QUOTED: '"value with quotes"\ncontinued',
REGULAR: 'normal',
});
});
});

describe('updateEnvFile', () => {
Expand Down
Loading