diff --git a/packages/jsrepo/src/utils/env.ts b/packages/jsrepo/src/utils/env.ts index 89a2f4d4..82d82ba2 100644 --- a/packages/jsrepo/src/utils/env.ts +++ b/packages/jsrepo/src/utils/env.ts @@ -29,18 +29,105 @@ export function searchForEnvFile( } export function parseEnvVariables(contents: string): Record { - 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 - ); + const result: Record = {}; + 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 { diff --git a/packages/jsrepo/tests/utils/env.test.ts b/packages/jsrepo/tests/utils/env.test.ts index fffef3c1..f2ea2f1d 100644 --- a/packages/jsrepo/tests/utils/env.test.ts +++ b/packages/jsrepo/tests/utils/env.test.ts @@ -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', () => {