diff --git a/e2e-tests/tests/pnpm-workspace.test.ts b/e2e-tests/tests/pnpm-workspace.test.ts index fd1b473b1..87b47e6f2 100644 --- a/e2e-tests/tests/pnpm-workspace.test.ts +++ b/e2e-tests/tests/pnpm-workspace.test.ts @@ -101,7 +101,8 @@ describe('pnpm workspace', () => { org: "${TEST_ARGS.ORG_SLUG}", project: "${TEST_ARGS.PROJECT_SLUG}" }), sveltekit()] - });" + }); + " `); }); diff --git a/e2e-tests/tests/sveltekit-tracing.test.ts b/e2e-tests/tests/sveltekit-tracing.test.ts index afcabb16e..296885365 100644 --- a/e2e-tests/tests/sveltekit-tracing.test.ts +++ b/e2e-tests/tests/sveltekit-tracing.test.ts @@ -105,7 +105,8 @@ describe('Sveltekit with instrumentation and tracing', () => { org: "${TEST_ARGS.ORG_SLUG}", project: "${TEST_ARGS.PROJECT_SLUG}" }), sveltekit()] - });" + }); + " `); }); diff --git a/src/react-native/javascript.ts b/src/react-native/javascript.ts index b0ca158cd..32cd1611f 100644 --- a/src/react-native/javascript.ts +++ b/src/react-native/javascript.ts @@ -11,6 +11,7 @@ import { traceStep } from '../telemetry'; import { makeCodeSnippet, showCopyPasteInstructions } from '../utils/clack'; import { getFirstMatchedPath } from './glob'; import { RN_SDK_PACKAGE } from './react-native-wizard'; +import { preserveTrailingNewline } from '../utils/ast-utils'; // @ts-expect-error - magicast is ESM and TS complains about that. It works though import { generateCode, ProxifiedModule, parseModule } from 'magicast'; @@ -248,7 +249,8 @@ export async function wrapRootComponent() { traceStep('add-sentry-wrap', () => { try { - fs.writeFileSync(jsPath, generateCode(mod.$ast).code, 'utf-8'); + const code = preserveTrailingNewline(js, generateCode(mod.$ast).code); + fs.writeFileSync(jsPath, code, 'utf-8'); clack.log.success( `Added ${chalk.cyan('Sentry.wrap')} to ${chalk.cyan(jsRelativePath)}.`, ); diff --git a/src/react-router/codemods/react-router-config.ts b/src/react-router/codemods/react-router-config.ts index 234f0abf7..ec6f55385 100644 --- a/src/react-router/codemods/react-router-config.ts +++ b/src/react-router/codemods/react-router-config.ts @@ -10,7 +10,7 @@ import { parseModule, generateCode } from 'magicast'; import clack from '@clack/prompts'; import chalk from 'chalk'; -import { findProperty } from '../../utils/ast-utils'; +import { findProperty, preserveTrailingNewline } from '../../utils/ast-utils'; /** * Extracts the ObjectExpression from various export patterns. @@ -205,7 +205,10 @@ export default { throw new Error('Failed to modify React Router config structure'); } - const code = generateCode(mod.$ast).code; + const code = preserveTrailingNewline( + configContent, + generateCode(mod.$ast).code, + ); await fs.promises.writeFile(configPath, code); return { ssrWasChanged }; diff --git a/src/react-router/codemods/vite.ts b/src/react-router/codemods/vite.ts index 05678bf57..6bf8476c0 100644 --- a/src/react-router/codemods/vite.ts +++ b/src/react-router/codemods/vite.ts @@ -10,7 +10,11 @@ import { parseModule, generateCode } from 'magicast'; import clack from '@clack/prompts'; import chalk from 'chalk'; -import { hasSentryContent, findProperty } from '../../utils/ast-utils'; +import { + hasSentryContent, + findProperty, + preserveTrailingNewline, +} from '../../utils/ast-utils'; /** * Extracts ObjectExpression from function body. @@ -199,7 +203,10 @@ export async function instrumentViteConfig( throw new Error('Failed to modify Vite config structure'); } - const code = generateCode(mod.$ast).code; + const code = preserveTrailingNewline( + configContent, + generateCode(mod.$ast).code, + ); await fs.promises.writeFile(configPath, code); return { wasConverted }; diff --git a/src/sourcemaps/tools/vite.ts b/src/sourcemaps/tools/vite.ts index ee0b52df3..56ca506da 100644 --- a/src/sourcemaps/tools/vite.ts +++ b/src/sourcemaps/tools/vite.ts @@ -28,7 +28,11 @@ import { SourceMapUploadToolConfigurationFunction, SourceMapUploadToolConfigurationOptions, } from './types'; -import { findFile, hasSentryContent } from '../../utils/ast-utils'; +import { + findFile, + hasSentryContent, + preserveTrailingNewline, +} from '../../utils/ast-utils'; import * as path from 'path'; import * as fs from 'fs'; @@ -163,7 +167,10 @@ export async function addVitePluginToConfig( }, }); - const code = generateCode(mod.$ast).code; + const code = preserveTrailingNewline( + viteConfigContent, + generateCode(mod.$ast).code, + ); await fs.promises.writeFile(viteConfigPath, code); diff --git a/src/sveltekit/sdk-setup/vite.ts b/src/sveltekit/sdk-setup/vite.ts index 54a89381a..28ca9f6e7 100644 --- a/src/sveltekit/sdk-setup/vite.ts +++ b/src/sveltekit/sdk-setup/vite.ts @@ -14,7 +14,10 @@ import * as recast from 'recast'; import x = recast.types; import t = x.namedTypes; -import { hasSentryContent } from '../../utils/ast-utils'; +import { + hasSentryContent, + preserveTrailingNewline, +} from '../../utils/ast-utils'; import { debug } from '../../utils/debug'; import { abortIfCancelled } from '../../utils/clack'; import type { ProjectInfo } from './types'; @@ -64,7 +67,10 @@ Skipping adding Sentry functionality to.`, await modifyAndRecordFail( async () => { - const code = generateCode(viteModule.$ast).code; + const code = preserveTrailingNewline( + viteConfigContent, + generateCode(viteModule.$ast).code, + ); await fs.promises.writeFile(viteConfigPath, code); }, 'write-file', diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts index 3178d0408..100197f0e 100644 --- a/src/utils/ast-utils.ts +++ b/src/utils/ast-utils.ts @@ -329,3 +329,26 @@ export function findProperty( p.key.name === name, ) as t.ObjectProperty | undefined; // Safe: predicate guarantees type } + +/** + * Preserves trailing newline from original content if present. + * Code generators like magicast/recast don't preserve trailing newlines, + * so this ensures we don't unnecessarily modify user files. + * + * @param originalContent - The original file content + * @param generatedCode - The code generated by AST transformation + * @returns The generated code with trailing newline preserved if original had one + */ +export function preserveTrailingNewline( + originalContent: string, + generatedCode: string, +): string { + const hadTrailingNewline = originalContent.endsWith('\n'); + const hasTrailingNewline = generatedCode.endsWith('\n'); + + if (hadTrailingNewline && !hasTrailingNewline) { + return generatedCode + '\n'; + } + + return generatedCode; +} diff --git a/test/sourcemaps/tools/vite.test.ts b/test/sourcemaps/tools/vite.test.ts index 270e32a60..fbc777cf4 100644 --- a/test/sourcemaps/tools/vite.test.ts +++ b/test/sourcemaps/tools/vite.test.ts @@ -52,7 +52,8 @@ export default defineConfig({ build: { sourcemap: true } -})`, +}) +`, ], [ 'no build.sourcemap options', @@ -62,10 +63,10 @@ export default defineConfig({ vue(), ], build: { - test: 1, + test: 1, } }) - `, +`, `import { sentryVitePlugin } from "@sentry/vite-plugin"; export default defineConfig({ plugins: [vue(), sentryVitePlugin({ @@ -76,7 +77,8 @@ export default defineConfig({ test: 1, sourcemap: true } -})`, +}) +`, ], [ 'keep sourcemap: "hidden"', @@ -89,7 +91,7 @@ export default { sourcemap: "hidden", } } - `, +`, `import { sentryVitePlugin } from "@sentry/vite-plugin"; export default { plugins: [vue(), sentryVitePlugin({ @@ -99,7 +101,8 @@ export default { build: { sourcemap: "hidden", } -}`, +} +`, ], [ 'rewrite sourcemap: false to true', @@ -114,7 +117,7 @@ const cfg = { } export default cfg; - `, +`, `import { sentryVitePlugin } from "@sentry/vite-plugin"; const cfg = { plugins: [vue(), sentryVitePlugin({ @@ -127,7 +130,8 @@ const cfg = { } } -export default cfg;`, +export default cfg; +`, ], ])( 'adds the plugin and enables source maps generation (%s)', diff --git a/test/utils/ast-utils.test.ts b/test/utils/ast-utils.test.ts index a52546b68..98f057b9f 100644 --- a/test/utils/ast-utils.test.ts +++ b/test/utils/ast-utils.test.ts @@ -3,6 +3,7 @@ import { getOrSetObjectProperty, hasSentryContent, parseJsonC, + preserveTrailingNewline, printJsonC, setOrUpdateObjectProperty, } from '../../src/utils/ast-utils'; @@ -221,6 +222,38 @@ describe('setOrUpdateObjectProperty', () => { }); }); +describe('preserveTrailingNewline', () => { + it('adds trailing newline if original had one but generated does not', () => { + const original = 'const foo = 1;\n'; + const generated = 'const foo = 1;'; + expect(preserveTrailingNewline(original, generated)).toBe( + 'const foo = 1;\n', + ); + }); + + it('does not add trailing newline if original did not have one', () => { + const original = 'const foo = 1;'; + const generated = 'const foo = 1;'; + expect(preserveTrailingNewline(original, generated)).toBe('const foo = 1;'); + }); + + it('does not double trailing newline if both already have one', () => { + const original = 'const foo = 1;\n'; + const generated = 'const foo = 1;\n'; + expect(preserveTrailingNewline(original, generated)).toBe( + 'const foo = 1;\n', + ); + }); + + it('does not remove trailing newline if original did not have one but generated does', () => { + const original = 'const foo = 1;'; + const generated = 'const foo = 1;\n'; + expect(preserveTrailingNewline(original, generated)).toBe( + 'const foo = 1;\n', + ); + }); +}); + describe('parse and print JSON-C', () => { it.each([ ['simple JSON', "{'foo': 'bar'}"],