Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-feet-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@manypkg/cli": minor
---

Keep detected line endings flavor of `package.json` files on Windows when updating those files
80 changes: 68 additions & 12 deletions packages/cli/src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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");
Expand All @@ -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;
}
14 changes: 9 additions & 5 deletions packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down