custompatch
Advanced tools
+661
| #!/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], | ||
| }; | ||
| } |
+74
| #!/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() | ||
| ); | ||
| } | ||
| } |
+145
| 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" | ||
| } | ||
| } |
+25
-6
@@ -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 @@ |
-472
| #!/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'); | ||
| }); | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
51547
135.56%3
-40%12
200%1589
263.62%124
18.1%2
-77.78%4
Infinity%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated