diff --git a/integration-tests/append-patches/__snapshots__/append-patches.test.ts.snap b/integration-tests/append-patches/__snapshots__/append-patches.test.ts.snap new file mode 100644 index 00000000..54a74835 --- /dev/null +++ b/integration-tests/append-patches/__snapshots__/append-patches.test.ts.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test append-patches: 00: basic patch file 1`] = ` +"SNAPSHOT: basic patch file +left-pad+1.3.0.patch +END SNAPSHOT" +`; + +exports[`Test append-patches: 01: after appending a patch file 1`] = ` +"SNAPSHOT: after appending a patch file +left-pad+1.3.0+001+initial.patch +left-pad+1.3.0+002+MillionDollars.patch +END SNAPSHOT" +`; + +exports[`Test append-patches: 02: the second patch file should go from patch-package to a million dollars 1`] = ` +"SNAPSHOT: the second patch file should go from patch-package to a million dollars +diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js +index a409e14..73d2a7c 100644 +--- a/node_modules/left-pad/index.js ++++ b/node_modules/left-pad/index.js +@@ -3,7 +3,7 @@ + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ +-'use patch-package'; ++'use a million dollars'; + module.exports = leftPad; + + var cache = [ +END SNAPSHOT" +`; + +exports[`Test append-patches: 03: creating a first patch file with --append 1`] = ` +"SNAPSHOT: creating a first patch file with --append +left-pad+1.3.0+001+FirstPatch.patch +END SNAPSHOT" +`; + +exports[`Test append-patches: 04: the squashed patch file should go from use strict to a million dollars 1`] = ` +"SNAPSHOT: the squashed patch file should go from use strict to a million dollars +diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js +index e90aec3..73d2a7c 100644 +--- a/node_modules/left-pad/index.js ++++ b/node_modules/left-pad/index.js +@@ -3,7 +3,7 @@ + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ +-'use strict'; ++'use a million dollars'; + module.exports = leftPad; + + var cache = [ +END SNAPSHOT" +`; + +exports[`Test append-patches: 05: after appending a billion dollars 1`] = ` +"SNAPSHOT: after appending a billion dollars +left-pad+1.3.0+001+FirstPatch.patch +left-pad+1.3.0+002+BillionDollars.patch +END SNAPSHOT" +`; + +exports[`Test append-patches: 06: after updating the appended patch file to a TRILLION dollars 1`] = ` +"SNAPSHOT: after updating the appended patch file to a TRILLION dollars +diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js +index 73d2a7c..f53ea10 100644 +--- a/node_modules/left-pad/index.js ++++ b/node_modules/left-pad/index.js +@@ -3,7 +3,7 @@ + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ +-'use a million dollars'; ++'use a trillion dollars'; + module.exports = leftPad; + + var cache = [ +END SNAPSHOT" +`; + +exports[`Test append-patches: 07: patch-package fails when a patch in the sequence is invalid 1`] = ` +"SNAPSHOT: patch-package fails when a patch in the sequence is invalid +Failed to apply patch left-pad+1.3.0+001+FirstPatch.patch to left-pad +END SNAPSHOT" +`; diff --git a/integration-tests/append-patches/append-patches.sh b/integration-tests/append-patches/append-patches.sh new file mode 100755 index 00000000..d168b9d6 --- /dev/null +++ b/integration-tests/append-patches/append-patches.sh @@ -0,0 +1,74 @@ +# make sure errors stop the script +set -e + +npm install + +echo "add patch-package" +npm add $1 +alias patch-package=./node_modules/.bin/patch-package + +function replace() { + npx replace "$1" "$2" node_modules/left-pad/index.js +} + +echo "making an initial patch file does not add a sequence number to the file by default" +replace 'use strict' 'use patch-package' + +patch-package left-pad + +echo "SNAPSHOT: basic patch file" +ls patches +echo "END SNAPSHOT" + +echo "using --apend creates a patch file with a sequence number and updates the original patch file" + +replace 'use patch-package' 'use a million dollars' + +patch-package left-pad --append 'MillionDollars' + +echo "SNAPSHOT: after appending a patch file" +ls patches +echo "END SNAPSHOT" + +echo "SNAPSHOT: the second patch file should go from patch-package to a million dollars" +cat patches/left-pad*MillionDollars.patch +echo "END SNAPSHOT" + +echo "we can squash the patches together by deleting the patch files" +rm patches/left-pad*patch + +patch-package left-pad --append 'FirstPatch' + +echo "SNAPSHOT: creating a first patch file with --append" +ls patches +echo "END SNAPSHOT" + +echo "SNAPSHOT: the squashed patch file should go from use strict to a million dollars" +cat patches/left-pad*FirstPatch.patch +echo "END SNAPSHOT" + +echo "i can update an appended patch file" + +replace 'use a million dollars' 'use a billion dollars' + +patch-package left-pad --append 'BillionDollars' + +echo "SNAPSHOT: after appending a billion dollars" +ls patches +echo "END SNAPSHOT" + +replace 'use a billion dollars' 'use a trillion dollars' +patch-package left-pad + +echo "SNAPSHOT: after updating the appended patch file to a TRILLION dollars" +cat patches/left-pad*BillionDollars.patch +echo "END SNAPSHOT" + +echo "if one of the patches in the sequence is invalid, the sequence is not applied" +npx replace 'use strict' 'use bananas' patches/*FirstPatch.patch + +(>&2 echo "SNAPSHOT: patch-package fails when a patch in the sequence is invalid") +if patch-package left-pad --append 'Bananas' ; then + exit 1 +fi +(>&2 echo "END SNAPSHOT") \ No newline at end of file diff --git a/integration-tests/append-patches/append-patches.test.ts b/integration-tests/append-patches/append-patches.test.ts new file mode 100644 index 00000000..ddc8e190 --- /dev/null +++ b/integration-tests/append-patches/append-patches.test.ts @@ -0,0 +1,5 @@ +import { runIntegrationTest } from "../runIntegrationTest" +runIntegrationTest({ + projectName: "append-patches", + shouldProduceSnapshots: true, +}) diff --git a/integration-tests/append-patches/package-lock.json b/integration-tests/append-patches/package-lock.json new file mode 100644 index 00000000..a2fe7182 --- /dev/null +++ b/integration-tests/append-patches/package-lock.json @@ -0,0 +1,381 @@ +{ + "name": "append-patches", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "append-patches", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "left-pad": "^1.3.0", + "replace": "^1.2.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "deprecated": "use String.prototype.padStart()" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/replace": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", + "integrity": "sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==", + "dependencies": { + "chalk": "2.4.2", + "minimatch": "3.0.5", + "yargs": "^15.3.1" + }, + "bin": { + "replace": "bin/replace.js", + "search": "bin/search.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/integration-tests/append-patches/package.json b/integration-tests/append-patches/package.json new file mode 100644 index 00000000..d3361126 --- /dev/null +++ b/integration-tests/append-patches/package.json @@ -0,0 +1,12 @@ +{ + "name": "append-patches", + "version": "1.0.0", + "description": "integration test for patch-package", + "main": "index.js", + "author": "", + "license": "ISC", + "dependencies": { + "left-pad": "^1.3.0", + "replace": "^1.2.2" + } +} diff --git a/integration-tests/runIntegrationTest.ts b/integration-tests/runIntegrationTest.ts index 7708a450..cd226dda 100644 --- a/integration-tests/runIntegrationTest.ts +++ b/integration-tests/runIntegrationTest.ts @@ -69,12 +69,16 @@ export function runIntegrationTest({ expect(snapshots && snapshots.length).toBeTruthy() }) if (snapshots) { - snapshots.forEach((snapshot) => { + snapshots.forEach((snapshot, i) => { const snapshotDescriptionMatch = snapshot.match(/SNAPSHOT: (.*)/) if (snapshotDescriptionMatch) { - it(snapshotDescriptionMatch[1], () => { - expect(snapshot).toMatchSnapshot() - }) + it( + `${i.toString().padStart(2, "0")}: ` + + snapshotDescriptionMatch[1], + () => { + expect(snapshot).toMatchSnapshot() + }, + ) } else { throw new Error("bad snapshot format") } diff --git a/property-based-tests/executeTestCase.ts b/property-based-tests/executeTestCase.ts index 87d19a77..5a5ad6e6 100644 --- a/property-based-tests/executeTestCase.ts +++ b/property-based-tests/executeTestCase.ts @@ -1,5 +1,4 @@ import * as tmp from "tmp" -import * as path from "path" import { spawnSafeSync } from "../src/spawnSafe" import { executeEffects } from "../src/patch/apply" @@ -8,6 +7,7 @@ import { reversePatch } from "../src/patch/reverse" import { TestCase, Files } from "./testCases" import { appendFileSync, existsSync, writeFileSync } from "fs" +import { join, dirname } from "path" jest.mock("fs-extra", () => { let workingFiles: Files @@ -24,7 +24,7 @@ jest.mock("fs-extra", () => { setWorkingFiles, getWorkingFiles, ensureDirSync: jest.fn(), - readFileSync: jest.fn(path => getWorkingFiles()[path].contents), + readFileSync: jest.fn((path) => getWorkingFiles()[path].contents), writeFileSync: jest.fn( (path: string, contents: string, opts?: { mode?: number }) => { getWorkingFiles()[path] = { @@ -33,12 +33,12 @@ jest.mock("fs-extra", () => { } }, ), - unlinkSync: jest.fn(path => delete getWorkingFiles()[path]), + unlinkSync: jest.fn((path) => delete getWorkingFiles()[path]), moveSync: jest.fn((from, to) => { getWorkingFiles()[to] = getWorkingFiles()[from] delete getWorkingFiles()[from] }), - statSync: jest.fn(path => getWorkingFiles()[path]), + statSync: jest.fn((path) => getWorkingFiles()[path]), chmodSync: jest.fn((path, mode) => { const { contents } = getWorkingFiles()[path] getWorkingFiles()[path] = { contents, mode } @@ -49,10 +49,10 @@ jest.mock("fs-extra", () => { function writeFiles(cwd: string, files: Files): void { const mkdirpSync = require("fs-extra/lib/mkdirs/index.js").mkdirpSync const writeFileSync = require("fs").writeFileSync - Object.keys(files).forEach(filePath => { + Object.keys(files).forEach((filePath) => { if (!filePath.startsWith(".git/")) { - mkdirpSync(path.join(cwd, path.dirname(filePath))) - writeFileSync(path.join(cwd, filePath), files[filePath].contents, { + mkdirpSync(join(cwd, dirname(filePath))) + writeFileSync(join(cwd, filePath), files[filePath].contents, { mode: files[filePath].mode, }) } @@ -62,7 +62,7 @@ function writeFiles(cwd: string, files: Files): void { function removeLeadingSpaceOnBlankLines(patchFileContents: string): string { return patchFileContents .split("\n") - .map(line => (line === " " ? "" : line)) + .map((line) => (line === " " ? "" : line)) .join("\n") } diff --git a/src/applyPatches.ts b/src/applyPatches.ts index 04d7e48e..bf60b45f 100644 --- a/src/applyPatches.ts +++ b/src/applyPatches.ts @@ -1,18 +1,14 @@ import chalk from "chalk" -import { getPatchFiles } from "./patchFs" -import { executeEffects } from "./patch/apply" import { existsSync } from "fs-extra" -import { join, resolve, relative } from "./path" import { posix } from "path" -import { - getPackageDetailsFromPatchFilename, - PackageDetails, - PatchedPackageDetails, -} from "./PackageDetails" -import { reversePatch } from "./patch/reverse" import semver from "semver" -import { readPatch } from "./patch/read" +import { PackageDetails } from "./PackageDetails" import { packageIsDevDependency } from "./packageIsDevDependency" +import { executeEffects } from "./patch/apply" +import { readPatch } from "./patch/read" +import { reversePatch } from "./patch/reverse" +import { getGroupedPatches } from "./patchFs" +import { join, relative, resolve } from "./path" class PatchApplicationError extends Error { constructor(msg: string) { @@ -20,14 +16,6 @@ class PatchApplicationError extends Error { } } -function findPatchFiles(patchesDirectory: string): string[] { - if (!existsSync(patchesDirectory)) { - return [] - } - - return getPatchFiles(patchesDirectory) as string[] -} - function getInstalledPackageVersion({ appPath, path, @@ -94,43 +82,22 @@ export function applyPatchesForApp({ shouldExitWithWarning: boolean }): void { const patchesDirectory = join(appPath, patchDir) - const files = findPatchFiles(patchesDirectory) + const groupedPatches = getGroupedPatches(patchesDirectory) - if (files.length === 0) { + if (groupedPatches.numPatchFiles === 0) { console.error(chalk.blueBright("No patch files found")) return } const errors: string[] = [] - const warnings: string[] = [] - - const groupedPatchFileDetails: Record = {} - for (const file of files) { - const details = getPackageDetailsFromPatchFilename(file) - if (!details) { - warnings.push(`Unrecognized patch file in patches directory ${file}`) - continue - } - if (!groupedPatchFileDetails[details.pathSpecifier]) { - groupedPatchFileDetails[details.pathSpecifier] = [] - } - groupedPatchFileDetails[details.pathSpecifier].push(details) - } + const warnings: string[] = [...groupedPatches.warnings] - for (const [_, details] of Object.entries(groupedPatchFileDetails)) { - details.sort((a, b) => { - return (a.sequenceNumber ?? 0) - (b.sequenceNumber ?? 0) - }) + for (const [pathSpecifier, details] of Object.entries( + groupedPatches.pathSpecifierToPatchFiles, + )) { packageLoop: for (const patchDetails of details) { try { - const { - name, - version, - path, - pathSpecifier, - isDevOnly, - patchFilename, - } = patchDetails + const { name, version, path, isDevOnly, patchFilename } = patchDetails const installedPackageVersion = getInstalledPackageVersion({ appPath, @@ -162,6 +129,7 @@ export function applyPatchesForApp({ reverse, patchDetails, patchDir, + cwd: process.cwd(), }) ) { // yay patch was applied successfully @@ -276,11 +244,13 @@ export function applyPatch({ reverse, patchDetails, patchDir, + cwd, }: { patchFilePath: string reverse: boolean patchDetails: PackageDetails patchDir: string + cwd: string }): boolean { const patch = readPatch({ patchFilePath, @@ -288,10 +258,16 @@ export function applyPatch({ patchDir, }) try { - executeEffects(reverse ? reversePatch(patch) : patch, { dryRun: false }) + executeEffects(reverse ? reversePatch(patch) : patch, { + dryRun: false, + cwd, + }) } catch (e) { try { - executeEffects(reverse ? patch : reversePatch(patch), { dryRun: true }) + executeEffects(reverse ? patch : reversePatch(patch), { + dryRun: true, + cwd, + }) } catch (e) { return false } diff --git a/src/index.ts b/src/index.ts index 6278f04a..3a5fbc9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ const argv = minimist(process.argv.slice(2), { "error-on-warn", "create-issue", ], - string: ["patch-dir"], + string: ["patch-dir", "append"], }) const packageNames = argv._ @@ -70,6 +70,10 @@ if (argv.version || argv.v) { excludePaths, patchDir, createIssue, + mode: + "append" in argv + ? { type: "append", name: argv.append || undefined } + : { type: "overwrite_last" }, }) }) } else { diff --git a/src/makePatch.ts b/src/makePatch.ts index d51465d3..c5cebc45 100644 --- a/src/makePatch.ts +++ b/src/makePatch.ts @@ -1,34 +1,36 @@ import chalk from "chalk" -import { join, dirname, resolve } from "./path" -import { spawnSafeSync } from "./spawnSafe" -import { PackageManager } from "./detectPackageManager" -import { removeIgnoredFiles } from "./filterFiles" +import { renameSync } from "fs" import { - writeFileSync, + copySync, existsSync, - mkdirSync, - unlinkSync, mkdirpSync, + mkdirSync, realpathSync, + unlinkSync, + writeFileSync, } from "fs-extra" import { sync as rimraf } from "rimraf" -import { copySync } from "fs-extra" import { dirSync } from "tmp" -import { getPatchFiles } from "./patchFs" -import { - getPatchDetailsFromCliString, - getPackageDetailsFromPatchFilename, - PackageDetails, -} from "./PackageDetails" -import { resolveRelativeFileDependencies } from "./resolveRelativeFileDependencies" -import { getPackageResolution } from "./getPackageResolution" -import { parsePatchFile } from "./patch/parse" import { gzipSync } from "zlib" -import { getPackageVersion } from "./getPackageVersion" +import { applyPatch } from "./applyPatches" import { maybePrintIssueCreationPrompt, openIssueCreationLink, } from "./createIssue" +import { PackageManager } from "./detectPackageManager" +import { removeIgnoredFiles } from "./filterFiles" +import { getPackageResolution } from "./getPackageResolution" +import { getPackageVersion } from "./getPackageVersion" +import { + getPatchDetailsFromCliString, + PackageDetails, + PatchedPackageDetails, +} from "./PackageDetails" +import { parsePatchFile } from "./patch/parse" +import { getGroupedPatches } from "./patchFs" +import { dirname, join, resolve } from "./path" +import { resolveRelativeFileDependencies } from "./resolveRelativeFileDependencies" +import { spawnSafeSync } from "./spawnSafe" function printNoPackageFoundError( packageName: string, @@ -49,6 +51,7 @@ export function makePatch({ excludePaths, patchDir, createIssue, + mode, }: { packagePathSpecifier: string appPath: string @@ -57,6 +60,7 @@ export function makePatch({ excludePaths: RegExp patchDir: string createIssue: boolean + mode: { type: "overwrite_last" } | { type: "append"; name?: string } }) { const packageDetails = getPatchDetailsFromCliString(packagePathSpecifier) @@ -64,6 +68,12 @@ export function makePatch({ console.error("No such package", packagePathSpecifier) return } + + const existingPatches = + getGroupedPatches(patchDir).pathSpecifierToPatchFiles[ + packageDetails.pathSpecifier + ] || [] + const appPackageJson = require(join(appPath, "package.json")) const packagePath = join(appPath, packageDetails.path) const packageJsonPath = join(packagePath, "package.json") @@ -110,15 +120,15 @@ export function makePatch({ join(resolve(packageDetails.path), "package.json"), ) - // copy .npmrc/.yarnrc in case packages are hosted in private registry - // copy .yarn directory as well to ensure installations work in yarn 2 - // tslint:disable-next-line:align - ;[".npmrc", ".yarnrc", ".yarn"].forEach((rcFile) => { - const rcPath = join(appPath, rcFile) - if (existsSync(rcPath)) { - copySync(rcPath, join(tmpRepo.name, rcFile), { dereference: true }) - } - }) + // copy .npmrc/.yarnrc in case packages are hosted in private registry + // copy .yarn directory as well to ensure installations work in yarn 2 + // tslint:disable-next-line:align + ;[".npmrc", ".yarnrc", ".yarn"].forEach((rcFile) => { + const rcPath = join(appPath, rcFile) + if (existsSync(rcPath)) { + copySync(rcPath, join(tmpRepo.name, rcFile), { dereference: true }) + } + }) if (packageManager === "yarn") { console.info( @@ -188,6 +198,26 @@ export function makePatch({ // remove ignored files first removeIgnoredFiles(tmpRepoPackagePath, includePaths, excludePaths) + // apply all existing patches if appending + // otherwise apply all but the last + const patchesToApplyBeforeCommit = + mode.type === "append" ? existingPatches : existingPatches.slice(0, -1) + for (const patchDetails of patchesToApplyBeforeCommit) { + if ( + !applyPatch({ + patchDetails, + patchDir, + patchFilePath: join(appPath, patchDir, patchDetails.patchFilename), + reverse: false, + cwd: tmpRepo.name, + }) + ) { + console.error( + `Failed to apply patch ${patchDetails.patchFilename} to ${packageDetails.pathSpecifier}`, + ) + process.exit(1) + } + } git("add", "-f", packageDetails.path) git("commit", "--allow-empty", "-m", "init") @@ -216,7 +246,7 @@ export function makePatch({ "--ignore-space-at-eol", "--no-ext-diff", "--src-prefix=a/", - "--dst-prefix=b/" + "--dst-prefix=b/", ) if (diffResult.stdout.length === 0) { @@ -280,16 +310,52 @@ export function makePatch({ } // maybe delete existing - getPatchFiles(patchDir).forEach((filename) => { - const deets = getPackageDetailsFromPatchFilename(filename) - if (deets && deets.path === packageDetails.path) { - unlinkSync(join(patchDir, filename)) + if (mode.type === "overwrite_last") { + const prevPatch = existingPatches[existingPatches.length - 1] as + | PatchedPackageDetails + | undefined + if (prevPatch) { + const patchFilePath = join(appPath, patchDir, prevPatch.patchFilename) + try { + unlinkSync(patchFilePath) + } catch (e) { + // noop + } } - }) + } else if (existingPatches.length === 1) { + // if we are appending to an existing patch that doesn't have a sequence number let's rename it + const prevPatch = existingPatches[0] + if (prevPatch.sequenceNumber === undefined) { + const newFileName = createPatchFileName({ + packageDetails, + packageVersion, + sequenceNumber: 1, + sequenceName: prevPatch.sequenceName ?? "initial", + }) + const oldPath = join(appPath, patchDir, prevPatch.patchFilename) + const newPath = join(appPath, patchDir, newFileName) + renameSync(oldPath, newPath) + prevPatch.sequenceNumber = 1 + prevPatch.patchFilename = newFileName + prevPatch.sequenceName = prevPatch.sequenceName ?? "initial" + } + } + + const lastPatch = existingPatches[existingPatches.length - 1] as + | PatchedPackageDetails + | undefined + const sequenceName = + mode.type === "append" ? mode.name : lastPatch?.sequenceName + const sequenceNumber = + mode.type === "append" + ? (lastPatch?.sequenceNumber ?? 0) + 1 + : lastPatch?.sequenceNumber const patchFileName = createPatchFileName({ packageDetails, packageVersion, + sequenceName, + sequenceNumber, }) const patchPath = join(patchesDir, patchFileName) @@ -321,13 +387,24 @@ export function makePatch({ function createPatchFileName({ packageDetails, packageVersion, + sequenceNumber, + sequenceName, }: { packageDetails: PackageDetails packageVersion: string + sequenceNumber?: number + sequenceName?: string }) { const packageNames = packageDetails.packageNames .map((name) => name.replace(/\//g, "+")) .join("++") - return `${packageNames}+${packageVersion}.patch` + const nameAndVersion = `${packageNames}+${packageVersion}` + const num = + sequenceNumber === undefined + ? "" + : `+${sequenceNumber.toString().padStart(3, "0")}` + const name = !sequenceName ? "" : `+${sequenceName}` + + return `${nameAndVersion}${num}${name}.patch` } diff --git a/src/patch/apply.ts b/src/patch/apply.ts index c2601bae..d1d6d5a2 100644 --- a/src/patch/apply.ts +++ b/src/patch/apply.ts @@ -1,43 +1,48 @@ import fs from "fs-extra" -import { dirname } from "path" +import { dirname, join, relative, resolve } from "path" import { ParsedPatchFile, FilePatch, Hunk } from "./parse" import { assertNever } from "../assertNever" export const executeEffects = ( effects: ParsedPatchFile, - { dryRun }: { dryRun: boolean }, + { dryRun, cwd }: { dryRun: boolean; cwd?: string }, ) => { - effects.forEach(eff => { + const inCwd = (path: string) => (cwd ? join(cwd, path) : path) + const humanReadable = (path: string) => relative(process.cwd(), inCwd(path)) + effects.forEach((eff) => { switch (eff.type) { case "file deletion": if (dryRun) { - if (!fs.existsSync(eff.path)) { + if (!fs.existsSync(inCwd(eff.path))) { throw new Error( - "Trying to delete file that doesn't exist: " + eff.path, + "Trying to delete file that doesn't exist: " + + humanReadable(eff.path), ) } } else { // TODO: integrity checks - fs.unlinkSync(eff.path) + fs.unlinkSync(inCwd(eff.path)) } break case "rename": if (dryRun) { // TODO: see what patch files look like if moving to exising path - if (!fs.existsSync(eff.fromPath)) { + if (!fs.existsSync(inCwd(eff.fromPath))) { throw new Error( - "Trying to move file that doesn't exist: " + eff.fromPath, + "Trying to move file that doesn't exist: " + + humanReadable(eff.fromPath), ) } } else { - fs.moveSync(eff.fromPath, eff.toPath) + fs.moveSync(inCwd(eff.fromPath), inCwd(eff.toPath)) } break case "file creation": if (dryRun) { - if (fs.existsSync(eff.path)) { + if (fs.existsSync(inCwd(eff.path))) { throw new Error( - "Trying to create file that already exists: " + eff.path, + "Trying to create file that already exists: " + + humanReadable(eff.path), ) } // todo: check file contents matches @@ -46,23 +51,26 @@ export const executeEffects = ( ? eff.hunk.parts[0].lines.join("\n") + (eff.hunk.parts[0].noNewlineAtEndOfFile ? "" : "\n") : "" - fs.ensureDirSync(dirname(eff.path)) - fs.writeFileSync(eff.path, fileContents, { mode: eff.mode }) + const path = inCwd(eff.path) + fs.ensureDirSync(dirname(path)) + fs.writeFileSync(path, fileContents, { mode: eff.mode }) } break case "patch": - applyPatch(eff, { dryRun }) + applyPatch(eff, { dryRun, cwd }) break case "mode change": - const currentMode = fs.statSync(eff.path).mode + const currentMode = fs.statSync(inCwd(eff.path)).mode if ( ((isExecutable(eff.newMode) && isExecutable(currentMode)) || (!isExecutable(eff.newMode) && !isExecutable(currentMode))) && dryRun ) { - console.warn(`Mode change is not required for file ${eff.path}`) + console.warn( + `Mode change is not required for file ${humanReadable(eff.path)}`, + ) } - fs.chmodSync(eff.path, eff.newMode) + fs.chmodSync(inCwd(eff.path), eff.newMode) break default: assertNever(eff) @@ -104,8 +112,9 @@ function linesAreEqual(a: string, b: string) { function applyPatch( { hunks, path }: FilePatch, - { dryRun }: { dryRun: boolean }, + { dryRun, cwd }: { dryRun: boolean; cwd?: string }, ): void { + path = cwd ? resolve(cwd, path) : path // modifying the file in place const fileContents = fs.readFileSync(path).toString() const mode = fs.statSync(path).mode @@ -128,7 +137,10 @@ function applyPatch( if (Math.abs(fuzzingOffset) > 20) { throw new Error( - `Cant apply hunk ${hunks.indexOf(hunk)} for file ${path}`, + `Cant apply hunk ${hunks.indexOf(hunk)} for file ${relative( + process.cwd(), + path, + )}`, ) } } diff --git a/src/patchFs.ts b/src/patchFs.ts index 9786365e..a4241cc1 100644 --- a/src/patchFs.ts +++ b/src/patchFs.ts @@ -1,3 +1,7 @@ +import { + PatchedPackageDetails, + getPackageDetailsFromPatchFilename, +} from "./PackageDetails" import { relative } from "./path" import klawSync from "klaw-sync" @@ -5,8 +9,51 @@ export const getPatchFiles = (patchesDir: string) => { try { return klawSync(patchesDir, { nodir: true }) .map(({ path }) => relative(patchesDir, path)) - .filter(path => path.endsWith(".patch")) + .filter((path) => path.endsWith(".patch")) } catch (e) { return [] } } + +interface GroupedPatches { + numPatchFiles: number + pathSpecifierToPatchFiles: Record + warnings: string[] +} +export const getGroupedPatches = (patchesDirectory: string): GroupedPatches => { + const files = getPatchFiles(patchesDirectory) + + if (files.length === 0) { + return { + numPatchFiles: 0, + pathSpecifierToPatchFiles: {}, + warnings: [], + } + } + + const warnings: string[] = [] + + const pathSpecifierToPatchFiles: Record = {} + for (const file of files) { + const details = getPackageDetailsFromPatchFilename(file) + if (!details) { + warnings.push(`Unrecognized patch file in patches directory ${file}`) + continue + } + if (!pathSpecifierToPatchFiles[details.pathSpecifier]) { + pathSpecifierToPatchFiles[details.pathSpecifier] = [] + } + pathSpecifierToPatchFiles[details.pathSpecifier].push(details) + } + for (const arr of Object.values(pathSpecifierToPatchFiles)) { + arr.sort((a, b) => { + return (a.sequenceNumber ?? 0) - (b.sequenceNumber ?? 0) + }) + } + + return { + numPatchFiles: files.length, + pathSpecifierToPatchFiles, + warnings, + } +} diff --git a/tslint.json b/tslint.json index ea76ddb6..9d5e2bdf 100644 --- a/tslint.json +++ b/tslint.json @@ -17,6 +17,7 @@ "no-trailing-whitespace": [false], "object-literal-key-quotes": [false], "max-line-length": false, + "no-shadowed-variable": false, "no-default-export": true }, "rulesDirectory": []