Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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": patch
---

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