v1.17.0
Six open complaints about patch-package were reproduced against this code and the four that reproduced here are now closed. All four belong to one class — the tool reporting success without doing the job — which this project treats as worse than any refusal.
Fixed
-
applyno longer lays a rewritten patch on top of the version already in the tree. Switch branches and the patch file under one name becomes a different file, whilenode_modulesstays patched by the old one. An appending hunk fits a second time, so both versions ended up in the file while the run printed a green tick and1 applied— measured onms@2.1.2, both// FIX VERSION ONEand// FIX VERSION TWOin one file, withstatuscalling it in the tree. patch-package reports the same on #487 and #557, and there it at least ends in a refusal.The record now carries the hashes of the files each patch left behind, which turns "the patch file changed" into a statement about the tree: when not one of those files has moved since, the previous version is provably still there, and
applyrefuses instead of stacking — naming the file to look at and the package to reinstall. When the tree has moved on, because the package was reinstalled or someone edited it, there is nothing to prove and it warns instead, so a clean install does not fail over a record. A refusal keeps the record it was proved from; without that the state file was rewritten without the refused patch and the nextbun installknew nothing.The record costs +2 ms per 90 patches in the case that runs on every install — 17 ms against 19 ms on a stand of 30 packages with three sequenced patches each, files of 800 lines, median of five.
-
createno longer overwrites the patch of a neighbouring directory. An alias exists to hold two versions of one package, and both directories answer to one manifest name. The patch file took its name from the manifest, so with matching versions the secondcreatereplaced the first patch and printed✅ Patch created; the change was simply gone. With differing versions the names differed only by version, andrebase is-number 0took off the patches of both directories while announcing it was rebasingmynum. -
rebaseunderstands a package installed under an npm alias.rebase mynum 0answeredNo patches found for mynum— the only one of six commands that could not resolve the name. Worse,rebase is-number 0did work and then printed advice that did not:Now edit node_modules/is-numberfor a directory that does not exist, andcreate is-number --append, which exits 1. The directory in the hints now comes from the paths inside the patch, which is where it is actually written. -
The record survives a patch file that is gone. Delete or rename one and its entry stays behind — it is the only thing that remembers those changes are probably still in
node_modules, andstatussays so. Butrebaseandreverserebuilt the record after the rollback and read every file named in it, so a missing one threwENOENT: the work was done and the command reported a stack instead.applydid not throw; it dropped the entry silently, so the warning lived exactly until the next install — andapplyruns frompostinstall. Measured: one run erased it while the deleted patch's change sat in the tree untouched. -
retargetstopped being a dead end. It refused a set carrying more than one version with "sort that out first" — no way out, at the very placeapplysends you. It still refuses, because moving them all would give two files the same name, but now it says that and lists the files written for a version that is not installed.
Changed
-
A patch file is named after the directory it patches, not after the package manifest. For everything except an npm alias these are the same name and nothing changes. For an alias,
create mynumnow writespatches/mynum+7.0.0.patch— which is also what patch-package writes, checked by running it. Patches created by earlier versions carry the manifest name and are still read: the key is looked up by directory first and by manifest name second, and that order matters, since starting from the manifest name would hand one directory the patches of its neighbour. The nextcreatemoves such a file onto the directory name and says so, and never touches one that patches a different directory. -
Two patches for one package are explained instead of merely counted. Upgrade a package, run
createagain, andpatches/holds two files for one package; both are applied and the tree ends up with the changes of two versions at once (patch-package #480). What was said about it wasversion mismatch (patch: 2.1.2, installed: 2.1.3)on the stale file — true, and no help, because the trouble is the set.applynow names the other file when one of the package's patches is written for exactly the installed version, andcreatesays it the moment the second file appears, asking the tree whether the new patch already carries the old one's changes rather than guessing. The applying itself is unchanged: measured, our tree matches patch-package byte for byte here, and it warns and applies too.
Verified
- 272 tests,
tsc --noEmit, on Linux, macOS and Windows, plus smoke jobs on bun 1.0.36, 1.1.45 and 1.2.23. The suite runs twice locally, with Apple diff and with GNU diff. - The corpus of 292 real patches ran on every change touching parsing, applying or un-applying: 291 compared, all matching patch-package and the pre-change code, 0 NOT-idempotent, and round-trip down to the two known cases —
react-native-launch-navigator+1.0.8, whose newline cannot be recovered, and@radix-ui+react-dialog+1.1.6, which patch-package refuses as well. - 23 mutations across the five changes. Four survived a run, and none of them meant "spare code" by default: two were unreachable second locks and were removed, and two were holes in the suite — nothing covered separating two directories that share an old-style name, and nothing covered the order the patch key is looked up in. Both are covered now.
- Two of the defects above were found by comparing against
mainon a live tree rather than by the suite, which is now written down as a step of its own: a change that narrows something is invisible to tests written for the new behaviour.
Full Changelog: v1.16.0...v1.17.0