diff --git a/transforms/magic-redirect.ts b/transforms/magic-redirect.ts index 0463158..7333352 100644 --- a/transforms/magic-redirect.ts +++ b/transforms/magic-redirect.ts @@ -10,6 +10,7 @@ import { memberExpression, } from 'jscodeshift' import { getParsedFile } from '../utils/parse' +import { getOptions } from '../utils/recastOptions' import { recursiveParent } from '../utils/recursiveParent' const unifiedMagicString = (path: ASTPath, projectRequestName: string) => { @@ -51,5 +52,5 @@ export default function transformer(file: FileInfo, _api: API) { }) .map((path) => unifiedMagicString(path, recursiveParent(path.parentPath) || 'req')) - return parsedFile.toSource() + return parsedFile.toSource(getOptions(file.source)) } diff --git a/transforms/pluralized-methods.ts b/transforms/pluralized-methods.ts index e2ed072..14a295b 100644 --- a/transforms/pluralized-methods.ts +++ b/transforms/pluralized-methods.ts @@ -1,6 +1,7 @@ import type { API, FileInfo } from 'jscodeshift' import { Identifier, identifier } from 'jscodeshift' import { getParsedFile } from '../utils/parse' +import { getOptions } from '../utils/recastOptions' export default function transformer(file: FileInfo, _api: API): string { const parsedFile = getParsedFile(file) @@ -17,5 +18,5 @@ export default function transformer(file: FileInfo, _api: API): string { .replaceWith(() => identifier(plural)) } - return parsedFile.toSource() + return parsedFile.toSource(getOptions(file.source)) } diff --git a/transforms/req-param.ts b/transforms/req-param.ts index 2eb9d72..96d52ae 100644 --- a/transforms/req-param.ts +++ b/transforms/req-param.ts @@ -1,5 +1,6 @@ import type { API, FileInfo } from 'jscodeshift' import { CallExpression, identifier, memberExpression, withParser } from 'jscodeshift' +import { getOptions } from '../utils/recastOptions' import { recursiveParent } from '../utils/recursiveParent' export default function transformer(file: FileInfo, _api: API): string { @@ -40,5 +41,5 @@ export default function transformer(file: FileInfo, _api: API): string { return path }) - .toSource() + .toSource(getOptions(file.source)) } diff --git a/transforms/v4-deprecated-signatures.ts b/transforms/v4-deprecated-signatures.ts index 5800729..842b22c 100644 --- a/transforms/v4-deprecated-signatures.ts +++ b/transforms/v4-deprecated-signatures.ts @@ -1,5 +1,6 @@ import type { API, ASTPath, FileInfo } from 'jscodeshift' import { CallExpression, callExpression, identifier, memberExpression, withParser } from 'jscodeshift' +import { getOptions } from '../utils/recastOptions' import { recursiveParent } from '../utils/recursiveParent' const separateStatusAndBody = (path: ASTPath, calleePropertyName: string) => { @@ -153,5 +154,5 @@ export default function transformer(file: FileInfo, _api: API): string { return path }) - return parsedFile.toSource() + return parsedFile.toSource(getOptions(file.source)) } diff --git a/utils/__test__/recastOptions.spec.ts b/utils/__test__/recastOptions.spec.ts new file mode 100644 index 0000000..7047b87 --- /dev/null +++ b/utils/__test__/recastOptions.spec.ts @@ -0,0 +1,68 @@ +import { getOptions } from '../recastOptions' + +describe('recastOptions', () => { + describe('getOptions', () => { + it('should return Unix line terminator for code with only LF', () => { + const code = 'const a = 1\nconst b = 2\n' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + + it('should return Windows line terminator for code with only CRLF', () => { + const code = 'const a = 1\r\nconst b = 2\r\n' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\r\n' }) + }) + + it('should return Unix line terminator for code with mixed line terminators', () => { + const code = 'const a = 1\r\nconst b = 2\nconst c = 3\r\n' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + + it('should return Unix line terminator for code with no line terminators', () => { + const code = 'const a = 1' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + + it('should return Unix line terminator for empty string', () => { + const code = '' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + + it('should handle code with LF at beginning after CR', () => { + const code = '\r\nconst a = 1\nconst b = 2' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + + it('should handle single CRLF without other line breaks', () => { + const code = 'const a = 1\r\nconst b = 2' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\r\n' }) + }) + + it('should handle multiple CRLF without LF', () => { + const code = 'line1\r\nline2\r\nline3\r\n' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\r\n' }) + }) + + it('should detect LF even when preceded by CR in different context', () => { + const code = 'const str = "\\r"\nconst a = 1\r\n' + const result = getOptions(code) + + expect(result).toEqual({ lineTerminator: '\n' }) + }) + }) +}) diff --git a/utils/recastOptions.ts b/utils/recastOptions.ts new file mode 100644 index 0000000..f0c9130 --- /dev/null +++ b/utils/recastOptions.ts @@ -0,0 +1,17 @@ +/** + * By default, jscodeshift(recast) uses the line terminator of the OS the code runs on. + * This is often not desired, so we instead try to detect it from the input. + * If there is at least one Windows-style linebreak (CRLF) in the input and + * no Unix-style linebreak (LF), use that. In all other cases, use Unix-style (LF). + * @return '\n' or '\r\n' + */ +export function getOptions(code: string) { + return { lineTerminator: detectLineTerminator(code) } +} + +function detectLineTerminator(code: string) { + const hasCRLF = /\r\n/.test(code) + const hasLF = /[^\r]\n/.test(code) + + return hasCRLF && !hasLF ? '\r\n' : '\n' +}