Skip to content

Commit aa5daed

Browse files
authored
fix: fix env line endings (#376)
1 parent f782b3e commit aa5daed

3 files changed

Lines changed: 63 additions & 8 deletions

File tree

.changeset/eighty-hornets-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dotenv-diff': patch
3+
---
4+
5+
fix env handle windows styled line endings

packages/cli/src/core/fixEnv.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export function applyFixes(options: ApplyFixesOptions): {
5050
if (duplicateKeys.length) {
5151
const duplicateSet = new Set(duplicateKeys);
5252

53-
const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
53+
const raw = fs.readFileSync(envPath, 'utf-8');
54+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
55+
const lines = raw.split(eol);
5456
const seen = new Set<string>();
5557
const newLines: string[] = [];
5658

@@ -69,18 +71,19 @@ export function applyFixes(options: ApplyFixesOptions): {
6971
newLines.unshift(line);
7072
}
7173

72-
fs.writeFileSync(envPath, newLines.join('\n'));
74+
fs.writeFileSync(envPath, newLines.join(eol));
7375
result.removedDuplicates = duplicateKeys;
7476
}
7577

7678
// --- Add missing keys to .env ---
7779
if (missingKeys.length) {
7880
const content = fs.readFileSync(envPath, 'utf-8');
81+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
7982
const newContent =
8083
content +
81-
(content.endsWith('\n') ? '' : '\n') +
82-
missingKeys.map((k) => `${k}=`).join('\n') +
83-
'\n';
84+
(content.endsWith('\n') ? '' : eol) +
85+
missingKeys.map((k) => `${k}=`).join(eol) +
86+
eol;
8487
fs.writeFileSync(envPath, newContent);
8588
result.addedEnv = missingKeys;
8689
}

packages/cli/test/unit/core/fixEnv.test.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('applyFixes', () => {
6262

6363
it('does not duplicate keys in .env.example if already present', () => {
6464
fs.writeFileSync(examplePath, 'A=\nB=\n');
65-
const { changed, result } = applyFixes({
65+
const {} = applyFixes({
6666
envPath,
6767
missingKeys: ['B'],
6868
duplicateKeys: [],
@@ -150,7 +150,7 @@ B=2
150150
});
151151

152152
const env = fs.readFileSync(envPath, 'utf-8');
153-
expect(env).toBe(`A=2\nC=1\nB=2\n`);
153+
expect(env).toBe('A=2\nC=1\nB=2\n');
154154
});
155155

156156
it('handles ensureGitignore=true without throwing (best-effort)', () => {
@@ -164,6 +164,53 @@ B=2
164164
expect(changed).toBe(false);
165165
});
166166

167+
describe('line ending preservation', () => {
168+
it('preserves CRLF when removing duplicate keys', () => {
169+
fs.writeFileSync(envPath, 'A=1\r\nB=2\r\nA=3\r\n');
170+
171+
applyFixes({ envPath, missingKeys: [], duplicateKeys: ['A'] });
172+
173+
const finalContent = fs.readFileSync(envPath, 'utf-8');
174+
expect(finalContent).toBe('B=2\r\nA=3\r\n');
175+
});
176+
177+
it('preserves LF when removing duplicate keys', () => {
178+
fs.writeFileSync(envPath, 'A=1\nB=2\nA=3\n');
179+
180+
applyFixes({ envPath, missingKeys: [], duplicateKeys: ['A'] });
181+
182+
const finalContent = fs.readFileSync(envPath, 'utf-8');
183+
expect(finalContent).toBe('B=2\nA=3\n');
184+
});
185+
186+
it('preserves CRLF when adding missing keys', () => {
187+
fs.writeFileSync(envPath, 'A=1\r\n');
188+
189+
applyFixes({ envPath, missingKeys: ['B', 'C'], duplicateKeys: [] });
190+
191+
const finalContent = fs.readFileSync(envPath, 'utf-8');
192+
expect(finalContent).toBe('A=1\r\nB=\r\nC=\r\n');
193+
});
194+
195+
it('preserves LF when adding missing keys', () => {
196+
fs.writeFileSync(envPath, 'A=1\n');
197+
198+
applyFixes({ envPath, missingKeys: ['B', 'C'], duplicateKeys: [] });
199+
200+
const finalContent = fs.readFileSync(envPath, 'utf-8');
201+
expect(finalContent).toBe('A=1\nB=\nC=\n');
202+
});
203+
204+
it('uses CRLF separator when file has no trailing newline and CRLF style', () => {
205+
fs.writeFileSync(envPath, 'A=1\r\nB=2'); // no trailing newline
206+
207+
applyFixes({ envPath, missingKeys: ['C'], duplicateKeys: [] });
208+
209+
const finalContent = fs.readFileSync(envPath, 'utf-8');
210+
expect(finalContent).toBe('A=1\r\nB=2\r\nC=\r\n');
211+
});
212+
});
213+
167214
describe('ensureGitignore functionality', () => {
168215
it('creates .gitignore when in git repo but no .gitignore exists', () => {
169216
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dotenv-diff-'));
@@ -230,7 +277,7 @@ B=2
230277
const gitignorePath = path.join(tmpDir, '.gitignore');
231278
fs.writeFileSync(gitignorePath, '.env\n.env.*\n');
232279

233-
const { changed, result } = applyFixes({
280+
const { result } = applyFixes({
234281
envPath,
235282
missingKeys: [],
236283
duplicateKeys: [],

0 commit comments

Comments
 (0)