Skip to content

Commit 3590acc

Browse files
committed
fix(sim-setup): write $ as \$ in double quotes, the one form Next, Bun, and Compose all read literally
1 parent 38e9aca commit 3590acc

2 files changed

Lines changed: 34 additions & 11 deletions

File tree

‎packages/sim-setup/src/env-files.test.ts‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
22
import { tmpdir } from 'node:os'
33
import path from 'node:path'
4+
import { loadEnvConfig } from '@next/env'
45
import { afterEach, describe, expect, it } from 'vitest'
56
import { parseEnv, reconcileEnvContent, upsertEnv, writeEnvFile } from './env-files'
67

@@ -21,16 +22,28 @@ describe('upsertEnv', () => {
2122
).toThrow('Duplicate active RESEND_API_KEY entries')
2223
})
2324

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

31-
it('single-quotes a value with $ so dotenv, Bun, and Compose do not expand it', () => {
32-
expect(upsertEnv('', 'SMTP_PASSWORD', 'pa$$word$HOME')).toBe("SMTP_PASSWORD='pa$$word$HOME'\n")
33-
})
32+
it.each(['pa$$word', 'key$HOME', `key\${HOME}suffix`, "it's $5"])(
33+
'Next loads %j back exactly as it was written',
34+
(value) => {
35+
const root = mkdtempSync(path.join(tmpdir(), 'sim-setup-env-expand-'))
36+
roots.push(root)
37+
writeFileSync(path.join(root, '.env'), upsertEnv('', 'SIM_SETUP_EXPAND_PROBE', value))
38+
const silent = { info: () => {}, error: () => {} }
39+
try {
40+
const { parsedEnv } = loadEnvConfig(root, false, silent, true)
41+
expect(parsedEnv?.SIM_SETUP_EXPAND_PROBE).toBe(value)
42+
} finally {
43+
Reflect.deleteProperty(process.env, 'SIM_SETUP_EXPAND_PROBE')
44+
}
45+
}
46+
)
3447

3548
it('writes an ordinary value without quotes', () => {
3649
const dsn = 'postgresql://postgres:p%23w@127.0.0.1:5432/simstudio'

‎packages/sim-setup/src/env-files.ts‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ function parseValue(raw: string): string {
6767
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
6868
const quote = trimmed[0]
6969
const end = trimmed.indexOf(quote, 1)
70-
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)
70+
const inner = end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)
71+
return quote === '"' ? inner.replaceAll('\\$', '$') : inner
7172
}
7273
return trimmed.replace(/\s+#.*$/, '').trim()
7374
}
@@ -90,19 +91,28 @@ export function readEnvFile(target: EnvTarget): EnvFile {
9091

9192
/**
9293
* Quotes a value only when `#`, `$`, edge whitespace, or a leading quote would
93-
* change it for `parseValue`, Next's dotenv, Bun, or Docker Compose (the last three
94-
* expand `$NAME` in unquoted values). Single quotes are preferred because all four
95-
* read them literally.
94+
* change it for `parseValue`, Next's dotenv, Bun, or Docker Compose. Single quotes
95+
* are preferred because all four read them literally, except `$`: Next and Bun
96+
* expand `$NAME` even inside single quotes, and the one spelling all four read as a
97+
* literal `$` is `\$` inside double quotes.
9698
*/
9799
function formatEnvValue(key: string, value: string): string {
98100
if (/[\r\n]/.test(value)) {
99101
throw new Error(`${key} cannot be written to an environment file: it contains a line break`)
100102
}
101-
if (value === value.trim() && !/[#$]/.test(value) && !/^["'`]/.test(value)) return value
103+
if (value.includes('$')) {
104+
if (/["\\`]/.test(value)) {
105+
throw new Error(
106+
`${key} cannot be written to an environment file: it mixes $ with a double quote, backslash, or backtick`
107+
)
108+
}
109+
return `"${value.replaceAll('$', '\\$')}"`
110+
}
111+
if (value === value.trim() && !value.includes('#') && !/^["'`]/.test(value)) return value
102112
if (!value.includes("'")) return `'${value}'`
103-
if (!/["\\$`]/.test(value)) return `"${value}"`
113+
if (!/["\\`]/.test(value)) return `"${value}"`
104114
throw new Error(
105-
`${key} cannot be written to an environment file: it mixes quotes with a #, $, or edge whitespace`
115+
`${key} cannot be written to an environment file: it mixes quotes with a # or edge whitespace`
106116
)
107117
}
108118

0 commit comments

Comments
 (0)