diff --git a/.changeset/clever-feet-move.md b/.changeset/clever-feet-move.md new file mode 100644 index 00000000..f8a9c6c8 --- /dev/null +++ b/.changeset/clever-feet-move.md @@ -0,0 +1,5 @@ +--- +"@manypkg/cli": patch +--- + +Keep detected line endings flavor of `package.json` files on Windows when updating those files diff --git a/packages/cli/src/run.test.ts b/packages/cli/src/run.test.ts index e4f661d1..c145565a 100644 --- a/packages/cli/src/run.test.ts +++ b/packages/cli/src/run.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest"; import fixturez from "fixturez"; import stripAnsi from "strip-ansi"; import { exec } from "tinyexec"; +import fs from "node:fs"; +import path from "node:path"; const f = fixturez(__dirname); @@ -28,18 +30,11 @@ describe("Run command", () => { ])( 'should execute "%s %s" and exit with %i', async (arg0, arg1, expectedExitCode) => { - const { exitCode, stdout, stderr } = await exec( - "node", - [require.resolve("../bin.js"), "run", arg0, arg1], - { - nodeOptions: { - cwd: f.find("basic-with-scripts"), - env: { - ...process.env, - NODE_OPTIONS: "--experimental-strip-types", - }, - }, - } + const { exitCode, stdout, stderr } = await executeBin( + f.find("basic-with-scripts"), + "run", + arg0, + arg1 ); expect(exitCode).toBe(expectedExitCode); expect(stripAnsi(stdout.toString())).toMatchSnapshot("stdout"); @@ -49,3 +44,64 @@ describe("Run command", () => { } ); }); + +describe("Fix command", () => { + it.each([ + ["package-one", "fix", "lf", 0], + ["package-one", "fix", "crlf", 0], + ] as const satisfies [string, string, LineEndings, number][])( + 'should execute "%s %s" without changing line endings from %s', + async (arg0, arg1, sourceLineEnding, expectedExitCode) => { + // arrange + const temp = f.copy("basic-with-scripts"); + const filePath = path.join(temp, "package.json"); + convertFileLineEndings(filePath, sourceLineEnding); + // act + const { exitCode } = await executeBin(temp, "fix", arg0, arg1); + // assert + expect(exitCode).toBe(expectedExitCode); + const fixedPackageFile = fs.readFileSync(filePath, "utf8"); + f.cleanup(); + expect(detectLineEndings(fixedPackageFile)).toBe(sourceLineEnding); + } + ); +}); + +type LineEndings = "crlf" | "lf"; + +function convertFileLineEndings(path: string, targetLineEnding: LineEndings) { + let file = fs.readFileSync(path, "utf8"); + // detect mixed line endings + if ( + file.includes("\r\n") && + (file.match(/\r\n/g) || []).length !== (file.match(/\n/g) || []).length + ) { + throw new Error("mixed line endings in fixture file: " + path); + } + // if the line endings match, we don't need to convert the file + if (file.includes("\r\n") === (targetLineEnding === "crlf")) { + return; + } + const sourceLineEndingText = targetLineEnding === "crlf" ? "\n" : "\r\n"; + const targetLineEndingText = targetLineEnding === "crlf" ? "\r\n" : "\r\n"; + fs.writeFileSync( + path, + file.replaceAll(sourceLineEndingText, targetLineEndingText) + ); +} + +function executeBin(path: string, command: string, ...args: string[]) { + return exec("node", [require.resolve("../bin.js"), command, ...args], { + nodeOptions: { + cwd: path, + env: { + ...process.env, + NODE_OPTIONS: "--experimental-strip-types", + }, + }, + }); +} + +function detectLineEndings(content: string) { + return (content.includes("\r\n") ? "crlf" : "lf") satisfies LineEndings; +} diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index c3db0307..401f26ef 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -7,11 +7,15 @@ import detectIndent from "detect-indent"; export async function writePackage(pkg: Package) { let pkgRaw = await fs.readFile(path.join(pkg.dir, "package.json"), "utf-8"); let indent = detectIndent(pkgRaw).indent || " "; - return fs.writeFile( - path.join(pkg.dir, "package.json"), - JSON.stringify(pkg.packageJson, null, indent) + - (pkgRaw.endsWith("\n") ? "\n" : "") - ); + // Determine original EOL style and whether there was a trailing newline + const eol = pkgRaw.includes("\r\n") ? "\r\n" : "\n"; + // Stringify and then normalize EOLs to match the original file + let json = JSON.stringify(pkg.packageJson, null, indent); + json = eol !== "\n" ? json.replace(/\n/g, eol) : json; + if (pkgRaw.endsWith("\n") /* true for both LF and CRLF */) { + json += eol; + } + return fs.writeFile(path.join(pkg.dir, "package.json"), json); } export async function install(toolType: string, cwd: string) {