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
19 changes: 18 additions & 1 deletion packages/sim-setup/src/env-files.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { loadEnvConfig } from '@next/env'
import { afterEach, describe, expect, it } from 'vitest'
import { parseEnv, reconcileEnvContent, upsertEnv, writeEnvFile } from './env-files'

Expand All @@ -21,13 +22,29 @@ describe('upsertEnv', () => {
).toThrow('Duplicate active RESEND_API_KEY entries')
})

it.each(['ab #cd', 'a#b', ' padded ', "'starts-with-quote", "it's #1"])(
it.each(['ab #cd', 'a#b', ' padded ', "'starts-with-quote", "it's #1", "it's $5 #1"])(
'reads back %j exactly as it was written',
(value) => {
expect(parseEnv(upsertEnv('', 'SECRET', value)).get('SECRET')).toBe(value)
}
)

it.each(['pa$$word', 'key$HOME', `key\${HOME}suffix`, "it's $5"])(
'Next loads %j back exactly as it was written',
(value) => {
const root = mkdtempSync(path.join(tmpdir(), 'sim-setup-env-expand-'))
roots.push(root)
writeFileSync(path.join(root, '.env'), upsertEnv('', 'SIM_SETUP_EXPAND_PROBE', value))
const silent = { info: () => {}, error: () => {} }
try {
const { parsedEnv } = loadEnvConfig(root, false, silent, true)
expect(parsedEnv?.SIM_SETUP_EXPAND_PROBE).toBe(value)
} finally {
Reflect.deleteProperty(process.env, 'SIM_SETUP_EXPAND_PROBE')
}
}
)

it('writes an ordinary value without quotes', () => {
const dsn = 'postgresql://postgres:p%23w@127.0.0.1:5432/simstudio'
expect(upsertEnv('', 'DATABASE_URL', dsn)).toBe(`DATABASE_URL=${dsn}\n`)
Expand Down
21 changes: 16 additions & 5 deletions packages/sim-setup/src/env-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ function parseValue(raw: string): string {
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
const quote = trimmed[0]
const end = trimmed.indexOf(quote, 1)
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)
const inner = end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)
return quote === '"' ? inner.replaceAll('\\$', '$') : inner
}
return trimmed.replace(/\s+#.*$/, '').trim()
}
Expand All @@ -89,17 +90,27 @@ export function readEnvFile(target: EnvTarget): EnvFile {
}

/**
* Quotes a value only when `#`, edge whitespace, or a leading quote would change
* it for `parseValue`, Next's dotenv, Bun, or Docker Compose. Single quotes are
* preferred because all four read them literally.
* Quotes a value only when `#`, `$`, edge whitespace, or a leading quote would
* change it for `parseValue`, Next's dotenv, Bun, or Docker Compose. Single quotes
* are preferred because all four read them literally, except `$`: Next and Bun
* expand `$NAME` even inside single quotes, and the one spelling all four read as a
* literal `$` is `\$` inside double quotes.
*/
function formatEnvValue(key: string, value: string): string {
if (/[\r\n]/.test(value)) {
throw new Error(`${key} cannot be written to an environment file: it contains a line break`)
}
if (value.includes('$')) {
if (/["\\`]/.test(value)) {
throw new Error(
`${key} cannot be written to an environment file: it mixes $ with a double quote, backslash, or backtick`
)
}
return `"${value.replaceAll('$', '\\$')}"`
}
if (value === value.trim() && !value.includes('#') && !/^["'`]/.test(value)) return value
if (!value.includes("'")) return `'${value}'`
if (!/["\\$`]/.test(value)) return `"${value}"`
if (!/["\\`]/.test(value)) return `"${value}"`
throw new Error(
`${key} cannot be written to an environment file: it mixes quotes with a # or edge whitespace`
)
Expand Down
Loading