Socket
Book a DemoSign in
Socket

custompatch

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

custompatch - npm Package Compare versions

Comparing version
1.0.28
to
1.1.4
+661
index.mjs
#!/usr/bin/env node
// package.json
var version = "1.1.4";
// 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();
}
export const ansiColors = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white'
];
/**
*
* @param colorName {String}
* @param background {Boolean}
* @returns {string}
*/
export function startColor(colorName, background = false)
{
/**
* @type {number}
*/
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); // grey
}
/**
*
* @param background {Boolean}
* @returns {string}
*/
export function stopColor(background = false)
{
return ansi(background ? 49 : 39);
}
/**
*
* @param code {Number}
* @returns {string}
*/
function ansi(code)
{
return '\u001B[' + code + 'm';
}
/**
* Dumps the given one or more strings to the console by concatenating them first
* @param {...String} variableArguments
*/
export function echo(...variableArguments)
{
console.log.call(null, variableArguments.join(''));
}
import fs from 'node:fs';
import path from 'node:path';
import { echo, startColor, stopColor } from './ansiUtils';
/**
*
* @param pathName {String}
* @returns {string}
*/
export function pathNormalize(pathName)
{
return path.normalize(
path.sep === '/'
? pathName.replace(/\\/g, '/')
: pathName.replace(/\//g, '\\\\')
);
}
/**
*
* @param dirPath {String}
*/
export function ensureDirectoryExists(dirPath)
{
if (!fs.existsSync(dirPath))
{
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
*
* @param filePath {String}
* @returns {string}
*/
export function readFileContent(filePath)
{
try
{
return fs.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 '';
}
}
/**
* Generates valid filename for the patch from the given package name and version
* @param pkgName {String}
* @param version {String}
* @returns {string}
*/
export function makePatchName(pkgName, version)
{
return pkgName.replace(/[\\\/]/g, '+') + '#' + version + '.patch';
}
/**
* Splits the given patch filename into package name and version
* @param filename {String}
* @returns {{pkgName: String, version: String}}
*/
export function parsePatchName(filename)
{
const pkg = filename.replace('.patch','').split('#');
return {
pkgName: pkg[0].replace(/\+/g, path.sep),
version: pkg[1],
};
}
#!/usr/bin/env node
import { version } from '../package.json';
import { program } from 'commander';
import fs from 'node:fs';
import path from 'node:path';
import { applyPatches } from './patchApplying';
import { createPatches } from './patchCreation';
import { programOptions, curDir } from './variables';
import { echo, startColor, stopColor } from './ansiUtils';
// If the user asked just for the version - do not print a colored version of ours; Commander will print it black-and-white.
if (!programOptions.version)
{
echo(
startColor('whiteBright'),
'CustomPatch',
stopColor(),
' version ',
startColor('greenBright'),
version,
stopColor(),
'\n'
);
}
// If there is no node_modules inside the project folder - quit
if (!fs.existsSync(path.join(curDir, 'node_modules')))
{
echo(
startColor('redBright'),
'ERROR: ',
stopColor(),
'Missing ',
startColor('whiteBright'),
'"node_modules"',
stopColor(),
' folder'
);
process.exit(1);
}
// Enforce that -p and -r are not used together
if (programOptions.patch && programOptions.reverse)
{
echo(
startColor('redBright'),
'ERROR: ',
stopColor(),
'Cannot use -p/--patch and -r/--reverse together.'
);
process.exit(1);
}
if (programOptions.patch)
{
// we want to apply patches to one or more specific packages
applyPatches(program.args);
}
else if (programOptions.reverse)
{
// we want to reverse the patches of one or more specific packages (or all patches, if no package name is provided)
applyPatches(program.args, true);
}
else if (program.args.length > 0)
{
// we want to create patches for the given package names
createPatches(program.args);
}
else
{
// we want to apply all available patches to their corresponding packages
applyPatches();
}
import pacote from 'pacote';
import fs from 'node:fs';
import path from 'node:path';
import { echo, startColor, stopColor } from './ansiUtils';
import { getScopelessName, removeBuildMetadataFromVersion } from './utils';
import { tmpDir } from './variables';
/**
* Generate the download URL for the given package name
* @param pkgName {String}
* @param pkgVersion {String}
* @returns {String}
*/
export function npmTarballURL(pkgName, pkgVersion)
{
const scopelessName = getScopelessName(pkgName);
return `https://registry.npmjs.org/${pkgName}/-/${scopelessName}-${removeBuildMetadataFromVersion(pkgVersion)}.tgz`;
}
/**
* Download the TAR-ball of the given NPM package
* @param pkgName {String}
* @param pkgVersion {String}
* @param callback {Function}
*/
export function fetchPackage(pkgName, pkgVersion, callback)
{
const url = npmTarballURL(pkgName, pkgVersion);
const dest = path.join(tmpDir, pkgName);
echo(
'Fetching tarball of ',
startColor('whiteBright'),
pkgName,
stopColor(),
' from ',
startColor('green'),
url,
stopColor()
);
pacote.extract(url, dest).then(() =>
{
callback(pkgName, pkgVersion);
fs.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()
);
});
}
import { echo, startColor, stopColor } from './ansiUtils';
import { hasPatches, getConfig, isVersionSuitable } from './utils';
import { parsePatchName, makePatchName, pathNormalize, readFileContent } from './fileUtils';
import { curDir, patchDir } from './variables';
import fs from 'node:fs';
import path from 'node:path';
import { applyPatch, parsePatch, reversePatch } from 'diff';
/**
* fetch original NPM package, then read the patch file and try to apply or reverse chunks
* @param pkgName {String}
* @param version {String}
* @param patchCounter {Number} Sequential number of the current patch when multiple (begins from one)
* @param reversing {Boolean}
*/
function readPatch(pkgName, version, patchCounter, reversing)
{
const packageName = pkgName.replace(/\+/g, path.sep);
const cfg = getConfig(packageName);
if(cfg)
{
echo('\n ',
patchCounter,
') ',
reversing ? 'Reversing' : 'Applying',
' patch for ',
startColor('magentaBright'),
pkgName,
stopColor(),
' ',
startColor('greenBright'),
version,
stopColor(),
' onto ',
startColor('whiteBright'),
cfg.version,
stopColor()
);
if (!isVersionSuitable(version, cfg.version))
{
echo(
startColor('yellowBright'),
'WARNING: ',
stopColor(),
'The patch is for v',
startColor('greenBright'),
version,
stopColor(),
' but you have installed ',
startColor('redBright'),
cfg.version,
stopColor()
);
}
else
{
if (version !== cfg.version)
{
echo(
startColor('yellowBright'),
'WARNING: ',
stopColor(),
'The patch for ',
startColor('greenBright'),
version,
stopColor(),
' may not ',
reversing ? 'reverse' : 'apply',
' cleanly to the installed ',
startColor('redBright'),
cfg.version,
stopColor()
);
}
const patchFile = makePatchName(pkgName, version);
const patch = fs.readFileSync(path.join(patchDir, patchFile), 'utf8');
const chunks = parsePatch(patch);
chunks.forEach((chunk, subIndex) =>
{
// Ensure that we have a valid file name
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 = path.join(curDir, 'node_modules', normalizedPath);
const fileContent = readFileContent(fileName);
if (reversing)
{
echo(
'\n(',
patchCounter,
'.',
(1 + subIndex),
') ',
'Reversing chunk ',
startColor('greenBright'),
filePath,
stopColor()
);
// Reverse the patch
const reversedPatchText = reversePatch(chunk);
const reversePatchedContent = applyPatch(fileContent, reversedPatchText);
if (reversePatchedContent === false)
{
// Failed to reverse the patch
// Attempt to apply the original patch to check if it's already reversed
const patchedContent = applyPatch(fileContent, chunk);
if (patchedContent !== false)
{
// Patch is already reversed
echo(
startColor('yellowBright'),
'WARNING: ',
stopColor(),
'Patch already reversed',
);
chunk.success = true;
}
else
{
// Patch failed for other reasons
echo(
startColor('yellowBright'),
'WARNING: ',
stopColor(),
'Failed to reverse patch for ',
startColor('redBright'),
filePath,
stopColor()
);
chunk.success = false;
}
}
else
{
try
{
fs.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()
);
// Apply the patch
const patchedContent = applyPatch(fileContent, chunk);
if (patchedContent === false)
{
// Failed to apply patch normally
// Try applying the reversed patch to check if already applied
const reversedPatchText = reversePatch(chunk);
const reversePatchedContent = applyPatch(fileContent, reversedPatchText);
if (reversePatchedContent !== false)
{
// The patch was already applied
echo(
startColor('yellowBright'),
'WARNING: ',
stopColor(),
'Patch already applied',
);
chunk.success = true;
}
else
{
// Patch failed for other reasons
if (!fs.existsSync(fileName))
{
chunk.success = false;
const folder = path.dirname(fileName);
if (!fs.existsSync(folder))
{
echo(
startColor('yellowBright'),
'WARNING: Folder ',
stopColor(),
startColor('redBright'),
path.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 !== version ? ' either already applied or for different version' : 'probably already applied',
stopColor()
);
chunk.success = false;
}
}
}
else
{
try
{
fs.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'
);
}
}
}
/**
* Apply all found patches or only those for the specified packages. Reversing requires specifying the package names.
* @param packageNames {Array<String>}
* @param reversing {Boolean}
*/
export function applyPatches(packageNames = [], reversing = false)
{
if (hasPatches())
{
// apply patches
const patchFiles = [];
fs.readdirSync(patchDir).map(item =>
{
if(!item.endsWith('.patch')) return;
const pkg = parsePatchName(item);
if (packageNames.length > 0 ? packageNames.includes(pkg.pkgName) : true)
{
const dest = path.join(curDir, 'node_modules', pkg.pkgName);
if(!fs.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);
});
}
}
import { echo, startColor, stopColor } from './ansiUtils';
import { curDir, patchDir, tmpDir, programOptions } from './variables';
import { fetchPackage } from './npmUtils';
import { getConfig } from './utils';
import { ensureDirectoryExists } from './fileUtils';
import fs from 'node:fs';
import path from 'node:path';
import { createTwoFilesPatch } from 'diff';
/**
*
* @param packageNames {Array<String>}
*/
export function createPatches(packageNames)
{
// create patch for each of the provided package names
packageNames.forEach(makePatch);
}
/**
* replace directory separators in package names like "@vue/cli" or "@babel/register"
* @param fn {String}
* @returns {String}
*/
function goodFileName(fn)
{
const pattern = new RegExp('/', 'g');
return fn.replace(pattern, '+');
}
function makePatchName(pkgName, version)
{
return goodFileName(pkgName) + '#' + version + '.patch';
}
/**
* @constant
* @type {import('@types/node').WriteStream} WriteStream
*/
/**
* compare a modified file from the local package to its original counterpart - if they are different, create a patch and append it to the patch stream
* @param pkgName {String}
* @param pathname {String}
* @param patch {WriteStream}
*/
function createPatch(pkgName, pathname, patch)
{
if(pathname === 'package.json' && !programOptions.all) return; // skip "package.json" - comparison is not reliable because NPM reorders keys and also appends many system keys (whose names begin with underscore)
const newFile = path.join(curDir, 'node_modules', pkgName, pathname);
const oldFile = path.join(tmpDir, pkgName, pathname);
const oldStr = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, 'utf8') : '';
const newStr = fs.readFileSync(newFile, 'utf8');
if(oldStr !== newStr) patch.write(createTwoFilesPatch(oldFile.replace(tmpDir,''), newFile.replace(path.join(curDir, 'node_modules'),''), oldStr, newStr));
}
/**
* recursively enumerate all files in the locally installed package
* @param pkgName {String}
* @param src {String}
* @param patch {WriteStream}
*/
function scanFiles(pkgName, src, patch)
{
const files = fs.readdirSync(path.join(curDir, 'node_modules', pkgName, src));
files.forEach(item =>
{
if (item === 'node_modules') return;
const pathname = path.join(src, item);
const stat = fs.lstatSync(path.join(curDir, 'node_modules', pkgName, pathname));
if(stat.isDirectory()) scanFiles(pkgName, pathname, patch);
else createPatch(pkgName, pathname, patch);
});
}
/**
* compare all files from the locally modified package with their original content
* @param pkgName {String}
* @param version {String}
*/
function comparePackages(pkgName, version)
{
const patchFile = makePatchName(pkgName, version);
// Ensure the patches directory exists
ensureDirectoryExists(patchDir);
const stream = fs.createWriteStream(path.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();
// Handle 'drain' event if necessary
if (!stream.write(''))
{
stream.once('drain', () => stream.end());
}
else
{
stream.end();
}
echo(
'Successfully created ',
startColor('greenBright'),
patchFile,
stopColor()
);
}
/**
* build a patch for the given package
* @param pkgName {String}
*/
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()
);
}
}
import fs from 'node:fs';
import path from 'node:path';
import { echo, startColor, stopColor } from './ansiUtils';
import { curDir, patchDir } from './variables';
/**
*
* @param version {String}
* @returns {string}
*/
export function removeBuildMetadataFromVersion(version)
{
const plusPos = version.indexOf('+');
if (plusPos === -1)
{
return version;
}
return version.substring(0, plusPos);
}
/**
*
* @param name {String}
* @returns {string}
*/
export function getScopelessName(name)
{
if (name[0] !== '@')
{
return name;
}
return name.split('/')[1];
}
export 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;
}
/**
* returns FALSE on error, Package.JSON on success
* @param pkgName {String}
* @returns {Object|boolean}
*/
export function getConfig(pkgName)
{
const folder = path.join(curDir, 'node_modules', pkgName);
const cfgName = path.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;
}
/**
* return FALSE if packageSemVer is lower than patchSemVer
* @param patchSemVer {String}
* @param packageSemVer {String}
* @returns {boolean}
*/
export 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];
}
import { program } from 'commander';
import os from 'node:os';
import path from 'node:path';
import { version } from '../package.json';
/**
* @type String
*/
export const curDir = process.cwd();
/**
* @type String
*/
export const tmpDir = os.tmpdir();
/**
* @type String
*/
export const patchDir = path.join(curDir, 'patches');
program
.name('custompatch')
.usage('[options] [packageName ...]')
.version(version)
.description(
'Tool for patching buggy NPM packages instead of forking them.\n' +
'When invoked without arguments - apply all patches from the "patches" folder.\n' +
'If 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();
/**
* @type Object
*/
export const programOptions = program.opts();
+16
-8
{
"name": "custompatch",
"version": "1.0.28",
"version": "1.1.4",
"description": "Tool for patching buggy NPM packages instead of forking them",

@@ -9,6 +9,10 @@ "author": "IVO GELOV",

"repository": "github:tmcdos/custompatch",
"bin": "./index.js",
"bin": "./index.mjs",
"scripts": {
"build": "esbuild ./src/index.js --bundle --platform=node --format=esm --target=es2020,node16 --packages=external --outfile=index.mjs",
"prepare": "npm run build"
},
"engines": {
"node": ">= 16.1.0",
"npm": ">= 6.0.0"
"node": ">= 16.20.0",
"npm": ">= 9.6.7"
},

@@ -22,7 +26,11 @@ "keywords": [

"commander": "^12.1.0",
"diff": "^4.0.2",
"pacote": "^9.5.0",
"rimraf": "^2.6.3",
"upath": "^1.1.2"
"diff": "^7.0.0",
"pacote": "^18.0.6"
},
"devDependencies": {
"@types/diff": "^5.2.2",
"@types/node": "^22.5.5",
"@types/pacote": "^11.1.8",
"esbuild": "0.24.2"
}
}

@@ -15,4 +15,5 @@ # CustomPatch

- it does not require/depend on GIT
- it does not require a lockfile present when creating patches
When you have made a bugfix for one of your dependencies but the author/maintainers refuse to accept your PR - you do not need to fork the package.
When you have made a bugfix for one of your dependencies but the author/maintainers refuse to accept your PR - you do not need to fork the package.
Create a patch and seamlessly apply it locally for all your builds.

@@ -47,3 +48,3 @@

This will create a folder called `patches` inside your project and a file called `name-of-the-buggy-package#version.patch` in this folder.
This will create a folder called `patches` inside your project and a file called `name-of-the-buggy-package#version.patch` in this folder.
This file will be a unified diff between your fixed version of the dependency and its original code.

@@ -59,10 +60,23 @@

Run `custompatch` without arguments inside your project folder - it will apply all the patches from `patches` folder.
Currently there is no possibility to apply only the patch for a specific dependency. Also, it is not yet possible to undo a patch.
Perhaps the command-line utility `patch` can do the job but this has not been tested:
Run `custompatch` without arguments inside your project folder - it will apply all the patches from `patches` folder.
If you want to target specific patches you can use the `--patch (-p)` flag like so:
```bash
patch -p1 -i patches/package-name#2.5.16.patch
custompatch --patch [name-of-the-buggy-package]
```
### Reversing patches
To reverse all patches you can use the `--reverse (-r)` flag like so:
```bash
custompatch --reverse
```
To reverse specific patches you can use the `--reverse (-r)` flag like so:
```bash
custompatch --reverse [name-of-the-buggy-package]
```
## Benefits of patching over forking

@@ -75,2 +89,7 @@

## Patch filenames format
The filename of a patch will be composed as `packageName`, followed by the `#` symbol, followed by `packageSemVer` (and the extension is `.patch`)
If the `packageName` contains a symbol which collides with the directory separator of the current filesystem - these symbols will be replaced with `+`
## License

@@ -77,0 +96,0 @@

#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const os = require('os');
const diff = require('diff');
const { program } = require('commander');
const ownPkg = require('./package.json');
const npmFolder = path.join(npmDir(),'node_modules','npm','node_modules');
let pacote, rimraf;
try
{
pacote = require('pacote');
}
catch(e)
{
pacote = require(path.join(npmFolder,'pacote'));
}
try
{
rimraf = require('rimraf');
}
catch(e)
{
rimraf = require(path.join(npmFolder,'rimraf'));
}
// ANSI styles
const ansiColors = ['black','red','green','yellow','blue','magenta','cyan','white'];
function startColor(colorName, background)
{
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); // grey
}
function stopColor(background)
{
return ansi(background ? 49 : 39);
}
function ansi(code)
{
return '\u001B[' + code + 'm';
}
function echo()
{
console.log.apply(null,arguments);
}
const curDir = process.cwd();
const tmpDir = os.tmpdir();
const patchDir = path.join(curDir, 'patches');
const patchFiles = [];
const asyncResults = [];
program
.name('custompatch')
.usage('[options] [packageName ...]')
.version(ownPkg.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');
program.parse();
const programOptions = program.opts();
if(!programOptions.version) echo(startColor('whiteBright') + 'CustomPatch' + stopColor() + ' version ' + startColor('greenBright') + ownPkg.version + stopColor() + '\n');
if(!fs.existsSync(path.join(curDir, '/node_modules')))
{
echo(startColor('redBright') + 'ERROR: ' + stopColor() + 'Missing ' + startColor('whiteBright') + '"node_modules"' + stopColor() + ' folder');
process.exit(1);
}
if(program.args.length > 0)
{
// create patch for each of the provided package names
program.args.forEach(makePatch);
}
else
{
if(!fs.existsSync(patchDir))
{
echo(startColor('yellowBright') + 'WARNING: ' + stopColor() + 'Missing ' + startColor('whiteBright') + 'patches' + stopColor() + ' folder - nothing to do');
process.exit(2);
}
// apply patches
fs.readdirSync(patchDir).map(item =>
{
if(item.substr(-6) !== '.patch') return;
const pkg = item.replace('.patch','').split('#');
const packageName = pkg[0].replace(/\+/g, path.sep);
const dest = path.join(curDir, 'node_modules', packageName);
if(!fs.existsSync(dest))
{
echo(startColor('yellowBright') + 'WARNING: ' + stopColor() + 'Package ' + startColor('whiteBright') + packageName + stopColor() + ' is not installed - skipping this patch');
return;
}
patchFiles.push({
pkgName: pkg[0],
version: pkg[1],
});
});
echo('Found ' + startColor('cyanBright') + patchFiles.length + stopColor() + ' patches');
patchFiles.forEach(({ pkgName, version}) =>
{
readPatch(pkgName, version);
});
// wait until all chunks of all patches have been processed
setTimeout(waitForResults, 20);
}
// find the NPM folder because PACOTE package is relative to this folder (not a global package)
function npmDir()
{
if(process.platform.indexOf('win') === 0)
{
return process.env.APPDATA ? path.join(process.env.APPDATA, 'npm') : path.dirname(process.execPath);
}
else
{
// /usr/local/bin/node --> prefix=/usr/local
let prefix = path.dirname(path.dirname(process.execPath));
// destdir is respected only on Unix
if (process.env.DESTDIR) prefix = path.join(process.env.DESTDIR, prefix);
return prefix;
}
}
// returns FALSE on error, Package.JSON on success
function getConfig(pkgName)
{
const folder = path.join(curDir, 'node_modules', pkgName);
const cfgName = path.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());
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;
}
// build a tarball URL for the given package version
function npmTarballURL(pkgName, pkgVersion, registryURL)
{
let registry;
if (registryURL)
{
registry = registryURL.endsWith('/') ? registryURL : registryURL + '/';
}
else
{
registry = 'https://registry.npmjs.org/';
}
const scopelessName = getScopelessName(pkgName);
return `${registry}${pkgName}/-/${scopelessName}-${removeBuildMetadataFromVersion(pkgVersion)}.tgz`;
}
function removeBuildMetadataFromVersion (version)
{
const plusPos = version.indexOf('+');
if (plusPos === -1) return version;
return version.substring(0, plusPos);
}
function getScopelessName (name)
{
if (name[0] !== '@') return name;
return name.split('/')[1];
}
// build a patch for the given package
function makePatch(pkgName)
{
echo('Creating patch for: ' + startColor('magentaBright') + pkgName + stopColor());
const cfg = getConfig(pkgName);
if(cfg)
{
fetchPackage(pkgName, npmTarballURL(pkgName, cfg.version), cfg.version, comparePackages);
}
else
{
echo(startColor('redBright') + 'ERROR: ' + stopColor() + 'Could not find the ' + startColor('whiteBright') + 'URL' + stopColor() + ' for ' + startColor('greenBright') + 'tarball' + stopColor());
}
}
// download the tarball
function fetchPackage(pkgName, url, version, callback)
{
echo('Fetching tarball of ' + startColor('whiteBright') + pkgName + stopColor() + ' from ' + startColor('green') + url + stopColor());
const dest = path.join(tmpDir, pkgName);
pacote.extract(url, dest).then(function ()
{
callback(pkgName, version);
rimraf(dest, function (err)
{
if(err) echo(startColor('redBright') + 'ERROR: ' + stopColor() + 'Could not clean up the TEMP folder');
});
}).catch(function (err)
{
echo(startColor('redBright') + err.message + stopColor());
});
}
// replace directory separators in package names like "@vue/cli" or "@babel/register"
function goodFileName(fn)
{
const pattern = new RegExp('/', 'g');
return fn.replace(pattern, '+');
}
function makePatchName(pkgName, version)
{
return goodFileName(pkgName) + '#' + version + '.patch';
}
// compare all files from the locally modified package with their original content
function comparePackages(pkgName, version)
{
const patchFile = makePatchName(pkgName, version);
const stream = fs.createWriteStream(path.join(patchDir, patchFile));
stream.cork();
scanFiles(pkgName, '', stream);
stream.end();
echo('Successfully created ' + startColor('greenBright') + patchFile + stopColor());
}
// recursively enumerate all files in the locally installed package
function scanFiles(pkgName, src, patch)
{
const files = fs.readdirSync(path.join(curDir, 'node_modules', pkgName, src));
files.forEach(item =>
{
if (item === 'node_modules') return;
const pathname = path.join(src, item);
const stat = fs.lstatSync(path.join(curDir, 'node_modules', pkgName, pathname));
if(stat.isDirectory()) scanFiles(pkgName, pathname, patch);
else createPatch(pkgName, pathname, patch);
});
}
// compare a modified file from the local package to its original counterpart - if they are different, create a patch and append it to the patch stream
function createPatch(pkgName, pathname, patch)
{
const newFile = path.join(curDir, 'node_modules', pkgName, pathname);
const oldFile = path.join(tmpDir, pkgName, pathname);
let oldStr = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, 'utf8') : '';
let newStr = fs.readFileSync(newFile, 'utf8');
if(pathname === 'package.json' && !programOptions.all) return; // skip "package.json" - comparison is not reliable because NPM reorders keys and also appends many system keys (whose names begin with underscore)
/*
{
let oldJson = {}, newJson = {};
try
{
oldJson = JSON.parse(oldStr);
newJson = JSON.parse(newStr);
}
catch(e)
{
echo(startColor('redBright') + 'ERROR: ' + stopColor() + 'Could not parse ' + startColor('green') + 'package.json' + stopColor() + ' = ' + startColor('redBright') + e.message + stopColor());
return;
}
// remove all keys which start with underscore
let key;
for(key in oldJson)
if(key[0] === '_') delete oldJson[key];
for(key in newJson)
if(key[0] === '_') delete newJson[key];
// sort the keys
let oldSorted = {}, newSorted = {};
Object.keys(oldJson).sort().forEach(function(key)
{
oldSorted[key] = oldJson[key];
});
Object.keys(newJson).sort().forEach(function(key)
{
newSorted[key] = newJson[key];
});
oldStr = JSON.stringify(oldSorted, null, 2);
newStr = JSON.stringify(newSorted, null, 2);
}
*/
if(oldStr !== newStr) patch.write(diff.createTwoFilesPatch(oldFile.replace(tmpDir,''), newFile.replace(path.join(curDir, 'node_modules'),''), oldStr, newStr));
}
// fetch original NPM package, then read the patch file and try to apply hunks
function readPatch(pkgName, version)
{
const packageName = pkgName.replace(/\+/g, path.sep);
const cfg = getConfig(packageName);
if(cfg)
{
const patchFile = pkgName + '#' + version + '.patch';
const patch = fs.readFileSync(path.join(patchDir, patchFile),'utf8');
const chunks = [];
diff.applyPatches(patch,
{
loadFile: loadFile,
patched: (info, content, callback) =>
{
chunks.push({
packageName: pkgName,
packageVersion: cfg.version,
patchVersion: version,
chunkInfo: info,
newContent: content,
});
callback();
},
complete: (err) =>
{
asyncResults.push(chunks);
},
});
}
else
{
asyncResults.push([]);
}
}
// return FALSE if packageSemVer is lower than patchSemVer
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];
}
function loadFile(info, callback)
{
/*
info =
{
index: '\vue2-dragula\dist\vue-dragula.js',
oldFileName: '\vue2-dragula\dist\vue-dragula.js',
oldHeader: '',
newFileName: '\vue2-dragula\dist\vue-dragula.js',
newHeader: '',
hunks: [object1, object2, ...]
}
*/
const oldName = path.join(curDir, 'node_modules', pathNormalize(info.index));
if(fs.existsSync(oldName))
{
// read the original file
fs.readFile(oldName, 'utf8', callback);
}
else
{
callback(null, ''); // old file does not exist - i.e. it is empty
}
}
function pathNormalize(pathName)
{
return path.normalize(path.sep === '/' ? pathName.replace(/\\/g, '/') : pathName.replace(/\//g,'\\\\'));
}
function waitForResults()
{
if (asyncResults.length < patchFiles.length) setTimeout(waitForResults, 20);
else
{
const tree = {};
asyncResults.flat().forEach(item =>
{
if (!item) return;
if (!tree[item.packageName])
{
tree[item.packageName] = {
packageName: item.packageName,
packageVersion: item.packageVersion,
patchVersion: item.patchVersion,
chunks: [],
};
}
tree[item.packageName].chunks.push({
chunkInfo: item.chunkInfo,
newContent: item.newContent,
});
});
Object.values(tree).forEach((pkg, index) =>
{
echo('\n ' + (1 + index) + ') Applying patch for ' + startColor('magentaBright') + pkg.packageName + stopColor() + ' ' + startColor('greenBright') + pkg.patchVersion + stopColor());
if (!isVersionSuitable(pkg.patchVersion, pkg.packageVersion))
{
echo(startColor('yellowBright') + 'WARNING: ' + stopColor() + 'The patch is for v' + startColor('greenBright') + pkg.patchVersion + stopColor()
+ ' but you have installed ' + startColor('redBright') + pkg.packageVersion + stopColor());
return;
}
if(pkg.packageVersion !== pkg.patchVersion)
{
echo(startColor('yellowBright') + 'WARNING: ' + stopColor() + 'The patch for ' + startColor('greenBright') + pkg.patchVersion + stopColor()
+ ' may not apply cleanly to the installed ' + startColor('redBright') + pkg.packageVersion + stopColor());
}
pkg.chunks.forEach((chunk, subIndex) =>
{
echo('\n(' + (1 + index) + '.' + (1 + subIndex) + ') Patching chunk ' + startColor('greenBright') + pathNormalize(chunk.chunkInfo.index) + stopColor());
chunk.success = true;
// replace original file with the patched content
if(chunk.newContent !== false)
{
try
{
fs.writeFileSync(path.join(curDir, 'node_modules', pathNormalize(chunk.chunkInfo.index)), chunk.newContent, 'utf8');
}
catch(err)
{
echo('Could not write the new content for chunk ' + startColor('greenBright') + pathNormalize(chunk.chunkInfo.index) + stopColor() + ' = ' + startColor('redBright') + err + stopColor());
chunk.success = false;
}
}
else
{
chunk.success = false;
const oldName = path.join(curDir, 'node_modules', pathNormalize(chunk.chunkInfo.index));
if(!fs.existsSync(oldName))
{
const folder = path.dirname(oldName);
if (!fs.existsSync(folder))
{
echo(startColor('yellowBright') + 'WARNING: Folder ' + stopColor() + startColor('redBright') + path.dirname(pathNormalize(chunk.chunkInfo.index)) + stopColor() + startColor('yellowBright') + ' does not exist - the patch is probably for older version');
return;
}
}
echo(startColor('yellowBright') + 'WARNING: ' + stopColor() + 'Chunk failed - ' + startColor('redBright') + ' either already applied or for different version' + stopColor());
}
});
const allChunks = pkg.chunks.every(chunk => chunk.success);
const noneChunks = pkg.chunks.every(chunk => !chunk.success);
echo('\nPatch for ' + startColor('magentaBright') + pkg.packageName + stopColor() + ' was '
+ startColor(allChunks ? 'cyanBright' : noneChunks ? 'redBright' : 'yellow')
+ (allChunks ? 'successfully' : noneChunks ? 'not' : 'partially') + stopColor() + ' applied');
});
}
}