Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2ec3191

Browse files
committedJul 12, 2023
initial support for applying patch sequence
1 parent 1c0cfbe commit 2ec3191

11 files changed

+294
-112
lines changed
 
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Test apply-multiple-patches: patch-package fails when a patch in the sequence is invalid 1`] = `
4+
"SNAPSHOT: patch-package fails when a patch in the sequence is invalid
5+
6+
**ERROR** Failed to apply patch for package left-pad at path
7+
8+
node_modules/left-pad
9+
10+
This error was caused because patch-package cannot apply the following patch file:
11+
12+
patches/left-pad+1.3.0+03+broken.patch
13+
14+
Try removing node_modules and trying again. If that doesn't work, maybe there was
15+
an accidental change made to the patch file? Try recreating it by manually
16+
editing the appropriate files and running:
17+
18+
patch-package left-pad
19+
20+
If that doesn't work, then it's a bug in patch-package, so please submit a bug
21+
report. Thanks!
22+
23+
https://github.com/ds300/patch-package/issues
24+
25+
26+
---
27+
patch-package finished with 1 error(s).
28+
END SNAPSHOT"
29+
`;
30+
31+
exports[`Test apply-multiple-patches: patch-package happily applies both good patches 1`] = `
32+
"SNAPSHOT: patch-package happily applies both good patches
33+
patch-package 0.0.0
34+
Applying patches...
35+
left-pad@1.3.0 (1 hello) ✔
36+
left-pad@1.3.0 (2 world) ✔
37+
END SNAPSHOT"
38+
`;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# make sure errors stop the script
2+
set -e
3+
4+
echo "add patch-package"
5+
npm add $1
6+
alias patch-package=./node_modules/.bin/patch-package
7+
8+
echo "SNAPSHOT: patch-package happily applies both good patches"
9+
patch-package
10+
echo "END SNAPSHOT"
11+
12+
cp *broken.patch patches/
13+
14+
(>&2 echo "SNAPSHOT: patch-package fails when a patch in the sequence is invalid")
15+
if patch-package
16+
then
17+
exit 1
18+
fi
19+
(>&2 echo "END SNAPSHOT")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { runIntegrationTest } from "../runIntegrationTest"
2+
runIntegrationTest({
3+
projectName: "apply-multiple-patches",
4+
shouldProduceSnapshots: true,
5+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js
2+
index e90aec3..409dad7 100644
3+
--- a/node_modules/left-pad/index.js
4+
+++ b/node_modules/left-pad/index.js
5+
@@ -1,3 +1,4 @@
6+
+// hello. this is the first patch
7+
/* Thos program is free software. It comes without any warranty, to
8+
* the extent permitted by applicable law. You can redistribute it
9+
* and/or modify it under the terms of the Do What The Fuck You Want

‎integration-tests/apply-multiple-patches/package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "apply-multiple-patches",
3+
"version": "1.0.0",
4+
"description": "integration test for patch-package",
5+
"main": "index.js",
6+
"author": "",
7+
"license": "ISC",
8+
"dependencies": {
9+
"left-pad": "^1.3.0"
10+
}
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js
2+
index e90aec3..409dad7 100644
3+
--- a/node_modules/left-pad/index.js
4+
+++ b/node_modules/left-pad/index.js
5+
@@ -1,3 +1,4 @@
6+
+// hello. this is the first patch
7+
/* This program is free software. It comes without any warranty, to
8+
* the extent permitted by applicable law. You can redistribute it
9+
* and/or modify it under the terms of the Do What The Fuck You Want
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js
2+
index e90aec3..409dad7 100644
3+
--- a/node_modules/left-pad/index.js
4+
+++ b/node_modules/left-pad/index.js
5+
@@ -1,3 +1,4 @@
6+
+// hello. this is the first patch
7+
/* This program is free software. It comes without any warranty, to
8+
* the extent permitted by applicable law. You can redistribute it
9+
* and/or modify it under the terms of the Do What The Fuck You Want

‎src/PackageDetails.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Object {
9797
"patchFilename": "banana++apple+0.4.2.patch",
9898
"path": "node_modules/banana/node_modules/apple",
9999
"pathSpecifier": "banana/apple",
100+
"sequenceName": undefined,
101+
"sequenceNumber": undefined,
100102
"version": "0.4.2",
101103
}
102104
`)
@@ -119,6 +121,8 @@ Object {
119121
"patchFilename": "@types+banana++@types+apple++@mollusc+man+0.4.2-banana-tree.patch",
120122
"path": "node_modules/@types/banana/node_modules/@types/apple/node_modules/@mollusc/man",
121123
"pathSpecifier": "@types/banana/@types/apple/@mollusc/man",
124+
"sequenceName": undefined,
125+
"sequenceNumber": undefined,
122126
"version": "0.4.2-banana-tree",
123127
}
124128
`)
@@ -140,6 +144,8 @@ Object {
140144
"patchFilename": "@types+banana.patch++hello+0.4.2-banana-tree.patch",
141145
"path": "node_modules/@types/banana.patch/node_modules/hello",
142146
"pathSpecifier": "@types/banana.patch/hello",
147+
"sequenceName": undefined,
148+
"sequenceNumber": undefined,
143149
"version": "0.4.2-banana-tree",
144150
}
145151
`)
@@ -161,8 +167,53 @@ Object {
161167
"patchFilename": "@types+banana.patch++hello+0.4.2-banana-tree.dev.patch",
162168
"path": "node_modules/@types/banana.patch/node_modules/hello",
163169
"pathSpecifier": "@types/banana.patch/hello",
170+
"sequenceName": undefined,
171+
"sequenceNumber": undefined,
164172
"version": "0.4.2-banana-tree",
165173
}
174+
`)
175+
})
176+
177+
it("works for ordered patches", () => {
178+
expect(getPackageDetailsFromPatchFilename("left-pad+1.3.0+02+world"))
179+
.toMatchInlineSnapshot(`
180+
Object {
181+
"humanReadablePathSpecifier": "left-pad",
182+
"isDevOnly": false,
183+
"isNested": false,
184+
"name": "left-pad",
185+
"packageNames": Array [
186+
"left-pad",
187+
],
188+
"patchFilename": "left-pad+1.3.0+02+world",
189+
"path": "node_modules/left-pad",
190+
"pathSpecifier": "left-pad",
191+
"sequenceName": "world",
192+
"sequenceNumber": 2,
193+
"version": "1.3.0",
194+
}
195+
`)
196+
197+
expect(
198+
getPackageDetailsFromPatchFilename(
199+
"@microsoft/api-extractor+2.0.0+01+FixThing",
200+
),
201+
).toMatchInlineSnapshot(`
202+
Object {
203+
"humanReadablePathSpecifier": "@microsoft/api-extractor",
204+
"isDevOnly": false,
205+
"isNested": false,
206+
"name": "@microsoft/api-extractor",
207+
"packageNames": Array [
208+
"@microsoft/api-extractor",
209+
],
210+
"patchFilename": "@microsoft/api-extractor+2.0.0+01+FixThing",
211+
"path": "node_modules/@microsoft/api-extractor",
212+
"pathSpecifier": "@microsoft/api-extractor",
213+
"sequenceName": "FixThing",
214+
"sequenceNumber": 1,
215+
"version": "2.0.0",
216+
}
166217
`)
167218
})
168219
})

‎src/PackageDetails.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface PatchedPackageDetails extends PackageDetails {
1313
version: string
1414
patchFilename: string
1515
isDevOnly: boolean
16+
sequenceName?: string
17+
sequenceNumber?: number
1618
}
1719

1820
export function parseNameAndVersion(
@@ -89,27 +91,6 @@ export function parseNameAndVersion(
8991
export function getPackageDetailsFromPatchFilename(
9092
patchFilename: string,
9193
): PatchedPackageDetails | null {
92-
const legacyMatch = patchFilename.match(
93-
/^([^+=]+?)(:|\+)(\d+\.\d+\.\d+.*?)(\.dev)?\.patch$/,
94-
)
95-
96-
if (legacyMatch) {
97-
const name = legacyMatch[1]
98-
const version = legacyMatch[3]
99-
100-
return {
101-
packageNames: [name],
102-
pathSpecifier: name,
103-
humanReadablePathSpecifier: name,
104-
path: join("node_modules", name),
105-
name,
106-
version,
107-
isNested: false,
108-
patchFilename,
109-
isDevOnly: patchFilename.endsWith(".dev.patch"),
110-
}
111-
}
112-
11394
const parts = patchFilename
11495
.replace(/(\.dev)?\.patch$/, "")
11596
.split("++")
@@ -141,6 +122,8 @@ export function getPackageDetailsFromPatchFilename(
141122
isNested: parts.length > 1,
142123
packageNames: parts.map(({ packageName: name }) => name),
143124
isDevOnly: patchFilename.endsWith(".dev.patch"),
125+
sequenceName: lastPart.sequenceName,
126+
sequenceNumber: lastPart.sequenceNumber,
144127
}
145128
}
146129

‎src/applyPatches.ts

Lines changed: 119 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { posix } from "path"
77
import {
88
getPackageDetailsFromPatchFilename,
99
PackageDetails,
10+
PatchedPackageDetails,
1011
} from "./PackageDetails"
1112
import { reversePatch } from "./patch/reverse"
1213
import semver from "semver"
@@ -103,99 +104,126 @@ export function applyPatchesForApp({
103104
const errors: string[] = []
104105
const warnings: string[] = []
105106

106-
for (const filename of files) {
107-
try {
108-
const packageDetails = getPackageDetailsFromPatchFilename(filename)
109-
110-
if (!packageDetails) {
111-
warnings.push(
112-
`Unrecognized patch file in patches directory ${filename}`,
113-
)
114-
continue
115-
}
116-
117-
const {
118-
name,
119-
version,
120-
path,
121-
pathSpecifier,
122-
isDevOnly,
123-
patchFilename,
124-
} = packageDetails
125-
126-
const installedPackageVersion = getInstalledPackageVersion({
127-
appPath,
128-
path,
129-
pathSpecifier,
130-
isDevOnly:
131-
isDevOnly ||
132-
// check for direct-dependents in prod
133-
(process.env.NODE_ENV === "production" &&
134-
packageIsDevDependency({ appPath, packageDetails })),
135-
patchFilename,
136-
})
137-
if (!installedPackageVersion) {
138-
// it's ok we're in production mode and this is a dev only package
139-
console.log(
140-
`Skipping dev-only ${chalk.bold(
141-
pathSpecifier,
142-
)}@${version} ${chalk.blue("✔")}`,
143-
)
144-
continue
145-
}
107+
const groupedPatchFileDetails: Record<string, PatchedPackageDetails[]> = {}
108+
for (const file of files) {
109+
const details = getPackageDetailsFromPatchFilename(file)
110+
if (!details) {
111+
warnings.push(`Unrecognized patch file in patches directory ${file}`)
112+
continue
113+
}
114+
if (!groupedPatchFileDetails[details.pathSpecifier]) {
115+
groupedPatchFileDetails[details.pathSpecifier] = []
116+
}
117+
groupedPatchFileDetails[details.pathSpecifier].push(details)
118+
}
146119

147-
if (
148-
applyPatch({
149-
patchFilePath: resolve(patchesDirectory, filename) as string,
150-
reverse,
151-
packageDetails,
152-
patchDir,
120+
for (const [_, details] of Object.entries(groupedPatchFileDetails)) {
121+
details.sort((a, b) => {
122+
return (a.sequenceNumber ?? 0) - (b.sequenceNumber ?? 0)
123+
})
124+
packageLoop: for (const packageDetails of details) {
125+
try {
126+
const {
127+
name,
128+
version,
129+
path,
130+
pathSpecifier,
131+
isDevOnly,
132+
patchFilename,
133+
} = packageDetails
134+
135+
const installedPackageVersion = getInstalledPackageVersion({
136+
appPath,
137+
path,
138+
pathSpecifier,
139+
isDevOnly:
140+
isDevOnly ||
141+
// check for direct-dependents in prod
142+
(process.env.NODE_ENV === "production" &&
143+
packageIsDevDependency({ appPath, packageDetails })),
144+
patchFilename,
153145
})
154-
) {
155-
// yay patch was applied successfully
156-
// print warning if version mismatch
157-
if (installedPackageVersion !== version) {
158-
warnings.push(
159-
createVersionMismatchWarning({
146+
if (!installedPackageVersion) {
147+
// it's ok we're in production mode and this is a dev only package
148+
console.log(
149+
`Skipping dev-only ${chalk.bold(
150+
pathSpecifier,
151+
)}@${version} ${chalk.blue("✔")}`,
152+
)
153+
continue
154+
}
155+
156+
if (
157+
applyPatch({
158+
patchFilePath: resolve(patchesDirectory, patchFilename) as string,
159+
reverse,
160+
packageDetails,
161+
patchDir,
162+
})
163+
) {
164+
// yay patch was applied successfully
165+
// print warning if version mismatch
166+
if (installedPackageVersion !== version) {
167+
warnings.push(
168+
createVersionMismatchWarning({
169+
packageName: name,
170+
actualVersion: installedPackageVersion,
171+
originalVersion: version,
172+
pathSpecifier,
173+
path,
174+
}),
175+
)
176+
}
177+
const sequenceString =
178+
packageDetails.sequenceNumber != null
179+
? ` (${packageDetails.sequenceNumber}${
180+
packageDetails.sequenceName
181+
? " " + packageDetails.sequenceName
182+
: ""
183+
})`
184+
: ""
185+
console.log(
186+
`${chalk.bold(
187+
pathSpecifier,
188+
)}@${version}${sequenceString} ${chalk.green("✔")}`,
189+
)
190+
} else if (installedPackageVersion === version) {
191+
// completely failed to apply patch
192+
// TODO: propagate useful error messages from patch application
193+
errors.push(
194+
createBrokenPatchFileError({
195+
packageName: name,
196+
patchFilename,
197+
pathSpecifier,
198+
path,
199+
}),
200+
)
201+
} else {
202+
errors.push(
203+
createPatchApplicationFailureError({
160204
packageName: name,
161205
actualVersion: installedPackageVersion,
162206
originalVersion: version,
163-
pathSpecifier,
207+
patchFilename,
164208
path,
209+
pathSpecifier,
165210
}),
166211
)
167212
}
168-
console.log(
169-
`${chalk.bold(pathSpecifier)}@${version} ${chalk.green("✔")}`,
170-
)
171-
} else if (installedPackageVersion === version) {
172-
// completely failed to apply patch
173-
// TODO: propagate useful error messages from patch application
174-
errors.push(
175-
createBrokenPatchFileError({
176-
packageName: name,
177-
patchFileName: filename,
178-
pathSpecifier,
179-
path,
180-
}),
181-
)
182-
} else {
183-
errors.push(
184-
createPatchApplictionFailureError({
185-
packageName: name,
186-
actualVersion: installedPackageVersion,
187-
originalVersion: version,
188-
patchFileName: filename,
189-
path,
190-
pathSpecifier,
191-
}),
192-
)
193-
}
194-
} catch (error) {
195-
if (error instanceof PatchApplicationError) {
196-
errors.push(error.message)
197-
} else {
198-
errors.push(createUnexpectedError({ filename, error }))
213+
} catch (error) {
214+
if (error instanceof PatchApplicationError) {
215+
errors.push(error.message)
216+
} else {
217+
errors.push(
218+
createUnexpectedError({
219+
filename: packageDetails.patchFilename,
220+
error: error as Error,
221+
}),
222+
)
223+
}
224+
if (details.length > 0) {
225+
continue packageLoop
226+
}
199227
}
200228
}
201229
}
@@ -302,12 +330,12 @@ ${chalk.yellow("Warning:")} patch-package detected a patch file version mismatch
302330

303331
function createBrokenPatchFileError({
304332
packageName,
305-
patchFileName,
333+
patchFilename,
306334
path,
307335
pathSpecifier,
308336
}: {
309337
packageName: string
310-
patchFileName: string
338+
patchFilename: string
311339
path: string
312340
pathSpecifier: string
313341
}) {
@@ -320,7 +348,7 @@ ${chalk.red.bold("**ERROR**")} ${chalk.red(
320348
321349
This error was caused because patch-package cannot apply the following patch file:
322350
323-
patches/${patchFileName}
351+
patches/${patchFilename}
324352
325353
Try removing node_modules and trying again. If that doesn't work, maybe there was
326354
an accidental change made to the patch file? Try recreating it by manually
@@ -336,18 +364,18 @@ ${chalk.red.bold("**ERROR**")} ${chalk.red(
336364
`
337365
}
338366

339-
function createPatchApplictionFailureError({
367+
function createPatchApplicationFailureError({
340368
packageName,
341369
actualVersion,
342370
originalVersion,
343-
patchFileName,
371+
patchFilename,
344372
path,
345373
pathSpecifier,
346374
}: {
347375
packageName: string
348376
actualVersion: string
349377
originalVersion: string
350-
patchFileName: string
378+
patchFilename: string
351379
path: string
352380
pathSpecifier: string
353381
}) {
@@ -376,7 +404,7 @@ ${chalk.red.bold("**ERROR**")} ${chalk.red(
376404
patch-package ${pathSpecifier}
377405
378406
Info:
379-
Patch file: patches/${patchFileName}
407+
Patch file: patches/${patchFilename}
380408
Patch was made for version: ${chalk.green.bold(originalVersion)}
381409
Installed version: ${chalk.red.bold(actualVersion)}
382410
`

0 commit comments

Comments
 (0)
Please sign in to comment.