Skip to content

Commit aec88cc

Browse files
David Sheldrickds300
authored andcommitted
wip JS-only patch application
1 parent 3d2365c commit aec88cc

File tree

6 files changed

+401
-101
lines changed

6 files changed

+401
-101
lines changed

src/__tests__/patch.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// tslint:disable
2+
3+
import { parseHunkHeaderLine, patch } from "../patch"
4+
import * as fs from "fs-extra"
5+
6+
const properReadFileSync = fs.readFileSync
7+
const properWriteFileSync = fs.writeFileSync
8+
const properUnlinkSync = fs.unlinkSync
9+
const properMoveSync = fs.moveSync
10+
11+
describe("parseHunkHeaderLine", () => {
12+
it("parses hunk header lines", () => {
13+
expect(parseHunkHeaderLine("@@ -0,0 +1,21 @@")).toEqual({
14+
original: {
15+
length: 0,
16+
start: 0,
17+
},
18+
patched: {
19+
length: 21,
20+
start: 1,
21+
},
22+
})
23+
})
24+
})
25+
26+
describe("patch", () => {
27+
let mockFs: null | Record<string, string> = null
28+
29+
beforeEach(() => {
30+
mockFs = {
31+
"other/file.js": `once
32+
upon
33+
a
34+
time
35+
the
36+
end`,
37+
"patch/file.patch": `diff --git a/other/file.js b/other/file.js
38+
index b7cb24e..c152982 100644
39+
--- a/other/file.js
40+
+++ b/other/file.js
41+
@@ -1,6 +1,6 @@
42+
once
43+
upon
44+
-a
45+
+the
46+
time
47+
the
48+
end
49+
`,
50+
"other/file3.js": `somewhere
51+
over
52+
the
53+
rainbow`,
54+
"delete/file.patch": `diff --git a/other/file3.js b/other/file3.js
55+
deleted file mode 100644
56+
index 9367e0d..0000000
57+
--- a/other/file3.js
58+
+++ /dev/null
59+
@@ -1,4 +0,0 @@
60+
-somewhere
61+
-over
62+
-the
63+
-rainbow`,
64+
"create/file.patch": `diff --git a/other/newFile.js b/other/newFile.js
65+
new file mode 100644
66+
index 0000000..98043fb
67+
--- /dev/null
68+
+++ b/other/newFile.js
69+
@@ -0,0 +1,5 @@
70+
+this
71+
+is
72+
+a
73+
+new
74+
+file`,
75+
"rename/file.patch": `diff --git a/james.js b/peter.js
76+
similarity index 100%
77+
rename from james.js
78+
rename to peter.js`,
79+
"james.js": "i am peter",
80+
"move-and-edit/file.patch": `diff --git a/banana.js b/orange.js
81+
similarity index 68%
82+
rename from banana.js
83+
rename to orange.js
84+
index 98043fb..f029e2f 100644
85+
--- a/banana.js
86+
+++ b/orange.js
87+
@@ -1,5 +1,5 @@
88+
this
89+
is
90+
a
91+
-new
92+
+orange
93+
file`,
94+
"banana.js": `this
95+
is
96+
a
97+
new
98+
file`,
99+
}
100+
// tslint:disable
101+
;(fs as any).readFileSync = jest.fn(path => {
102+
return mockFs && mockFs[path]
103+
})
104+
;(fs as any).writeFileSync = jest.fn((path, data) => {
105+
mockFs && (mockFs[path] = data)
106+
})
107+
;(fs as any).unlinkSync = jest.fn(path => {
108+
mockFs && delete mockFs[path]
109+
})
110+
;(fs as any).moveSync = jest.fn((from, to) => {
111+
if (!mockFs) return
112+
113+
mockFs[to] = mockFs[from]
114+
delete mockFs[from]
115+
})
116+
})
117+
118+
afterEach(() => {
119+
;(fs as any).readFileSync = properReadFileSync
120+
;(fs as any).writeFileSync = properWriteFileSync
121+
;(fs as any).unlinkSync = properUnlinkSync
122+
;(fs as any).moveSync = properMoveSync
123+
})
124+
125+
it("patches files", () => {
126+
patch("patch/file.patch")
127+
expect(mockFs && mockFs["other/file.js"]).toBe(`once
128+
upon
129+
the
130+
time
131+
the
132+
end`)
133+
})
134+
135+
it("deletes files", () => {
136+
patch("delete/file.patch")
137+
expect(mockFs && !mockFs["other/file3.js"])
138+
})
139+
140+
it("creates new files", () => {
141+
patch("create/file.patch")
142+
expect(mockFs && mockFs["other/newFile.js"]).toBe(`this
143+
is
144+
a
145+
new
146+
file`)
147+
})
148+
149+
it("renames files", () => {
150+
patch("rename/file.patch")
151+
expect(mockFs && !mockFs["james.js"])
152+
expect(mockFs && mockFs["peter.js"]).toBe("i am peter")
153+
})
154+
155+
it("renames and modifies files", () => {
156+
patch("move-and-edit/file.patch")
157+
expect(mockFs && !mockFs["banana.js"])
158+
expect(mockFs && mockFs["orange.js"]).toBe(`this
159+
is
160+
a
161+
orange
162+
file`)
163+
})
164+
})

src/applyPatches.ts

Lines changed: 80 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,98 @@
11
import { bold, cyan, green, red } from "chalk"
22
import * as fs from "fs"
33
import * as path from "path"
4-
import spawnSafeSync from "./spawnSafe"
5-
import { getPatchFiles, removeGitHeadersFromPath } from "./patchFs"
4+
import { getPatchFiles } from "./patchFs"
5+
import { patch } from "./patch"
66

7-
export default function findPatchFiles(appPath: string, reverse: boolean) {
8-
const patchesDirectory = path.join(appPath, "patches")
7+
type OpaqueString<S extends string> = string & { type: S }
8+
export type AppPath = OpaqueString<"AppPath">
9+
type PatchesDirectory = OpaqueString<"PatchesDirectory">
10+
type FileName = OpaqueString<"FileName">
11+
type PackageName = OpaqueString<"PackageName">
12+
type PackageVersion = OpaqueString<"PackageVersion">
13+
14+
function findPatchFiles(patchesDirectory: PatchesDirectory): FileName[] {
915
if (!fs.existsSync(patchesDirectory)) {
1016
return []
1117
}
12-
const files = getPatchFiles(patchesDirectory).filter(filename =>
13-
filename.match(/^.+(:|\+).+\.patch$/),
14-
)
18+
19+
return getPatchFiles(patchesDirectory) as FileName[]
20+
}
21+
22+
function getPatchDetailsFromFilename(filename: FileName) {
23+
// ok to coerce this, since we already filtered for valid package file names
24+
// in getPatchFiles
25+
const match = filename.match(/^(.+?)(:|\+)(.+)\.patch$/) as string[]
26+
const packageName = match[1] as PackageName
27+
const version = match[3] as PackageVersion
28+
29+
return {
30+
packageName,
31+
version,
32+
}
33+
}
34+
35+
function getInstalledPackageVersion(
36+
appPath: AppPath,
37+
packageName: PackageName,
38+
) {
39+
const packageDir = path.join(appPath, "node_modules", packageName)
40+
if (!fs.existsSync(packageDir)) {
41+
console.warn(
42+
`${red("Warning:")} Patch file found for package ${path.posix.basename(
43+
packageDir,
44+
)}` + ` which is not present at ${packageDir}`,
45+
)
46+
47+
return null
48+
}
49+
50+
return require(path.join(packageDir, "package.json"))
51+
.version as PackageVersion
52+
}
53+
54+
export function applyPatchesForApp(appPath: AppPath, reverse: boolean): void {
55+
// TODO: get rid of this line
56+
console.log("reverse", reverse)
57+
const patchesDirectory = path.join(appPath, "patches") as PatchesDirectory
58+
const files = findPatchFiles(patchesDirectory)
1559

1660
if (files.length === 0) {
1761
console.log(cyan("No patch files found"))
1862
}
1963

2064
files.forEach(filename => {
21-
const match = filename.match(/^(.+?)(:|\+)(.+)\.patch$/) as string[]
22-
const packageName = match[1]
23-
const version = match[3]
24-
const packageDir = path.join(appPath, "node_modules", packageName)
25-
26-
if (!fs.existsSync(packageDir)) {
27-
console.warn(
28-
`${red("Warning:")} Patch file found for package ${packageName}` +
29-
` which is not present at ${packageDir}`,
30-
)
31-
return null
32-
}
65+
const { packageName, version } = getPatchDetailsFromFilename(filename)
66+
67+
const installedPackageVersion = getInstalledPackageVersion(
68+
appPath,
69+
packageName,
70+
)
3371

34-
const packageJson = require(path.join(packageDir, "package.json"))
72+
if (!installedPackageVersion) {
73+
return
74+
}
3575

3676
try {
37-
applyPatch(path.resolve(patchesDirectory, filename), reverse)
77+
patch(path.resolve(patchesDirectory, filename) as FileName /*, reverse */)
3878

39-
if (packageJson.version !== version) {
40-
printVersionMismatchWarning(packageName, packageJson.version, version)
79+
if (installedPackageVersion !== version) {
80+
printVersionMismatchWarning(
81+
packageName,
82+
installedPackageVersion,
83+
version,
84+
)
4185
} else {
4286
console.log(`${bold(packageName)}@${version} ${green("✔")}`)
4387
}
4488
} catch (e) {
4589
// completely failed to apply patch
46-
if (packageJson.version === version) {
90+
if (installedPackageVersion === version) {
4791
printBrokenPatchFileError(packageName, filename)
4892
} else {
4993
printPatchApplictionFailureError(
5094
packageName,
51-
packageJson.version,
95+
installedPackageVersion,
5296
version,
5397
filename,
5498
)
@@ -58,73 +102,10 @@ export default function findPatchFiles(appPath: string, reverse: boolean) {
58102
})
59103
}
60104

61-
export function gitApplyArgs(
62-
patchFilePath: string,
63-
{
64-
reverse,
65-
check,
66-
}: {
67-
reverse?: boolean
68-
check?: boolean
69-
},
70-
) {
71-
const args = ["apply", "--ignore-whitespace", "--whitespace=nowarn"]
72-
if (reverse) {
73-
args.push("--reverse")
74-
}
75-
if (check) {
76-
args.push("--check")
77-
}
78-
79-
args.push(patchFilePath)
80-
81-
return args
82-
}
83-
84-
export function applyPatch(patchFilePath: string, reverse: boolean) {
85-
// first find out if the patch file was made by patch-package
86-
const firstLine = fs
87-
.readFileSync(patchFilePath)
88-
.slice(0, "patch-package\n".length)
89-
.toString()
90-
91-
// if not then remove git headers before applying to make sure git
92-
// doesn't skip files that aren't in the index
93-
if (firstLine !== "patch-package\n") {
94-
patchFilePath = removeGitHeadersFromPath(patchFilePath)
95-
}
96-
97-
try {
98-
spawnSafeSync(
99-
"git",
100-
gitApplyArgs(patchFilePath, { reverse, check: true }),
101-
{
102-
logStdErrOnError: false,
103-
},
104-
)
105-
106-
spawnSafeSync("git", gitApplyArgs(patchFilePath, { reverse }), {
107-
logStdErrOnError: false,
108-
})
109-
} catch (e) {
110-
// patch cli tool has no way to fail gracefully if patch was already
111-
// applied, so to check, we need to try a dry-run of applying the patch in
112-
// reverse, and if that works it means the patch was already applied
113-
// sucessfully. Otherwise the patch just failed for some reason.
114-
spawnSafeSync(
115-
"git",
116-
gitApplyArgs(patchFilePath, { reverse: !reverse, check: true }),
117-
{
118-
logStdErrOnError: false,
119-
},
120-
)
121-
}
122-
}
123-
124105
function printVersionMismatchWarning(
125-
packageName: string,
126-
actualVersion: string,
127-
originalVersion: string,
106+
packageName: PackageName,
107+
actualVersion: PackageVersion,
108+
originalVersion: PackageVersion,
128109
) {
129110
console.warn(`
130111
${red("Warning:")} patch-package detected a patch file version mismatch
@@ -150,7 +131,10 @@ ${red("Warning:")} patch-package detected a patch file version mismatch
150131
`)
151132
}
152133

153-
function printBrokenPatchFileError(packageName: string, patchFileName: string) {
134+
function printBrokenPatchFileError(
135+
packageName: PackageName,
136+
patchFileName: FileName,
137+
) {
154138
console.error(`
155139
${red.bold("**ERROR**")} ${red(
156140
`Failed to apply patch for package ${bold(packageName)}`,
@@ -165,10 +149,10 @@ ${red.bold("**ERROR**")} ${red(
165149
}
166150

167151
function printPatchApplictionFailureError(
168-
packageName: string,
169-
actualVersion: string,
170-
originalVersion: string,
171-
patchFileName: string,
152+
packageName: PackageName,
153+
actualVersion: PackageVersion,
154+
originalVersion: PackageVersion,
155+
patchFileName: FileName,
172156
) {
173157
console.error(`
174158
${red.bold("**ERROR**")} ${red(

0 commit comments

Comments
 (0)