Skip to content

Commit 9bee26a

Browse files
committed
support dev-only patches
1 parent 94a25d0 commit 9bee26a

13 files changed

+360
-5
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ or `patch` in unixy environments:
143143

144144
patch -p1 -i patches/package-name+0.44.2.patch
145145

146+
### Dev-only patches
147+
148+
If you deploy your package to production (e.g. your package is a server) then any patched `devDependencies` will not be present when patch-package runs in production. It will happily ignore those patch files if the package to be patched is listed directly in the `devDependencies` of your package.json. If it's a transitive dependency patch-package can't detect that it is safe to ignore and will throw an error. To fix this, mark patches for transitive dev dependencies as dev-only by renaming from, e.g.
149+
150+
package-name+0.44.0.patch
151+
152+
to
153+
154+
package-name+0.44.0.dev.patch
155+
156+
This will allow those patch files to be safely ignored when `NODE_ENV=production`.
157+
146158
## Benefits of patching over forking
147159

148160
- Sometimes forks need extra build steps, e.g. with react-native for Android. Forget that noise.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Test dev-only-patches: fake-package should be skipped 1`] = `
4+
"SNAPSHOT: fake-package should be skipped
5+
patch-package 0.0.0
6+
Applying patches...
7+
Skipping dev-only [email protected]
8+
9+
Skipping dev-only [email protected]
10+
END SNAPSHOT"
11+
`;
12+
13+
exports[`Test dev-only-patches: patch-package fails to find fake-package 1`] = `
14+
"SNAPSHOT: patch-package fails to find fake-package
15+
Error: Patch file found for package fake-package which is not present at node_modules/fake-package
16+
17+
If this package is a dev dependency, rename the patch file to
18+
19+
fake-package+3.0.0.dev.patch
20+
21+
END SNAPSHOT"
22+
`;
23+
24+
exports[`Test dev-only-patches: patch-package happily ignores slash because it's a dev dep 1`] = `
25+
"SNAPSHOT: patch-package happily ignores slash because it's a dev dep
26+
patch-package 0.0.0
27+
Applying patches...
28+
29+
Skipping dev-only [email protected]
30+
END SNAPSHOT"
31+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# make sure errors stop the script
2+
set -e
3+
4+
echo "set production mode"
5+
export NODE_ENV=production
6+
7+
echo "add patch-package"
8+
yarn add $1
9+
10+
echo "SNAPSHOT: patch-package happily ignores slash because it's a dev dep"
11+
npx patch-package
12+
echo "END SNAPSHOT"
13+
14+
echo "create fake-package+3.0.0.patch"
15+
cp patches/slash+3.0.0.patch patches/fake-package+3.0.0.patch
16+
17+
(>&2 echo "SNAPSHOT: patch-package fails to find fake-package")
18+
if npx patch-package
19+
then
20+
exit 1
21+
fi
22+
(>&2 echo "END SNAPSHOT")
23+
24+
echo "rename fake-package patch file to .dev.patch"
25+
mv patches/fake-package+3.0.0.patch patches/fake-package+3.0.0.dev.patch
26+
27+
echo "SNAPSHOT: fake-package should be skipped"
28+
npx patch-package
29+
echo "END SNAPSHOT"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { runIntegrationTest } from "../runIntegrationTest"
2+
runIntegrationTest({
3+
projectName: "dev-only-patches",
4+
shouldProduceSnapshots: true,
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "dev-only-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+
"rimraf": "3.0.0"
11+
},
12+
"devDependencies": {
13+
"slash": "3.0.0"
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff --git a/node_modules/left-pad/index.js b/node_modules/left-pad/index.js
2+
index e90aec3..823ee5f 100644
3+
--- a/node_modules/left-pad/index.js
4+
+++ b/node_modules/left-pad/index.js
5+
@@ -4,7 +4,7 @@
6+
* To Public License, Version 2, as published by Sam Hocevar. See
7+
* http://www.wtfpl.net/ for more details. */
8+
'use strict';
9+
-module.exports = leftPad;
10+
+module.exports = patchPackage;
11+
12+
var cache = [
13+
'',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
diff --git a/node_modules/slash/index.js b/node_modules/slash/index.js
2+
index 103fbea..e3c36f8 100644
3+
--- a/node_modules/slash/index.js
4+
+++ b/node_modules/slash/index.js
5+
@@ -1,5 +1,5 @@
6+
'use strict';
7+
-module.exports = path => {
8+
+module.patchPackage = path => {
9+
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
10+
const hasNonAscii = /[^\u0000-\u0080]+/.test(path); // eslint-disable-line no-control-regex
11+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
balanced-match@^1.0.0:
6+
version "1.0.0"
7+
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
8+
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
9+
10+
brace-expansion@^1.1.7:
11+
version "1.1.11"
12+
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
13+
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
14+
dependencies:
15+
balanced-match "^1.0.0"
16+
concat-map "0.0.1"
17+
18+
19+
version "0.0.1"
20+
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
21+
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
22+
23+
fs.realpath@^1.0.0:
24+
version "1.0.0"
25+
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
26+
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
27+
28+
glob@^7.1.3:
29+
version "7.1.4"
30+
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
31+
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
32+
dependencies:
33+
fs.realpath "^1.0.0"
34+
inflight "^1.0.4"
35+
inherits "2"
36+
minimatch "^3.0.4"
37+
once "^1.3.0"
38+
path-is-absolute "^1.0.0"
39+
40+
inflight@^1.0.4:
41+
version "1.0.6"
42+
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
43+
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
44+
dependencies:
45+
once "^1.3.0"
46+
wrappy "1"
47+
48+
inherits@2:
49+
version "2.0.4"
50+
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
51+
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
52+
53+
left-pad@^1.3.0:
54+
version "1.3.0"
55+
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
56+
integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
57+
58+
minimatch@^3.0.4:
59+
version "3.0.4"
60+
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
61+
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
62+
dependencies:
63+
brace-expansion "^1.1.7"
64+
65+
once@^1.3.0:
66+
version "1.4.0"
67+
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
68+
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
69+
dependencies:
70+
wrappy "1"
71+
72+
path-is-absolute@^1.0.0:
73+
version "1.0.1"
74+
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
75+
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
76+
77+
rimraf@^3.0.0:
78+
version "3.0.0"
79+
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b"
80+
integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==
81+
dependencies:
82+
glob "^7.1.3"
83+
84+
slash@^3.0.0:
85+
version "3.0.0"
86+
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
87+
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
88+
89+
wrappy@1:
90+
version "1.0.2"
91+
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
92+
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

src/PackageDetails.test.ts

+44
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe("getPackageDetailsFromPatchFilename", () => {
1010
).toMatchInlineSnapshot(`
1111
Object {
1212
"humanReadablePathSpecifier": "@types/banana",
13+
"isDevOnly": false,
1314
"isNested": false,
1415
"name": "@types/banana",
1516
"packageNames": Array [
@@ -26,6 +27,7 @@ Object {
2627
.toMatchInlineSnapshot(`
2728
Object {
2829
"humanReadablePathSpecifier": "banana",
30+
"isDevOnly": false,
2931
"isNested": false,
3032
"name": "banana",
3133
"packageNames": Array [
@@ -42,6 +44,7 @@ Object {
4244
.toMatchInlineSnapshot(`
4345
Object {
4446
"humanReadablePathSpecifier": "banana",
47+
"isDevOnly": false,
4548
"isNested": false,
4649
"name": "banana",
4750
"packageNames": Array [
@@ -59,13 +62,31 @@ Object {
5962
expect(
6063
getPackageDetailsFromPatchFilename("@types+banana-0.4.2.patch"),
6164
).toBe(null)
65+
66+
expect(getPackageDetailsFromPatchFilename("banana+0.4.2.dev.patch"))
67+
.toMatchInlineSnapshot(`
68+
Object {
69+
"humanReadablePathSpecifier": "banana",
70+
"isDevOnly": true,
71+
"isNested": false,
72+
"name": "banana",
73+
"packageNames": Array [
74+
"banana",
75+
],
76+
"patchFilename": "banana+0.4.2.dev.patch",
77+
"path": "node_modules/banana",
78+
"pathSpecifier": "banana",
79+
"version": "0.4.2.dev",
80+
}
81+
`)
6282
})
6383

6484
it("parses new-style patch filenames", () => {
6585
expect(getPackageDetailsFromPatchFilename("banana++apple+0.4.2.patch"))
6686
.toMatchInlineSnapshot(`
6787
Object {
6888
"humanReadablePathSpecifier": "banana => apple",
89+
"isDevOnly": false,
6990
"isNested": true,
7091
"name": "apple",
7192
"packageNames": Array [
@@ -86,6 +107,7 @@ Object {
86107
).toMatchInlineSnapshot(`
87108
Object {
88109
"humanReadablePathSpecifier": "@types/banana => @types/apple => @mollusc/man",
110+
"isDevOnly": false,
89111
"isNested": true,
90112
"name": "@mollusc/man",
91113
"packageNames": Array [
@@ -107,6 +129,7 @@ Object {
107129
).toMatchInlineSnapshot(`
108130
Object {
109131
"humanReadablePathSpecifier": "@types/banana.patch => hello",
132+
"isDevOnly": false,
110133
"isNested": true,
111134
"name": "hello",
112135
"packageNames": Array [
@@ -118,6 +141,27 @@ Object {
118141
"pathSpecifier": "@types/banana.patch/hello",
119142
"version": "0.4.2-banana-tree",
120143
}
144+
`)
145+
146+
expect(
147+
getPackageDetailsFromPatchFilename(
148+
"@types+banana.patch++hello+0.4.2-banana-tree.dev.patch",
149+
),
150+
).toMatchInlineSnapshot(`
151+
Object {
152+
"humanReadablePathSpecifier": "@types/banana.patch => hello",
153+
"isDevOnly": true,
154+
"isNested": true,
155+
"name": "hello",
156+
"packageNames": Array [
157+
"@types/banana.patch",
158+
"hello",
159+
],
160+
"patchFilename": "@types+banana.patch++hello+0.4.2-banana-tree.dev.patch",
161+
"path": "node_modules/@types/banana.patch/node_modules/hello",
162+
"pathSpecifier": "@types/banana.patch/hello",
163+
"version": "0.4.2-banana-tree",
164+
}
121165
`)
122166
})
123167
})

src/PackageDetails.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ export interface PackageDetails {
99
packageNames: string[]
1010
}
1111

12-
interface PatchedPackageDetails extends PackageDetails {
12+
export interface PatchedPackageDetails extends PackageDetails {
1313
version: string
1414
patchFilename: string
15+
isDevOnly: boolean
1516
}
1617

1718
function parseNameAndVersion(
@@ -47,7 +48,7 @@ export function getPackageDetailsFromPatchFilename(
4748
patchFilename: string,
4849
): PatchedPackageDetails | null {
4950
const legacyMatch = patchFilename.match(
50-
/^([^+=]+?)(:|\+)(\d+\.\d+\.\d+.*)\.patch$/,
51+
/^([^+=]+?)(:|\+)(\d+\.\d+\.\d+.*)(\.dev)?\.patch$/,
5152
)
5253

5354
if (legacyMatch) {
@@ -63,11 +64,12 @@ export function getPackageDetailsFromPatchFilename(
6364
version,
6465
isNested: false,
6566
patchFilename,
67+
isDevOnly: patchFilename.endsWith(".dev.patch"),
6668
}
6769
}
6870

6971
const parts = patchFilename
70-
.replace(/\.patch$/, "")
72+
.replace(/(\.dev)?\.patch$/, "")
7173
.split("++")
7274
.map(parseNameAndVersion)
7375
.filter((x): x is NonNullable<typeof x> => x !== null)
@@ -94,6 +96,7 @@ export function getPackageDetailsFromPatchFilename(
9496
humanReadablePathSpecifier: parts.map(({ name }) => name).join(" => "),
9597
isNested: parts.length > 1,
9698
packageNames: parts.map(({ name }) => name),
99+
isDevOnly: patchFilename.endsWith(".dev.patch"),
97100
}
98101
}
99102

0 commit comments

Comments
 (0)