custompatch
Version:
Tool for patching buggy NPM packages instead of forking them
662 lines (650 loc) • 18.7 kB
JavaScript
// package.json
var version = "1.1.8";
// src/index.js
import { program as program2 } from "commander";
import fs6 from "node:fs";
import path7 from "node:path";
// src/ansiUtils.js
var ansiColors = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white"
];
function startColor(colorName, background = false) {
let idx = ansiColors.indexOf(colorName);
if (idx !== -1) {
return ansi(idx + (background ? 40 : 30));
}
idx = ansiColors.indexOf(colorName.replace("Bright", ""));
if (idx !== -1) {
return ansi(idx + (background ? 100 : 90));
}
return ansi(background ? 100 : 90);
}
function stopColor(background = false) {
return ansi(background ? 49 : 39);
}
function ansi(code) {
return "\x1B[" + code + "m";
}
function echo(...variableArguments) {
console.log.call(null, variableArguments.join(""));
}
// src/utils.js
import fs from "node:fs";
import path2 from "node:path";
// src/variables.js
import { program } from "commander";
import os from "node:os";
import path from "node:path";
var curDir = process.cwd();
var tmpDir = os.tmpdir();
var patchDir = path.join(curDir, "patches");
program.name("custompatch").usage("[options] [packageName ...]").version(version).description(
'Tool for patching buggy NPM packages instead of forking them.\nWhen invoked without arguments - apply all patches from the "patches" folder.\nIf one or more package names are specified - create a patch for the given NPM package (already patched by you in your "node_modules" folder) and save it inside "patches" folder.'
).option(
"-a, --all",
'Include "package.json" files in the patch, by default these are ignored'
).option(
"-r, --reverse",
"Reverse the patch(es) instead of applying them"
).option(
"-p, --patch",
"Apply the patch(es) to the specified package(s) instead of all patches"
);
program.parse();
var programOptions = program.opts();
// src/utils.js
function removeBuildMetadataFromVersion(version2) {
const plusPos = version2.indexOf("+");
if (plusPos === -1) {
return version2;
}
return version2.substring(0, plusPos);
}
function getScopelessName(name) {
if (name[0] !== "@") {
return name;
}
return name.split("/")[1];
}
function hasPatches() {
if (!fs.existsSync(patchDir)) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Missing ",
startColor("whiteBright"),
"patches",
stopColor(),
" folder - nothing to do"
);
process.exit(2);
}
return true;
}
function getConfig(pkgName) {
const folder = path2.join(curDir, "node_modules", pkgName);
const cfgName = path2.join(folder, "package.json");
if (!fs.existsSync(folder)) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
'Missing folder "',
startColor("whiteBright"),
"./node_modules/",
stopColor(),
startColor("greenBright"),
pkgName,
stopColor(),
'"'
);
return false;
}
try {
fs.accessSync(cfgName, fs.constants.R_OK);
} catch (e) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Can not read ",
startColor("whiteBright"),
'"package.json"',
stopColor(),
" for ",
startColor("greenBright"),
pkgName,
stopColor()
);
return false;
}
const pkgConfig = fs.readFileSync(cfgName, "utf8");
let cfg = {};
try {
cfg = JSON.parse(pkgConfig);
} catch (e) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Could not parse ",
startColor("whiteBright"),
'"package.json"',
stopColor(),
" - ",
startColor("redBright"),
e.message,
stopColor()
);
return false;
}
return cfg;
}
function isVersionSuitable(patchSemVer, packageSemVer) {
const oldVer = patchSemVer.split(".");
const newVer = packageSemVer.split(".");
if (+oldVer[0] < +newVer[0]) return true;
if (+oldVer[0] > +newVer[0]) return false;
if (+oldVer[1] < +newVer[1]) return true;
if (+oldVer[1] > +newVer[1]) return false;
return +oldVer[2] <= +newVer[2];
}
// src/fileUtils.js
import fs2 from "node:fs";
import path3 from "node:path";
function pathNormalize(pathName) {
return path3.normalize(
path3.sep === "/" ? pathName.replace(/\\/g, "/") : pathName.replace(/\//g, "\\\\")
);
}
function ensureDirectoryExists(dirPath) {
if (!fs2.existsSync(dirPath)) {
fs2.mkdirSync(dirPath, { recursive: true });
}
}
function readFileContent(filePath) {
try {
return fs2.readFileSync(filePath, "utf8");
} catch (err) {
let errorMessage;
if (err instanceof Error) {
errorMessage = err.message;
} else {
errorMessage = String(err);
}
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
`Failed to read file ${filePath} - ${errorMessage}`
);
return "";
}
}
function makePatchName(pkgName, version2) {
return pkgName.replace(/[\\\/]/g, "+") + "#" + version2 + ".patch";
}
function parsePatchName(filename) {
const pkg = filename.replace(".patch", "").split("#");
return {
pkgName: pkg[0].replace(/\+/g, path3.sep),
version: pkg[1]
};
}
// src/patchApplying.js
import fs3 from "node:fs";
import path4 from "node:path";
import { applyPatch, parsePatch, reversePatch } from "diff";
function readPatch(pkgName, version2, patchCounter, reversing) {
const packageName = pkgName.replace(/\+/g, path4.sep);
const cfg = getConfig(packageName);
if (cfg) {
echo(
"\n ",
patchCounter,
") ",
reversing ? "Reversing" : "Applying",
" patch for ",
startColor("magentaBright"),
pkgName,
stopColor(),
" ",
startColor("greenBright"),
version2,
stopColor(),
" onto ",
startColor("whiteBright"),
cfg.version,
stopColor()
);
if (!isVersionSuitable(version2, cfg.version)) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"The patch is for v",
startColor("greenBright"),
version2,
stopColor(),
" but you have installed ",
startColor("redBright"),
cfg.version,
stopColor()
);
} else {
if (version2 !== cfg.version) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"The patch for ",
startColor("greenBright"),
version2,
stopColor(),
" may not ",
reversing ? "reverse" : "apply",
" cleanly to the installed ",
startColor("redBright"),
cfg.version,
stopColor()
);
}
const patchFile = makePatchName(pkgName, version2);
const patch = fs3.readFileSync(path4.join(patchDir, patchFile), "utf8");
const chunks = parsePatch(patch);
chunks.forEach((chunk, subIndex) => {
const filePath = chunk.newFileName ?? chunk.oldFileName;
if (!filePath) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"A chunk has no file names for package ",
startColor("greenBright"),
pkgName,
stopColor()
);
chunk.success = false;
} else {
const normalizedPath = pathNormalize(filePath);
const fileName = path4.join(curDir, "node_modules", normalizedPath);
const fileContent = readFileContent(fileName);
if (reversing) {
echo(
"\n(",
patchCounter,
".",
1 + subIndex,
") ",
"Reversing chunk ",
startColor("greenBright"),
filePath,
stopColor()
);
const reversedPatchText = reversePatch(chunk);
const reversePatchedContent = applyPatch(fileContent, reversedPatchText);
if (reversePatchedContent === false) {
const patchedContent = applyPatch(fileContent, chunk);
if (patchedContent !== false) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Patch already reversed"
);
chunk.success = true;
} else {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Failed to reverse patch for ",
startColor("redBright"),
filePath,
stopColor()
);
chunk.success = false;
}
} else {
try {
fs3.writeFileSync(fileName, reversePatchedContent, "utf8");
chunk.success = true;
} catch (err) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Could not write the new content for chunk ",
startColor("greenBright"),
fileName,
stopColor(),
" = ",
startColor("redBright"),
err.message || err
);
chunk.success = false;
}
}
} else {
echo(
"\n(",
patchCounter,
".",
1 + subIndex,
") ",
"Applying chunk ",
startColor("greenBright"),
filePath,
stopColor()
);
const patchedContent = applyPatch(fileContent, chunk);
if (patchedContent === false) {
const reversedPatchText = reversePatch(chunk);
const reversePatchedContent = applyPatch(fileContent, reversedPatchText);
if (reversePatchedContent !== false) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Patch already applied"
);
chunk.success = true;
} else {
if (!fs3.existsSync(fileName)) {
chunk.success = false;
const folder = path4.dirname(fileName);
if (!fs3.existsSync(folder)) {
echo(
startColor("yellowBright"),
"WARNING: Folder ",
stopColor(),
startColor("redBright"),
path4.dirname(fileName),
stopColor(),
startColor("yellowBright"),
" does not exist - the patch is probably for older version",
stopColor()
);
}
} else {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Chunk failed - ",
startColor("redBright"),
cfg.version !== version2 ? " either already applied or for different version" : "probably already applied",
stopColor()
);
chunk.success = false;
}
}
} else {
try {
fs3.writeFileSync(fileName, patchedContent, "utf8");
chunk.success = true;
} catch (err) {
echo(
"Could not write the new content for chunk ",
startColor("greenBright"),
fileName,
stopColor(),
" = ",
startColor("redBright"),
err.message || err,
stopColor()
);
chunk.success = false;
}
}
}
}
});
const allChunks = chunks.every((chunk) => chunk.success);
const noneChunks = chunks.every((chunk) => !chunk.success);
echo(
"\nPatch for ",
startColor("magentaBright"),
pkgName,
stopColor(),
" was ",
startColor(allChunks ? "cyanBright" : noneChunks ? "redBright" : "yellow"),
allChunks ? "successfully" : noneChunks ? "not" : "partially",
stopColor(),
reversing ? " reversed" : " applied"
);
}
}
}
function applyPatches(packageNames = [], reversing = false) {
if (hasPatches()) {
const patchFiles = [];
fs3.readdirSync(patchDir).map((item) => {
if (!item.endsWith(".patch")) return;
const pkg = parsePatchName(item);
if (packageNames.length > 0 ? packageNames.includes(pkg.pkgName) : true) {
const dest = path4.join(curDir, "node_modules", pkg.pkgName);
if (!fs3.existsSync(dest)) {
echo(
startColor("yellowBright"),
"WARNING: ",
stopColor(),
"Package ",
startColor("whiteBright"),
pkg.pkgName,
stopColor(),
" is not installed - skipping this patch"
);
return;
}
patchFiles.push(pkg);
}
});
echo(
"Found ",
startColor("cyanBright"),
patchFiles.length,
stopColor(),
" patches"
);
if (packageNames.length > 0 && patchFiles.length !== packageNames.length) {
packageNames.filter((name) => !patchFiles.find((file) => file.pkgName !== name)).forEach((name) => {
echo(
"No patch was found for ",
startColor("cyanBright"),
name,
stopColor()
);
});
}
patchFiles.forEach((item, index) => {
readPatch(item.pkgName, item.version, index + 1, reversing);
});
}
}
// src/npmUtils.js
import pacote from "pacote";
import fs4 from "node:fs";
import path5 from "node:path";
function npmTarballURL(pkgName, pkgVersion) {
const scopelessName = getScopelessName(pkgName);
return `https://registry.npmjs.org/${pkgName}/-/${scopelessName}-${removeBuildMetadataFromVersion(pkgVersion)}.tgz`;
}
function fetchPackage(pkgName, pkgVersion, callback) {
const url = npmTarballURL(pkgName, pkgVersion);
const dest = path5.join(tmpDir, pkgName);
echo(
"Fetching tarball of ",
startColor("whiteBright"),
pkgName,
stopColor(),
" from ",
startColor("green"),
url,
stopColor()
);
pacote.extract(url, dest).then(() => {
callback(pkgName, pkgVersion);
fs4.rm(dest, { recursive: true, force: true }, (err) => {
if (err) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Could not clean up the TEMP folder"
);
}
});
}).catch((err) => {
echo(
startColor("redBright"),
err.message,
stopColor()
);
});
}
// src/patchCreation.js
import fs5 from "node:fs";
import path6 from "node:path";
import { createTwoFilesPatch } from "diff";
function createPatches(packageNames) {
packageNames.forEach(makePatch);
}
function goodFileName(fn) {
const pattern = new RegExp("/", "g");
return fn.replace(pattern, "+");
}
function makePatchName2(pkgName, version2) {
return goodFileName(pkgName) + "#" + version2 + ".patch";
}
function createPatch(pkgName, pathname, patch) {
if (pathname === "package.json" && !programOptions.all) return;
const newFile = path6.join(curDir, "node_modules", pkgName, pathname);
const oldFile = path6.join(tmpDir, pkgName, pathname);
const oldStr = fs5.existsSync(oldFile) ? fs5.readFileSync(oldFile, "utf8") : "";
const newStr = fs5.readFileSync(newFile, "utf8");
if (oldStr !== newStr) patch.write(createTwoFilesPatch(oldFile.replace(tmpDir, ""), newFile.replace(path6.join(curDir, "node_modules"), ""), oldStr, newStr));
}
function scanFiles(pkgName, src, patch) {
const files = fs5.readdirSync(path6.join(curDir, "node_modules", pkgName, src));
files.forEach((item) => {
if (item === "node_modules") return;
const pathname = path6.join(src, item);
const stat = fs5.lstatSync(path6.join(curDir, "node_modules", pkgName, pathname));
if (stat.isDirectory()) scanFiles(pkgName, pathname, patch);
else createPatch(pkgName, pathname, patch);
});
}
function comparePackages(pkgName, version2) {
const patchFile = makePatchName2(pkgName, version2);
ensureDirectoryExists(patchDir);
const stream = fs5.createWriteStream(path6.join(patchDir, patchFile));
stream.on("error", (err) => {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Could not write patch file ",
startColor("cyanBright"),
patchFile,
stopColor(),
" = ",
startColor("redBright"),
err.message || err,
stopColor()
);
});
stream.cork();
scanFiles(pkgName, "", stream);
stream.uncork();
if (!stream.write("")) {
stream.once("drain", () => stream.end());
} else {
stream.end();
}
echo(
"Successfully created ",
startColor("greenBright"),
patchFile,
stopColor()
);
}
function makePatch(pkgName) {
echo(
"Creating patch for: ",
startColor("magentaBright"),
pkgName,
stopColor()
);
const cfg = getConfig(pkgName);
if (cfg) {
fetchPackage(pkgName, cfg.version, comparePackages);
} else {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Could not find the ",
startColor("whiteBright"),
"URL",
stopColor(),
" for ",
startColor("greenBright"),
"tarball",
stopColor()
);
}
}
// src/index.js
if (!programOptions.version) {
echo(
startColor("whiteBright"),
"CustomPatch",
stopColor(),
" version ",
startColor("greenBright"),
version,
stopColor(),
"\n"
);
}
if (!fs6.existsSync(path7.join(curDir, "node_modules"))) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Missing ",
startColor("whiteBright"),
'"node_modules"',
stopColor(),
" folder"
);
process.exit(1);
}
if (programOptions.patch && programOptions.reverse) {
echo(
startColor("redBright"),
"ERROR: ",
stopColor(),
"Cannot use -p/--patch and -r/--reverse together."
);
process.exit(1);
}
if (programOptions.patch) {
applyPatches(program2.args);
} else if (programOptions.reverse) {
applyPatches(program2.args, true);
} else if (program2.args.length > 0) {
createPatches(program2.args);
} else {
applyPatches();
}