From c75b76455bfd3232120536cf382bb39c55e50866 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 7 Apr 2026 15:55:36 +0900 Subject: [PATCH 1/4] 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 --- e2e-tests/tests/flutter.test.ts | 85 +++++++++++++++++++++++++++++++ src/android/android-wizard.ts | 4 ++ src/flutter/flutter-wizard.ts | 4 ++ src/utils/line-endings.ts | 36 ++++++++++++++ test/utils/line-endings.test.ts | 88 +++++++++++++++++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 src/utils/line-endings.ts create mode 100644 test/utils/line-endings.test.ts diff --git a/e2e-tests/tests/flutter.test.ts b/e2e-tests/tests/flutter.test.ts index d9392f74f..529e54139 100644 --- a/e2e-tests/tests/flutter.test.ts +++ b/e2e-tests/tests/flutter.test.ts @@ -1,4 +1,5 @@ import * as fs from 'node:fs'; +import * as path from 'node:path'; import { Integration } from '../../lib/Constants'; import { createIsolatedTestEnv, getWizardCommand } from '../utils'; import { @@ -159,4 +160,88 @@ describe('Flutter', () => { ); }); }); + + describe('with CRLF line endings', () => { + let wizardExitCode: number; + const { projectDir, cleanup } = createIsolatedTestEnv('flutter-test-app'); + + function convertToCrlf(dir: string) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + convertToCrlf(fullPath); + } else if ( + /\.(yaml|dart|properties)$/.test(entry.name) || + entry.name === '.gitignore' + ) { + const content = fs.readFileSync(fullPath, 'utf-8'); + fs.writeFileSync( + fullPath, + content.replace(/\r?\n/g, '\r\n'), + 'utf-8', + ); + } + } + } + + beforeAll(async () => { + convertToCrlf(projectDir); + + wizardExitCode = await withEnv({ + cwd: projectDir, + debug: true, + }) + .defineInteraction() + .expectOutput( + 'The Sentry Flutter Wizard will help you set up Sentry for your application', + ) + .whenAsked('Do you want to enable Tracing') + .respondWith(KEYS.ENTER) + .whenAsked( + 'to analyze CPU usage and optimize performance-critical code on iOS & macOS?', + ) + .respondWith(KEYS.ENTER) + .whenAsked('to record user interactions and debug issues?') + .respondWith(KEYS.ENTER) + .whenAsked('to send your application logs to Sentry?') + .respondWith(KEYS.ENTER) + .whenAsked( + 'Optionally add a project-scoped MCP server configuration for the Sentry MCP?', + ) + .respondWith(KEYS.DOWN, KEYS.ENTER) + .expectOutput('Successfully installed the Sentry Flutter SDK!') + .run(getWizardCommand(Integration.flutter)); + }); + + afterAll(() => { + cleanup(); + }); + + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + + test('modified files preserve CRLF line endings', () => { + const textFiles = fs + .readdirSync(projectDir, { recursive: true, encoding: 'utf-8' }) + .filter( + (f) => + /\.(yaml|dart|properties)$/.test(f) || f.endsWith('.gitignore'), + ); + + expect(textFiles.length).toBeGreaterThan(0); + + const filesWithMixedEndings = textFiles.filter((file) => { + const content = fs.readFileSync( + path.join(projectDir, file), + 'utf-8', + ); + const stripped = content.replace(/\r\n/g, ''); + return stripped.includes('\n'); + }); + + expect(filesWithMixedEndings).toEqual([]); + }); + }); }); diff --git a/src/android/android-wizard.ts b/src/android/android-wizard.ts index 79a429c18..462f95b7e 100644 --- a/src/android/android-wizard.ts +++ b/src/android/android-wizard.ts @@ -22,6 +22,7 @@ import * as codetools from './code-tools'; import * as gradle from './gradle'; import * as manifest from './manifest'; import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported'; +import { fixLineEndings } from '../utils/line-endings'; const proguardMappingCliSetupConfig: CliSetupConfig = { ...propertiesCliSetupConfig, @@ -187,6 +188,9 @@ async function runAndroidWizardWithTelemetry( selectedProject.slug, ); + // Fix mixed line endings caused by inserting LF content into CRLF files (Windows) + fixLineEndings(); + // ======== OUTRO ======== const issuesPageLink = selfHosted ? `${sentryUrl}organizations/${selectedProject.organization.slug}/issues/?project=${selectedProject.id}` diff --git a/src/flutter/flutter-wizard.ts b/src/flutter/flutter-wizard.ts index 1a59bd089..a920b18bc 100644 --- a/src/flutter/flutter-wizard.ts +++ b/src/flutter/flutter-wizard.ts @@ -20,6 +20,7 @@ import { traceStep, withTelemetry } from '../telemetry'; import { findFile } from './code-tools'; import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config'; import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported'; +import { fixLineEndings } from '../utils/line-endings'; export async function runFlutterWizard(options: WizardOptions): Promise { return withTelemetry( @@ -160,6 +161,9 @@ Set the ${chalk.cyan( } Sentry.setTag('main-patched', mainPatched); + // Fix mixed line endings caused by inserting LF content into CRLF files (Windows) + fixLineEndings(); + // ======== OUTRO ======== // Offer optional project-scoped MCP config for Sentry with org and project scope diff --git a/src/utils/line-endings.ts b/src/utils/line-endings.ts new file mode 100644 index 000000000..e2fee0688 --- /dev/null +++ b/src/utils/line-endings.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { getUncommittedOrUntrackedFiles } from './git'; + +/** + * Fixes mixed line endings in files modified by the wizard. + * + * When the wizard reads a CRLF file and inserts content with hardcoded \n, + * the result is mixed line endings. This function detects files with CRLF + * and normalizes all line endings to CRLF. + * + * Call this at the end of a wizard run, similar to runPrettierIfInstalled(). + */ +export function fixLineEndings(): void { + const files = getUncommittedOrUntrackedFiles() + .map((f) => (f.startsWith('- ') ? f.slice(2) : f)) + .filter(Boolean); + + for (const file of files) { + const filePath = path.resolve(file); + + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + continue; + } + + const content = fs.readFileSync(filePath, 'utf8'); + + if (!content.includes('\r\n')) { + continue; + } + + // File has CRLF — normalize all line endings to CRLF + const normalized = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'); + fs.writeFileSync(filePath, normalized, 'utf8'); + } +} diff --git a/test/utils/line-endings.test.ts b/test/utils/line-endings.test.ts new file mode 100644 index 000000000..d04c774d7 --- /dev/null +++ b/test/utils/line-endings.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { fixLineEndings } from '../../src/utils/line-endings'; +import * as git from '../../src/utils/git'; + +describe('fixLineEndings', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'line-endings-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true }); + vi.restoreAllMocks(); + }); + + it('normalizes mixed line endings to CRLF when file has CRLF', () => { + const filePath = path.join(tmpDir, 'mixed.txt'); + fs.writeFileSync(filePath, 'line1\r\nline2\nline3\r\n', 'utf8'); + + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + `- ${filePath}`, + ]); + + fixLineEndings(); + + const result = fs.readFileSync(filePath, 'utf8'); + expect(result).toBe('line1\r\nline2\r\nline3\r\n'); + }); + + it('skips files that are pure LF', () => { + const filePath = path.join(tmpDir, 'lf.txt'); + const original = 'line1\nline2\n'; + fs.writeFileSync(filePath, original, 'utf8'); + + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + `- ${filePath}`, + ]); + + fixLineEndings(); + + const result = fs.readFileSync(filePath, 'utf8'); + expect(result).toBe(original); + }); + + it('leaves consistent CRLF files unchanged', () => { + const filePath = path.join(tmpDir, 'crlf.txt'); + const original = 'line1\r\nline2\r\n'; + fs.writeFileSync(filePath, original, 'utf8'); + + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + `- ${filePath}`, + ]); + + fixLineEndings(); + + const result = fs.readFileSync(filePath, 'utf8'); + expect(result).toBe(original); + }); + + it('skips directories', () => { + const dirPath = path.join(tmpDir, 'subdir'); + fs.mkdirSync(dirPath); + + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + `- ${dirPath}`, + ]); + + expect(() => fixLineEndings()).not.toThrow(); + }); + + it('skips nonexistent files', () => { + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + '- nonexistent.txt', + ]); + + expect(() => fixLineEndings()).not.toThrow(); + }); + + it('does nothing when there are no modified files', () => { + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([]); + + expect(() => fixLineEndings()).not.toThrow(); + }); +}); From 3ecf364b051cb8bf97ae1dd1b55bc838512811be Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 7 Apr 2026 17:11:28 +0900 Subject: [PATCH 2/4] Run line fixing after mcp step for flutter --- src/flutter/flutter-wizard.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/flutter/flutter-wizard.ts b/src/flutter/flutter-wizard.ts index a920b18bc..694c15d1e 100644 --- a/src/flutter/flutter-wizard.ts +++ b/src/flutter/flutter-wizard.ts @@ -161,9 +161,6 @@ Set the ${chalk.cyan( } Sentry.setTag('main-patched', mainPatched); - // Fix mixed line endings caused by inserting LF content into CRLF files (Windows) - fixLineEndings(); - // ======== OUTRO ======== // Offer optional project-scoped MCP config for Sentry with org and project scope @@ -172,6 +169,9 @@ Set the ${chalk.cyan( selectedProject.slug, ); + // Fix mixed line endings caused by inserting LF content into CRLF files (Windows) + fixLineEndings(); + const issuesPageLink = selfHosted ? `${sentryUrl}organizations/${selectedProject.organization.slug}/issues/?project=${selectedProject.id}` : `https://${selectedProject.organization.slug}.sentry.io/issues/?project=${selectedProject.id}`; From 2e413cdbf9b9808145adc1f79762d279f40bbc90 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 7 Apr 2026 18:07:20 +0900 Subject: [PATCH 3/4] Only apply this fix to known file types --- src/utils/line-endings.ts | 20 ++++++++++++++++++++ test/utils/line-endings.test.ts | 21 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/utils/line-endings.ts b/src/utils/line-endings.ts index e2fee0688..a406ac92b 100644 --- a/src/utils/line-endings.ts +++ b/src/utils/line-endings.ts @@ -11,6 +11,20 @@ import { getUncommittedOrUntrackedFiles } from './git'; * * Call this at the end of a wizard run, similar to runPrettierIfInstalled(). */ +const TEXT_EXTENSIONS = new Set([ + '.dart', + '.yaml', + '.yml', + '.properties', + '.java', + '.kt', + '.xml', + '.gradle', + '.kts', + '.gitignore', + '.json', +]); + export function fixLineEndings(): void { const files = getUncommittedOrUntrackedFiles() .map((f) => (f.startsWith('- ') ? f.slice(2) : f)) @@ -23,6 +37,12 @@ export function fixLineEndings(): void { continue; } + const ext = path.extname(filePath).toLowerCase(); + const basename = path.basename(filePath); + if (!TEXT_EXTENSIONS.has(ext) && !TEXT_EXTENSIONS.has(basename)) { + continue; + } + const content = fs.readFileSync(filePath, 'utf8'); if (!content.includes('\r\n')) { diff --git a/test/utils/line-endings.test.ts b/test/utils/line-endings.test.ts index d04c774d7..05de3ffc4 100644 --- a/test/utils/line-endings.test.ts +++ b/test/utils/line-endings.test.ts @@ -18,7 +18,7 @@ describe('fixLineEndings', () => { }); it('normalizes mixed line endings to CRLF when file has CRLF', () => { - const filePath = path.join(tmpDir, 'mixed.txt'); + const filePath = path.join(tmpDir, 'mixed.dart'); fs.writeFileSync(filePath, 'line1\r\nline2\nline3\r\n', 'utf8'); vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ @@ -32,7 +32,7 @@ describe('fixLineEndings', () => { }); it('skips files that are pure LF', () => { - const filePath = path.join(tmpDir, 'lf.txt'); + const filePath = path.join(tmpDir, 'lf.dart'); const original = 'line1\nline2\n'; fs.writeFileSync(filePath, original, 'utf8'); @@ -47,7 +47,7 @@ describe('fixLineEndings', () => { }); it('leaves consistent CRLF files unchanged', () => { - const filePath = path.join(tmpDir, 'crlf.txt'); + const filePath = path.join(tmpDir, 'crlf.dart'); const original = 'line1\r\nline2\r\n'; fs.writeFileSync(filePath, original, 'utf8'); @@ -61,6 +61,21 @@ describe('fixLineEndings', () => { expect(result).toBe(original); }); + it('skips non-text files', () => { + const filePath = path.join(tmpDir, 'image.png'); + const original = 'line1\r\nline2\nline3\r\n'; + fs.writeFileSync(filePath, original, 'utf8'); + + vi.spyOn(git, 'getUncommittedOrUntrackedFiles').mockReturnValue([ + `- ${filePath}`, + ]); + + fixLineEndings(); + + const result = fs.readFileSync(filePath, 'utf8'); + expect(result).toBe(original); + }); + it('skips directories', () => { const dirPath = path.join(tmpDir, 'subdir'); fs.mkdirSync(dirPath); From 8e5636edc53b0556e5c0635f43b47d3251a65676 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 7 Apr 2026 19:04:55 +0900 Subject: [PATCH 4/4] Ensure we continue after converting the test app to crlf makes the repo dirty --- e2e-tests/tests/flutter.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e-tests/tests/flutter.test.ts b/e2e-tests/tests/flutter.test.ts index 529e54139..4f2c8ddbc 100644 --- a/e2e-tests/tests/flutter.test.ts +++ b/e2e-tests/tests/flutter.test.ts @@ -193,6 +193,8 @@ describe('Flutter', () => { debug: true, }) .defineInteraction() + .whenAsked('Do you want to continue anyway?') + .respondWith(KEYS.ENTER) .expectOutput( 'The Sentry Flutter Wizard will help you set up Sentry for your application', )