Skip to content

Commit c75b764

Browse files
committed
fix(android,flutter): Preserve CRLF line endings when present
This PR fixes mixed line endinges when the wizard modifies files that have CRLF (\r\n) line endings. Previously we would always append LF (\n), thus mixing line endings, causing issues on platforms that use CRLF. It adds a `fixLineEndings()` post-processing step for Android and Flutter wizards that detects modified files with CRLF and normalizes any LF to CRLF. Other wizards use proper formatters or don't suffer from this issue. Closes: #1249
1 parent 109b8e2 commit c75b764

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

e2e-tests/tests/flutter.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
23
import { Integration } from '../../lib/Constants';
34
import { createIsolatedTestEnv, getWizardCommand } from '../utils';
45
import {
@@ -159,4 +160,88 @@ describe('Flutter', () => {
159160
);
160161
});
161162
});
163+
164+
describe('with CRLF line endings', () => {
165+
let wizardExitCode: number;
166+
const { projectDir, cleanup } = createIsolatedTestEnv('flutter-test-app');
167+
168+
function convertToCrlf(dir: string) {
169+
const entries = fs.readdirSync(dir, { withFileTypes: true });
170+
for (const entry of entries) {
171+
const fullPath = path.join(dir, entry.name);
172+
if (entry.isDirectory()) {
173+
convertToCrlf(fullPath);
174+
} else if (
175+
/\.(yaml|dart|properties)$/.test(entry.name) ||
176+
entry.name === '.gitignore'
177+
) {
178+
const content = fs.readFileSync(fullPath, 'utf-8');
179+
fs.writeFileSync(
180+
fullPath,
181+
content.replace(/\r?\n/g, '\r\n'),
182+
'utf-8',
183+
);
184+
}
185+
}
186+
}
187+
188+
beforeAll(async () => {
189+
convertToCrlf(projectDir);
190+
191+
wizardExitCode = await withEnv({
192+
cwd: projectDir,
193+
debug: true,
194+
})
195+
.defineInteraction()
196+
.expectOutput(
197+
'The Sentry Flutter Wizard will help you set up Sentry for your application',
198+
)
199+
.whenAsked('Do you want to enable Tracing')
200+
.respondWith(KEYS.ENTER)
201+
.whenAsked(
202+
'to analyze CPU usage and optimize performance-critical code on iOS & macOS?',
203+
)
204+
.respondWith(KEYS.ENTER)
205+
.whenAsked('to record user interactions and debug issues?')
206+
.respondWith(KEYS.ENTER)
207+
.whenAsked('to send your application logs to Sentry?')
208+
.respondWith(KEYS.ENTER)
209+
.whenAsked(
210+
'Optionally add a project-scoped MCP server configuration for the Sentry MCP?',
211+
)
212+
.respondWith(KEYS.DOWN, KEYS.ENTER)
213+
.expectOutput('Successfully installed the Sentry Flutter SDK!')
214+
.run(getWizardCommand(Integration.flutter));
215+
});
216+
217+
afterAll(() => {
218+
cleanup();
219+
});
220+
221+
test('exits with exit code 0', () => {
222+
expect(wizardExitCode).toBe(0);
223+
});
224+
225+
test('modified files preserve CRLF line endings', () => {
226+
const textFiles = fs
227+
.readdirSync(projectDir, { recursive: true, encoding: 'utf-8' })
228+
.filter(
229+
(f) =>
230+
/\.(yaml|dart|properties)$/.test(f) || f.endsWith('.gitignore'),
231+
);
232+
233+
expect(textFiles.length).toBeGreaterThan(0);
234+
235+
const filesWithMixedEndings = textFiles.filter((file) => {
236+
const content = fs.readFileSync(
237+
path.join(projectDir, file),
238+
'utf-8',
239+
);
240+
const stripped = content.replace(/\r\n/g, '');
241+
return stripped.includes('\n');
242+
});
243+
244+
expect(filesWithMixedEndings).toEqual([]);
245+
});
246+
});
162247
});

src/android/android-wizard.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as codetools from './code-tools';
2222
import * as gradle from './gradle';
2323
import * as manifest from './manifest';
2424
import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported';
25+
import { fixLineEndings } from '../utils/line-endings';
2526

2627
const proguardMappingCliSetupConfig: CliSetupConfig = {
2728
...propertiesCliSetupConfig,
@@ -187,6 +188,9 @@ async function runAndroidWizardWithTelemetry(
187188
selectedProject.slug,
188189
);
189190

191+
// Fix mixed line endings caused by inserting LF content into CRLF files (Windows)
192+
fixLineEndings();
193+
190194
// ======== OUTRO ========
191195
const issuesPageLink = selfHosted
192196
? `${sentryUrl}organizations/${selectedProject.organization.slug}/issues/?project=${selectedProject.id}`

src/flutter/flutter-wizard.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { traceStep, withTelemetry } from '../telemetry';
2020
import { findFile } from './code-tools';
2121
import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config';
2222
import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported';
23+
import { fixLineEndings } from '../utils/line-endings';
2324

2425
export async function runFlutterWizard(options: WizardOptions): Promise<void> {
2526
return withTelemetry(
@@ -160,6 +161,9 @@ Set the ${chalk.cyan(
160161
}
161162
Sentry.setTag('main-patched', mainPatched);
162163

164+
// Fix mixed line endings caused by inserting LF content into CRLF files (Windows)
165+
fixLineEndings();
166+
163167
// ======== OUTRO ========
164168

165169
// Offer optional project-scoped MCP config for Sentry with org and project scope

src/utils/line-endings.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { getUncommittedOrUntrackedFiles } from './git';
4+
5+
/**
6+
* Fixes mixed line endings in files modified by the wizard.
7+
*
8+
* When the wizard reads a CRLF file and inserts content with hardcoded \n,
9+
* the result is mixed line endings. This function detects files with CRLF
10+
* and normalizes all line endings to CRLF.
11+
*
12+
* Call this at the end of a wizard run, similar to runPrettierIfInstalled().
13+
*/
14+
export function fixLineEndings(): void {
15+
const files = getUncommittedOrUntrackedFiles()
16+
.map((f) => (f.startsWith('- ') ? f.slice(2) : f))
17+
.filter(Boolean);
18+
19+
for (const file of files) {
20+
const filePath = path.resolve(file);
21+
22+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
23+
continue;
24+
}
25+
26+
const content = fs.readFileSync(filePath, 'utf8');
27+
28+
if (!content.includes('\r\n')) {
29+
continue;
30+
}
31+
32+
// File has CRLF — normalize all line endings to CRLF
33+
const normalized = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
34+
fs.writeFileSync(filePath, normalized, 'utf8');
35+
}
36+
}

test/utils/line-endings.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import * as os from 'os';
5+
import { fixLineEndings } from '../../src/utils/line-endings';
6+
import * as git from '../../src/utils/git';
7+
8+
describe('fixLineEndings', () => {
9+
let tmpDir: string;
10+
11+
beforeEach(() => {
12+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'line-endings-test-'));
13+
});
14+
15+
afterEach(() => {
16+
fs.rmSync(tmpDir, { recursive: true });
17+
vi.restoreAllMocks();
18+
});
19+
20+
it('normalizes mixed line endings to CRLF when file has CRLF', () => {
21+
const filePath = path.join(tmpDir, 'mixed.txt');
22+
fs.writeFileSync(filePath, 'line1\r\nline2\nline3\r\n', 'utf8');
23+
24+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([
25+
`- ${filePath}`,
26+
]);
27+
28+
fixLineEndings();
29+
30+
const result = fs.readFileSync(filePath, 'utf8');
31+
expect(result).toBe('line1\r\nline2\r\nline3\r\n');
32+
});
33+
34+
it('skips files that are pure LF', () => {
35+
const filePath = path.join(tmpDir, 'lf.txt');
36+
const original = 'line1\nline2\n';
37+
fs.writeFileSync(filePath, original, 'utf8');
38+
39+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([
40+
`- ${filePath}`,
41+
]);
42+
43+
fixLineEndings();
44+
45+
const result = fs.readFileSync(filePath, 'utf8');
46+
expect(result).toBe(original);
47+
});
48+
49+
it('leaves consistent CRLF files unchanged', () => {
50+
const filePath = path.join(tmpDir, 'crlf.txt');
51+
const original = 'line1\r\nline2\r\n';
52+
fs.writeFileSync(filePath, original, 'utf8');
53+
54+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([
55+
`- ${filePath}`,
56+
]);
57+
58+
fixLineEndings();
59+
60+
const result = fs.readFileSync(filePath, 'utf8');
61+
expect(result).toBe(original);
62+
});
63+
64+
it('skips directories', () => {
65+
const dirPath = path.join(tmpDir, 'subdir');
66+
fs.mkdirSync(dirPath);
67+
68+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([
69+
`- ${dirPath}`,
70+
]);
71+
72+
expect(() => fixLineEndings()).not.toThrow();
73+
});
74+
75+
it('skips nonexistent files', () => {
76+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([
77+
'- nonexistent.txt',
78+
]);
79+
80+
expect(() => fixLineEndings()).not.toThrow();
81+
});
82+
83+
it('does nothing when there are no modified files', () => {
84+
vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([]);
85+
86+
expect(() => fixLineEndings()).not.toThrow();
87+
});
88+
});

0 commit comments

Comments
 (0)