custompatch
Version:
Tool for patching buggy NPM packages instead of forking them
350 lines (340 loc) • 10.8 kB
JavaScript
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);
});
}
}