@capgo/capacitor-patch 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +150 -0
- package/bin/capgo-capacitor-patch +6 -0
- package/dist/esm/definitions.d.ts +19 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +7 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +79 -0
- package/patches/catalog.json +1 -0
- package/scripts/capacitor-patch/catalog.mjs +15 -0
- package/scripts/capacitor-patch/cli.mjs +129 -0
- package/scripts/capacitor-patch/config.mjs +72 -0
- package/scripts/capacitor-patch/diff.mjs +216 -0
- package/scripts/capacitor-patch/runner.mjs +234 -0
- package/scripts/capacitor-patch-hook.mjs +27 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const catalogUrl = new URL('../../patches/catalog.json', import.meta.url);
|
|
4
|
+
|
|
5
|
+
export function loadBuiltinCatalog() {
|
|
6
|
+
return readCatalogFile(catalogUrl);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readCatalogFile(file) {
|
|
10
|
+
const catalog = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
11
|
+
if (!Array.isArray(catalog)) {
|
|
12
|
+
throw new Error('Patch catalog must be an array.');
|
|
13
|
+
}
|
|
14
|
+
return catalog;
|
|
15
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { loadBuiltinCatalog } from './catalog.mjs';
|
|
4
|
+
import { getPluginConfig, loadCapacitorExtConfig } from './config.mjs';
|
|
5
|
+
import { printPatchResults, runCapacitorPatch, selectPatches } from './runner.mjs';
|
|
6
|
+
|
|
7
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
8
|
+
const { command, options } = parseArgs(argv);
|
|
9
|
+
|
|
10
|
+
if (command === 'help' || options.help) {
|
|
11
|
+
printHelp();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rootDir = path.resolve(options.root ?? process.cwd());
|
|
16
|
+
const extConfig = await loadCapacitorExtConfig(rootDir, process.env);
|
|
17
|
+
const patchConfig = {
|
|
18
|
+
...getPluginConfig(extConfig),
|
|
19
|
+
strict: options.strict ?? getPluginConfig(extConfig).strict,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (command === 'list') {
|
|
23
|
+
listPatches(loadBuiltinCatalog(), patchConfig, options);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (command === 'apply' || command === 'doctor') {
|
|
28
|
+
const phases = options.phase === 'all' ? ['package', 'native'] : [options.phase ?? 'package'];
|
|
29
|
+
let selectedCount = 0;
|
|
30
|
+
for (const phase of phases) {
|
|
31
|
+
const result = await runCapacitorPatch({
|
|
32
|
+
rootDir,
|
|
33
|
+
extConfig,
|
|
34
|
+
patchConfig,
|
|
35
|
+
phase,
|
|
36
|
+
platformName: options.platform,
|
|
37
|
+
dryRun: command === 'doctor',
|
|
38
|
+
});
|
|
39
|
+
selectedCount += result.selectedCount;
|
|
40
|
+
printPatchResults(result, { quietWhenEmpty: true });
|
|
41
|
+
}
|
|
42
|
+
if (selectedCount === 0) {
|
|
43
|
+
console.log('[CapacitorPatch] No patches selected. Enable plugins.CapacitorPatch.recommended or list patch IDs.');
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Unknown command: ${command}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listPatches(catalog, patchConfig, options) {
|
|
52
|
+
const rows = [];
|
|
53
|
+
|
|
54
|
+
for (const phase of ['package', 'native']) {
|
|
55
|
+
const selected = selectPatches(catalog, patchConfig, phase);
|
|
56
|
+
const selectedIds = new Set(selected.patches.map((entry) => entry.patch.id));
|
|
57
|
+
|
|
58
|
+
for (const patch of catalog.filter((item) => item.phase === phase)) {
|
|
59
|
+
if (!options.all && !selectedIds.has(patch.id)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rows.push({
|
|
64
|
+
id: patch.id,
|
|
65
|
+
phase: patch.phase,
|
|
66
|
+
recommended: patch.recommended === true ? 'yes' : 'no',
|
|
67
|
+
selected: selectedIds.has(patch.id) ? 'yes' : 'no',
|
|
68
|
+
title: patch.title ?? '',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.json) {
|
|
74
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (rows.length === 0) {
|
|
79
|
+
console.log('[CapacitorPatch] No patches to list.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
console.log(`${row.id}\t${row.phase}\trecommended:${row.recommended}\tselected:${row.selected}\t${row.title}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseArgs(argv) {
|
|
89
|
+
const options = {
|
|
90
|
+
phase: 'all',
|
|
91
|
+
};
|
|
92
|
+
let command = 'help';
|
|
93
|
+
|
|
94
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
95
|
+
const value = argv[index];
|
|
96
|
+
if (index === 0 && !value.startsWith('-')) {
|
|
97
|
+
command = value;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (value === '--root') {
|
|
102
|
+
options.root = argv[++index];
|
|
103
|
+
} else if (value === '--phase') {
|
|
104
|
+
options.phase = argv[++index];
|
|
105
|
+
} else if (value === '--platform') {
|
|
106
|
+
options.platform = argv[++index];
|
|
107
|
+
} else if (value === '--strict') {
|
|
108
|
+
options.strict = true;
|
|
109
|
+
} else if (value === '--all') {
|
|
110
|
+
options.all = true;
|
|
111
|
+
} else if (value === '--json') {
|
|
112
|
+
options.json = true;
|
|
113
|
+
} else if (value === '--help' || value === '-h') {
|
|
114
|
+
options.help = true;
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`Unknown option: ${value}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { command, options };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printHelp() {
|
|
124
|
+
console.log(`Usage:
|
|
125
|
+
capgo-capacitor-patch list [--all] [--json]
|
|
126
|
+
capgo-capacitor-patch apply [--root <dir>] [--phase package|native|all] [--platform ios|android] [--strict]
|
|
127
|
+
capgo-capacitor-patch doctor [--root <dir>] [--phase package|native|all] [--platform ios|android] [--strict]
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
export const PLUGIN_CONFIG_KEY = 'CapacitorPatch';
|
|
6
|
+
|
|
7
|
+
export function parseCapacitorConfig(rawValue) {
|
|
8
|
+
if (!rawValue) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(rawValue);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throw new Error(`Unable to parse CAPACITOR_CONFIG: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizePatchConfig(rawConfig = {}) {
|
|
20
|
+
return {
|
|
21
|
+
recommended: rawConfig.recommended === true,
|
|
22
|
+
patches: normalizeStringArray(rawConfig.patches),
|
|
23
|
+
disabled: normalizeStringArray(rawConfig.disabled),
|
|
24
|
+
strict: rawConfig.strict === true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getPluginConfig(extConfig = {}) {
|
|
29
|
+
return normalizePatchConfig(extConfig.plugins?.[PLUGIN_CONFIG_KEY]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function loadCapacitorExtConfig(rootDir, env = process.env) {
|
|
33
|
+
if (env.CAPACITOR_CONFIG) {
|
|
34
|
+
return parseCapacitorConfig(env.CAPACITOR_CONFIG);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fromCli = await loadConfigWithCapacitorCli(rootDir);
|
|
38
|
+
if (fromCli) {
|
|
39
|
+
return fromCli;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const jsonConfigPath = path.join(rootDir, 'capacitor.config.json');
|
|
43
|
+
if (fs.existsSync(jsonConfigPath)) {
|
|
44
|
+
return JSON.parse(fs.readFileSync(jsonConfigPath, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadConfigWithCapacitorCli(rootDir) {
|
|
51
|
+
const require = createRequire(import.meta.url);
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const { loadConfig } = require('@capacitor/cli/dist/config');
|
|
56
|
+
process.chdir(rootDir);
|
|
57
|
+
const config = await loadConfig();
|
|
58
|
+
return config.app?.extConfig ?? {};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
} finally {
|
|
62
|
+
process.chdir(cwd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeStringArray(value) {
|
|
67
|
+
if (!Array.isArray(value)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value.filter((item) => typeof item === 'string' && item.length > 0);
|
|
72
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export class PatchApplyError extends Error {
|
|
5
|
+
constructor(message, details = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PatchApplyError';
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function applyUnifiedDiff(rootDir, diffText, options = {}) {
|
|
13
|
+
const reverse = options.reverse === true;
|
|
14
|
+
const dryRun = options.dryRun === true;
|
|
15
|
+
const files = parseUnifiedDiff(diffText);
|
|
16
|
+
const updates = [];
|
|
17
|
+
|
|
18
|
+
for (const filePatch of files) {
|
|
19
|
+
const relativePath = getTargetPath(filePatch, reverse);
|
|
20
|
+
if (!relativePath) {
|
|
21
|
+
throw new PatchApplyError('Patch creates or deletes a file without a usable target path.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const absolutePath = path.resolve(rootDir, relativePath);
|
|
25
|
+
assertInsideRoot(rootDir, absolutePath);
|
|
26
|
+
|
|
27
|
+
const exists = fs.existsSync(absolutePath);
|
|
28
|
+
if (!exists && !isCreateFile(filePatch, reverse)) {
|
|
29
|
+
throw new PatchApplyError(`Missing patch target: ${relativePath}`, { file: relativePath });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const original = exists ? fs.readFileSync(absolutePath, 'utf8') : '';
|
|
33
|
+
const updated = applyHunks(original, filePatch.hunks, reverse, relativePath);
|
|
34
|
+
const deleteFile = isDeleteFile(filePatch, reverse);
|
|
35
|
+
|
|
36
|
+
if (deleteFile || updated !== original) {
|
|
37
|
+
updates.push({ absolutePath, relativePath, content: updated, deleteFile });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!dryRun) {
|
|
42
|
+
for (const update of updates) {
|
|
43
|
+
if (update.deleteFile) {
|
|
44
|
+
fs.rmSync(update.absolutePath, { force: true });
|
|
45
|
+
} else {
|
|
46
|
+
fs.mkdirSync(path.dirname(update.absolutePath), { recursive: true });
|
|
47
|
+
fs.writeFileSync(update.absolutePath, update.content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
changedFiles: updates.map((update) => update.relativePath),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseUnifiedDiff(diffText) {
|
|
58
|
+
const lines = diffText.replace(/\r\n/g, '\n').split('\n');
|
|
59
|
+
const files = [];
|
|
60
|
+
let index = 0;
|
|
61
|
+
|
|
62
|
+
while (index < lines.length) {
|
|
63
|
+
if (!lines[index].startsWith('--- ')) {
|
|
64
|
+
index += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const oldPath = parseDiffPath(lines[index]);
|
|
69
|
+
index += 1;
|
|
70
|
+
|
|
71
|
+
if (!lines[index]?.startsWith('+++ ')) {
|
|
72
|
+
throw new PatchApplyError('Malformed patch: missing +++ file header.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newPath = parseDiffPath(lines[index]);
|
|
76
|
+
index += 1;
|
|
77
|
+
const hunks = [];
|
|
78
|
+
|
|
79
|
+
while (index < lines.length && !lines[index].startsWith('--- ')) {
|
|
80
|
+
if (!lines[index].startsWith('@@ ')) {
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(lines[index]);
|
|
86
|
+
if (!match) {
|
|
87
|
+
throw new PatchApplyError(`Malformed patch hunk: ${lines[index]}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
index += 1;
|
|
91
|
+
const hunkLines = [];
|
|
92
|
+
while (index < lines.length && !lines[index].startsWith('@@ ') && !lines[index].startsWith('--- ')) {
|
|
93
|
+
if (lines[index].startsWith('\\')) {
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (lines[index] !== '' || index < lines.length - 1) {
|
|
98
|
+
hunkLines.push(lines[index]);
|
|
99
|
+
}
|
|
100
|
+
index += 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hunks.push({
|
|
104
|
+
oldStart: Number(match[1]),
|
|
105
|
+
oldCount: Number(match[2] ?? '1'),
|
|
106
|
+
newStart: Number(match[3]),
|
|
107
|
+
newCount: Number(match[4] ?? '1'),
|
|
108
|
+
lines: hunkLines,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
files.push({ oldPath, newPath, hunks });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (files.length === 0) {
|
|
116
|
+
throw new PatchApplyError('Patch does not contain any unified diff file headers.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function applyHunks(content, hunks, reverse, relativePath) {
|
|
123
|
+
const original = splitContent(content);
|
|
124
|
+
const lines = [...original.lines];
|
|
125
|
+
let offset = 0;
|
|
126
|
+
|
|
127
|
+
for (const hunk of hunks) {
|
|
128
|
+
const expected = [];
|
|
129
|
+
const replacement = [];
|
|
130
|
+
|
|
131
|
+
for (const rawLine of hunk.lines) {
|
|
132
|
+
const marker = rawLine[0];
|
|
133
|
+
const value = rawLine.slice(1);
|
|
134
|
+
|
|
135
|
+
if (marker === ' ') {
|
|
136
|
+
expected.push(value);
|
|
137
|
+
replacement.push(value);
|
|
138
|
+
} else if (marker === '-') {
|
|
139
|
+
if (reverse) {
|
|
140
|
+
replacement.push(value);
|
|
141
|
+
} else {
|
|
142
|
+
expected.push(value);
|
|
143
|
+
}
|
|
144
|
+
} else if (marker === '+') {
|
|
145
|
+
if (reverse) {
|
|
146
|
+
expected.push(value);
|
|
147
|
+
} else {
|
|
148
|
+
replacement.push(value);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
throw new PatchApplyError(`Unsupported patch line marker "${marker}" in ${relativePath}.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const start = Math.max(0, (reverse ? hunk.newStart : hunk.oldStart) - 1 + offset);
|
|
156
|
+
if (!matchesAt(lines, start, expected)) {
|
|
157
|
+
throw new PatchApplyError(`Patch hunk does not match ${relativePath} at line ${start + 1}.`, {
|
|
158
|
+
file: relativePath,
|
|
159
|
+
line: start + 1,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lines.splice(start, expected.length, ...replacement);
|
|
164
|
+
offset += replacement.length - expected.length;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return joinContent(lines, original.finalNewline);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseDiffPath(line) {
|
|
171
|
+
const value = line.slice(4).split('\t')[0].trim();
|
|
172
|
+
if (value === '/dev/null') {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return value.replace(/^[ab]\//, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getTargetPath(filePatch, reverse) {
|
|
179
|
+
return reverse ? (filePatch.oldPath ?? filePatch.newPath) : (filePatch.newPath ?? filePatch.oldPath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isCreateFile(filePatch, reverse) {
|
|
183
|
+
return reverse ? filePatch.newPath === null : filePatch.oldPath === null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isDeleteFile(filePatch, reverse) {
|
|
187
|
+
return reverse ? filePatch.oldPath === null : filePatch.newPath === null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function splitContent(content) {
|
|
191
|
+
const finalNewline = content.endsWith('\n');
|
|
192
|
+
const lines = content.split('\n');
|
|
193
|
+
if (finalNewline) {
|
|
194
|
+
lines.pop();
|
|
195
|
+
}
|
|
196
|
+
return { lines, finalNewline };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function joinContent(lines, finalNewline) {
|
|
200
|
+
return `${lines.join('\n')}${finalNewline ? '\n' : ''}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function matchesAt(lines, start, expected) {
|
|
204
|
+
if (start < 0 || start + expected.length > lines.length) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return expected.every((line, index) => lines[start + index] === line);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertInsideRoot(rootDir, absolutePath) {
|
|
212
|
+
const relative = path.relative(path.resolve(rootDir), absolutePath);
|
|
213
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
214
|
+
throw new PatchApplyError(`Patch target escapes root: ${absolutePath}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
|
|
6
|
+
import { loadBuiltinCatalog } from './catalog.mjs';
|
|
7
|
+
import { getPluginConfig } from './config.mjs';
|
|
8
|
+
import { PatchApplyError, applyUnifiedDiff } from './diff.mjs';
|
|
9
|
+
|
|
10
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
11
|
+
|
|
12
|
+
export async function runCapacitorPatch(options) {
|
|
13
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
14
|
+
const phase = options.phase;
|
|
15
|
+
const catalog = options.catalog ?? loadBuiltinCatalog();
|
|
16
|
+
const extConfig = options.extConfig ?? {};
|
|
17
|
+
const patchConfig = options.patchConfig ?? getPluginConfig(extConfig);
|
|
18
|
+
const patchBaseDir = path.resolve(options.patchBaseDir ?? packageRoot);
|
|
19
|
+
const selected = selectPatches(catalog, patchConfig, phase);
|
|
20
|
+
const results = [];
|
|
21
|
+
|
|
22
|
+
for (const unknownId of selected.unknownIds) {
|
|
23
|
+
results.push({
|
|
24
|
+
id: unknownId,
|
|
25
|
+
phase,
|
|
26
|
+
status: 'failed',
|
|
27
|
+
reason: 'Patch ID was not found in the catalog.',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const entry of selected.patches) {
|
|
32
|
+
results.push(
|
|
33
|
+
await applyCatalogPatch({
|
|
34
|
+
rootDir,
|
|
35
|
+
patchBaseDir,
|
|
36
|
+
patch: entry.patch,
|
|
37
|
+
selectedBy: entry.selectedBy,
|
|
38
|
+
phase,
|
|
39
|
+
platformName: options.platformName,
|
|
40
|
+
strict: patchConfig.strict,
|
|
41
|
+
dryRun: options.dryRun === true,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const failures = results.filter((result) => result.status === 'failed');
|
|
47
|
+
if (patchConfig.strict && failures.length > 0) {
|
|
48
|
+
throw new Error(failures.map((failure) => `${failure.id}: ${failure.reason}`).join('\n'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
phase,
|
|
53
|
+
selectedCount: selected.patches.length + selected.unknownIds.length,
|
|
54
|
+
results,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function selectPatches(catalog, patchConfig, phase) {
|
|
59
|
+
const disabled = new Set(patchConfig.disabled);
|
|
60
|
+
const explicit = new Set(patchConfig.patches);
|
|
61
|
+
const catalogById = new Map(catalog.map((patch) => [patch.id, patch]));
|
|
62
|
+
const patches = [];
|
|
63
|
+
|
|
64
|
+
for (const patch of catalog) {
|
|
65
|
+
if (!patch?.id || patch.phase !== phase || disabled.has(patch.id)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (explicit.has(patch.id)) {
|
|
70
|
+
patches.push({ patch, selectedBy: 'explicit' });
|
|
71
|
+
} else if (patchConfig.recommended && patch.recommended === true) {
|
|
72
|
+
patches.push({ patch, selectedBy: 'recommended' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
patches,
|
|
78
|
+
unknownIds: patchConfig.patches.filter((id) => !disabled.has(id) && !catalogById.has(id)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function applyCatalogPatch(options) {
|
|
83
|
+
const { patch, rootDir, patchBaseDir, selectedBy, platformName, strict, dryRun } = options;
|
|
84
|
+
const baseResult = {
|
|
85
|
+
id: patch.id,
|
|
86
|
+
title: patch.title,
|
|
87
|
+
phase: patch.phase,
|
|
88
|
+
selectedBy,
|
|
89
|
+
status: 'skipped',
|
|
90
|
+
changedFiles: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(patch.platforms) && platformName && !patch.platforms.includes(platformName)) {
|
|
94
|
+
return {
|
|
95
|
+
...baseResult,
|
|
96
|
+
reason: `Patch is for ${patch.platforms.join(', ')}, current platform is ${platformName}.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const target = patch.target ?? {};
|
|
101
|
+
const targetType = target.type ?? (target.packageName ? 'package' : 'native');
|
|
102
|
+
const targetRoot = resolveTargetRoot(rootDir, targetType, target);
|
|
103
|
+
if (!targetRoot) {
|
|
104
|
+
return {
|
|
105
|
+
...baseResult,
|
|
106
|
+
reason: `Target package ${target.packageName} is not installed.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const versionCheck = checkTargetVersion(targetRoot, target);
|
|
111
|
+
if (!versionCheck.ok) {
|
|
112
|
+
return {
|
|
113
|
+
...baseResult,
|
|
114
|
+
status: strict ? 'failed' : 'skipped',
|
|
115
|
+
reason: versionCheck.reason,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const patchFile = path.resolve(patchBaseDir, patch.patchFile);
|
|
120
|
+
if (!fs.existsSync(patchFile)) {
|
|
121
|
+
return {
|
|
122
|
+
...baseResult,
|
|
123
|
+
status: 'failed',
|
|
124
|
+
reason: `Patch file is missing: ${patch.patchFile}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const diffText = fs.readFileSync(patchFile, 'utf8');
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const applied = applyUnifiedDiff(targetRoot, diffText, { dryRun });
|
|
132
|
+
const status = dryRun ? 'would-apply' : 'applied';
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...baseResult,
|
|
136
|
+
status,
|
|
137
|
+
changedFiles: applied.changedFiles,
|
|
138
|
+
note:
|
|
139
|
+
patch.target?.packageName === '@capacitor/cli' && !dryRun
|
|
140
|
+
? 'This patch changes @capacitor/cli. Run the next cap command to use the patched CLI code.'
|
|
141
|
+
: undefined,
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof PatchApplyError && isAlreadyApplied(targetRoot, diffText)) {
|
|
145
|
+
return {
|
|
146
|
+
...baseResult,
|
|
147
|
+
status: 'already-applied',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...baseResult,
|
|
153
|
+
status: strict ? 'failed' : 'failed',
|
|
154
|
+
reason: error?.message ?? String(error),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveTargetRoot(rootDir, targetType, target) {
|
|
160
|
+
if (targetType === 'native') {
|
|
161
|
+
return rootDir;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const packageName = target.packageName;
|
|
165
|
+
if (!packageName) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const packageRootDir = path.join(rootDir, 'node_modules', ...packageName.split('/'));
|
|
170
|
+
const packageJson = path.join(packageRootDir, 'package.json');
|
|
171
|
+
return fs.existsSync(packageJson) ? packageRootDir : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function checkTargetVersion(targetRoot, target) {
|
|
175
|
+
if (!target.versionRange) {
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const packageJsonPath = path.join(targetRoot, 'package.json');
|
|
180
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
181
|
+
return { ok: false, reason: 'Target package has no package.json for version checking.' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
185
|
+
const version = packageJson.version;
|
|
186
|
+
if (!semver.valid(version) || !semver.satisfies(version, target.versionRange, { includePrerelease: true })) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
reason: `Installed version ${version ?? 'unknown'} does not satisfy ${target.versionRange}.`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { ok: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isAlreadyApplied(targetRoot, diffText) {
|
|
197
|
+
try {
|
|
198
|
+
applyUnifiedDiff(targetRoot, diffText, { reverse: true, dryRun: true });
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function printPatchResults(result, options = {}) {
|
|
206
|
+
if (options.quietWhenEmpty && result.selectedCount === 0) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (result.selectedCount === 0) {
|
|
211
|
+
console.log('[CapacitorPatch] No patches selected. Enable plugins.CapacitorPatch.recommended or list patch IDs.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const item of result.results) {
|
|
216
|
+
const label = item.title ? `${item.id} (${item.title})` : item.id;
|
|
217
|
+
if (item.status === 'applied' || item.status === 'would-apply') {
|
|
218
|
+
const action = item.status === 'would-apply' ? 'Would apply' : 'Applied';
|
|
219
|
+
console.log(`[CapacitorPatch] ${action} ${label}.`);
|
|
220
|
+
for (const file of item.changedFiles ?? []) {
|
|
221
|
+
console.log(`[CapacitorPatch] ${file}`);
|
|
222
|
+
}
|
|
223
|
+
if (item.note) {
|
|
224
|
+
console.log(`[CapacitorPatch] ${item.note}`);
|
|
225
|
+
}
|
|
226
|
+
} else if (item.status === 'already-applied') {
|
|
227
|
+
console.log(`[CapacitorPatch] Already applied ${label}.`);
|
|
228
|
+
} else if (item.status === 'skipped') {
|
|
229
|
+
console.log(`[CapacitorPatch] Skipped ${label}: ${item.reason}`);
|
|
230
|
+
} else {
|
|
231
|
+
console.warn(`[CapacitorPatch] Failed ${label}: ${item.reason}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getPluginConfig, loadCapacitorExtConfig } from './capacitor-patch/config.mjs';
|
|
2
|
+
import { printPatchResults, runCapacitorPatch } from './capacitor-patch/runner.mjs';
|
|
3
|
+
|
|
4
|
+
const phase = process.argv[2];
|
|
5
|
+
|
|
6
|
+
if (phase !== 'package' && phase !== 'native') {
|
|
7
|
+
console.error('[CapacitorPatch] Expected hook phase "package" or "native".');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
} else {
|
|
10
|
+
try {
|
|
11
|
+
const rootDir = process.env.CAPACITOR_ROOT_DIR ?? process.cwd();
|
|
12
|
+
const extConfig = await loadCapacitorExtConfig(rootDir, process.env);
|
|
13
|
+
const patchConfig = getPluginConfig(extConfig);
|
|
14
|
+
const result = await runCapacitorPatch({
|
|
15
|
+
rootDir,
|
|
16
|
+
extConfig,
|
|
17
|
+
patchConfig,
|
|
18
|
+
phase,
|
|
19
|
+
platformName: process.env.CAPACITOR_PLATFORM_NAME,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
printPatchResults(result, { quietWhenEmpty: true });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`[CapacitorPatch] ${error?.message ?? error}`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
}
|