@mcamdali/podexit 0.0.0-stage → 0.1.2
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 +21 -0
- package/README.de.md +254 -0
- package/README.es.md +254 -0
- package/README.fr.md +254 -0
- package/README.ja.md +254 -0
- package/README.ko.md +254 -0
- package/README.md +297 -2
- package/README.pt-BR.md +254 -0
- package/README.ru.md +254 -0
- package/README.tr.md +254 -0
- package/README.zh-CN.md +254 -0
- package/data/publisher-policy.json +42 -0
- package/data/publisher-policy.schema.json +96 -0
- package/dist/analyze.d.ts +20 -0
- package/dist/analyze.js +195 -0
- package/dist/cli/discover.d.ts +15 -0
- package/dist/cli/discover.js +65 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +11 -0
- package/dist/cli/run.d.ts +19 -0
- package/dist/cli/run.js +146 -0
- package/dist/core/classify.d.ts +39 -0
- package/dist/core/classify.js +71 -0
- package/dist/core/lockfile.d.ts +47 -0
- package/dist/core/lockfile.js +164 -0
- package/dist/core/policy.d.ts +45 -0
- package/dist/core/policy.js +40 -0
- package/dist/core/project.d.ts +13 -0
- package/dist/core/project.js +25 -0
- package/dist/core/timeline.d.ts +16 -0
- package/dist/core/timeline.js +14 -0
- package/dist/core/versions.d.ts +11 -0
- package/dist/core/versions.js +57 -0
- package/dist/data.d.ts +3 -0
- package/dist/data.js +9 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +28 -0
- package/dist/net/cache.d.ts +28 -0
- package/dist/net/cache.js +89 -0
- package/dist/net/cocoapods.d.ts +34 -0
- package/dist/net/cocoapods.js +83 -0
- package/dist/net/fetcher.d.ts +33 -0
- package/dist/net/fetcher.js +35 -0
- package/dist/net/github.d.ts +14 -0
- package/dist/net/github.js +42 -0
- package/dist/report/escape.d.ts +17 -0
- package/dist/report/escape.js +54 -0
- package/dist/report/index.d.ts +11 -0
- package/dist/report/index.js +21 -0
- package/dist/report/labels.d.ts +22 -0
- package/dist/report/labels.js +82 -0
- package/dist/report/markdown.d.ts +6 -0
- package/dist/report/markdown.js +75 -0
- package/dist/report/tty.d.ts +12 -0
- package/dist/report/tty.js +103 -0
- package/dist/report-model.d.ts +75 -0
- package/dist/report-model.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +7 -0
- package/package.json +72 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"dataVersion": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"pattern": "^\\d{4}\\.\\d{2}\\.\\d{2}$"
|
|
11
|
+
},
|
|
12
|
+
"publishers": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-z0-9-]+$"
|
|
20
|
+
},
|
|
21
|
+
"name": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1
|
|
24
|
+
},
|
|
25
|
+
"pods": {
|
|
26
|
+
"minItems": 1,
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 1
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"lastMajorOnCocoaPods": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0,
|
|
36
|
+
"maximum": 9007199254740991
|
|
37
|
+
},
|
|
38
|
+
"stoppedPublishing": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"pattern": "^\\d{4}-\\d{2}$"
|
|
41
|
+
},
|
|
42
|
+
"summary": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"minLength": 1
|
|
45
|
+
},
|
|
46
|
+
"includesSubdependencies": {
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
49
|
+
"swiftPackage": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"url": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"format": "uri"
|
|
55
|
+
},
|
|
56
|
+
"note": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"minLength": 1
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": [
|
|
62
|
+
"url",
|
|
63
|
+
"note"
|
|
64
|
+
],
|
|
65
|
+
"additionalProperties": false
|
|
66
|
+
},
|
|
67
|
+
"source": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"format": "uri"
|
|
70
|
+
},
|
|
71
|
+
"checked": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"format": "date",
|
|
74
|
+
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"required": [
|
|
78
|
+
"id",
|
|
79
|
+
"name",
|
|
80
|
+
"pods",
|
|
81
|
+
"lastMajorOnCocoaPods",
|
|
82
|
+
"stoppedPublishing",
|
|
83
|
+
"summary",
|
|
84
|
+
"source",
|
|
85
|
+
"checked"
|
|
86
|
+
],
|
|
87
|
+
"additionalProperties": false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"required": [
|
|
92
|
+
"dataVersion",
|
|
93
|
+
"publishers"
|
|
94
|
+
],
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PublisherPolicy } from './core/policy.js';
|
|
2
|
+
import type { Fetcher } from './net/fetcher.js';
|
|
3
|
+
import { type LockfileReport, type PodReport, type Report, SCHEMA_VERSION, type Summary, type SwiftPackageReport, type UpdatesReport } from './report-model.js';
|
|
4
|
+
export { type LockfileReport, type PodReport, type Report, SCHEMA_VERSION, type Summary, type SwiftPackageReport, type UpdatesReport, };
|
|
5
|
+
export interface AnalyzeOptions {
|
|
6
|
+
fetcher: Fetcher;
|
|
7
|
+
policy: PublisherPolicy;
|
|
8
|
+
/** Parallel pod lookups. Default 8. */
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function summarize(pods: readonly PodReport[]): Summary;
|
|
12
|
+
/** Analyzes the text of one Podfile.lock. `path` is only used for display and project detection. */
|
|
13
|
+
export declare function analyzeLockfile(text: string, path: string, options: AnalyzeOptions): Promise<LockfileReport>;
|
|
14
|
+
export interface ReportContext {
|
|
15
|
+
policy: PublisherPolicy;
|
|
16
|
+
offline: boolean;
|
|
17
|
+
now?: Date;
|
|
18
|
+
}
|
|
19
|
+
/** Wraps lockfile reports with the metadata every output format shows. */
|
|
20
|
+
export declare function buildReport(lockfiles: LockfileReport[], context: ReportContext): Report;
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { classifyLockfile, describeUpdates } from './core/classify.js';
|
|
2
|
+
import { parseLockfile } from './core/lockfile.js';
|
|
3
|
+
import { detectProjectKind, GUIDES } from './core/project.js';
|
|
4
|
+
import { BUILDS_KEEP_WORKING, TIMELINE } from './core/timeline.js';
|
|
5
|
+
import { compareVersions, latestVersion } from './core/versions.js';
|
|
6
|
+
import { fetchPodspec, releaseDates, trunkVersions } from './net/cocoapods.js';
|
|
7
|
+
import { hasPackageManifest, parseGitHubUrl, repositoryUrl, } from './net/github.js';
|
|
8
|
+
import { SCHEMA_VERSION, } from './report-model.js';
|
|
9
|
+
import { VERSION } from './version.js';
|
|
10
|
+
export { SCHEMA_VERSION, };
|
|
11
|
+
async function trunkFacts(fetcher, pod) {
|
|
12
|
+
const [versions, dates, podspec] = await Promise.all([
|
|
13
|
+
trunkVersions(fetcher, pod.name),
|
|
14
|
+
releaseDates(fetcher, pod.name),
|
|
15
|
+
fetchPodspec(fetcher, pod.name, pod.version),
|
|
16
|
+
]);
|
|
17
|
+
const repository = parseGitHubUrl(podspec?.source?.git) ?? parseGitHubUrl(podspec?.homepage) ?? null;
|
|
18
|
+
return {
|
|
19
|
+
latest: versions ? latestVersion(versions) : null,
|
|
20
|
+
dates,
|
|
21
|
+
tag: podspec?.source?.git ? (podspec.source.tag ?? null) : null,
|
|
22
|
+
repository,
|
|
23
|
+
otherSource: podspec?.source?.http ?? podspec?.source?.git ?? podspec?.homepage ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function updatesReport(pod, { status, publisher, via }, facts) {
|
|
27
|
+
const latest = facts?.latest ?? null;
|
|
28
|
+
const report = {
|
|
29
|
+
status,
|
|
30
|
+
summary: describeUpdates(status, pod, publisher, via),
|
|
31
|
+
latestOnTrunk: latest,
|
|
32
|
+
latestReleasedAt: latest ? (facts?.dates?.get(latest) ?? null) : null,
|
|
33
|
+
lockedReleasedAt: facts?.dates?.get(pod.version) ?? null,
|
|
34
|
+
behindTrunk: latest ? compareVersions(pod.version, latest) < 0 : null,
|
|
35
|
+
};
|
|
36
|
+
if (via)
|
|
37
|
+
report.via = via;
|
|
38
|
+
if (publisher) {
|
|
39
|
+
const { id, name, lastMajorOnCocoaPods, source } = publisher;
|
|
40
|
+
report.publisher = { id, name, lastMajorOnCocoaPods, source };
|
|
41
|
+
}
|
|
42
|
+
return report;
|
|
43
|
+
}
|
|
44
|
+
function hostOf(url) {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(url).hostname;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function upstreamPackage(fetcher, repository, ref) {
|
|
53
|
+
const url = repositoryUrl(repository);
|
|
54
|
+
const head = await hasPackageManifest(fetcher, repository);
|
|
55
|
+
if (head === null) {
|
|
56
|
+
return {
|
|
57
|
+
status: 'unknown',
|
|
58
|
+
summary: `No answer from GitHub for ${url}.`,
|
|
59
|
+
repository: url,
|
|
60
|
+
atLockedVersion: null,
|
|
61
|
+
via: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!head) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'no-spm',
|
|
67
|
+
summary: `${url} has no Package.swift on its default branch.`,
|
|
68
|
+
repository: url,
|
|
69
|
+
atLockedVersion: null,
|
|
70
|
+
via: 'upstream',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const atLocked = ref ? await hasPackageManifest(fetcher, repository, ref) : null;
|
|
74
|
+
const detail = atLocked === true
|
|
75
|
+
? ` It is also there at ${ref}, the version you use.`
|
|
76
|
+
: atLocked === false
|
|
77
|
+
? ` It is not there at ${ref}, the version you use: the switch needs a newer version.`
|
|
78
|
+
: '';
|
|
79
|
+
return {
|
|
80
|
+
status: 'spm-available',
|
|
81
|
+
summary: `${url} has a Package.swift on its default branch.${detail}`,
|
|
82
|
+
repository: url,
|
|
83
|
+
atLockedVersion: atLocked,
|
|
84
|
+
via: 'upstream',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function swiftPackageReport(fetcher, pod, { publisher, via }, facts) {
|
|
88
|
+
// Subdependencies are separate projects (GoogleUtilities, nanopb), so they are looked up upstream.
|
|
89
|
+
if (publisher?.swiftPackage && !via) {
|
|
90
|
+
return {
|
|
91
|
+
status: 'spm-available',
|
|
92
|
+
summary: `${publisher.swiftPackage.note} (${publisher.source})`,
|
|
93
|
+
repository: publisher.swiftPackage.url,
|
|
94
|
+
atLockedVersion: null,
|
|
95
|
+
via: 'publisher',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const { source } = pod;
|
|
99
|
+
if (source.kind === 'git') {
|
|
100
|
+
const repository = parseGitHubUrl(source.url);
|
|
101
|
+
if (repository)
|
|
102
|
+
return upstreamPackage(fetcher, repository, source.tag ?? source.commit ?? null);
|
|
103
|
+
return {
|
|
104
|
+
status: 'unknown',
|
|
105
|
+
summary: `Git source is not on GitHub (${source.url}).`,
|
|
106
|
+
repository: null,
|
|
107
|
+
atLockedVersion: null,
|
|
108
|
+
via: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (source.kind !== 'trunk') {
|
|
112
|
+
return {
|
|
113
|
+
status: 'not-checked',
|
|
114
|
+
summary: 'Local or private source; podexit does not look it up.',
|
|
115
|
+
repository: null,
|
|
116
|
+
atLockedVersion: null,
|
|
117
|
+
via: null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (facts?.repository)
|
|
121
|
+
return upstreamPackage(fetcher, facts.repository, facts.tag);
|
|
122
|
+
const summary = facts?.otherSource
|
|
123
|
+
? `The podspec points to ${hostOf(facts.otherSource)}, not a GitHub repository.`
|
|
124
|
+
: 'No podspec answer from the CocoaPods CDN (offline, or nothing recorded).';
|
|
125
|
+
return { status: 'unknown', summary, repository: null, atLockedVersion: null, via: null };
|
|
126
|
+
}
|
|
127
|
+
async function analyzePod(pod, classification, fetcher) {
|
|
128
|
+
const facts = pod.source.kind === 'trunk' ? await trunkFacts(fetcher, pod) : null;
|
|
129
|
+
return {
|
|
130
|
+
name: pod.name,
|
|
131
|
+
version: pod.version,
|
|
132
|
+
subspecs: pod.subspecs,
|
|
133
|
+
direct: pod.direct,
|
|
134
|
+
requiredBy: pod.requiredBy,
|
|
135
|
+
source: pod.source,
|
|
136
|
+
updates: updatesReport(pod, classification, facts),
|
|
137
|
+
swiftPackage: await swiftPackageReport(fetcher, pod, classification, facts),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function mapLimit(items, limit, task) {
|
|
141
|
+
const results = new Array(items.length);
|
|
142
|
+
let next = 0;
|
|
143
|
+
const worker = async () => {
|
|
144
|
+
while (next < items.length) {
|
|
145
|
+
const index = next;
|
|
146
|
+
next += 1;
|
|
147
|
+
results[index] = await task(items[index]);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
export function summarize(pods) {
|
|
154
|
+
const count = (predicate) => pods.filter(predicate).length;
|
|
155
|
+
return {
|
|
156
|
+
pods: pods.length,
|
|
157
|
+
direct: count((pod) => pod.direct),
|
|
158
|
+
frozenOnTrunk: count((pod) => pod.updates.status === 'frozen-on-trunk'),
|
|
159
|
+
capped: count((pod) => pod.updates.status === 'capped'),
|
|
160
|
+
notAffected: count((pod) => pod.updates.status === 'not-affected'),
|
|
161
|
+
unknown: count((pod) => pod.updates.status === 'unknown'),
|
|
162
|
+
spmAvailable: count((pod) => pod.swiftPackage.status === 'spm-available'),
|
|
163
|
+
noSpm: count((pod) => pod.swiftPackage.status === 'no-spm'),
|
|
164
|
+
spmUnknown: count((pod) => pod.swiftPackage.status === 'unknown'),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Analyzes the text of one Podfile.lock. `path` is only used for display and project detection. */
|
|
168
|
+
export async function analyzeLockfile(text, path, options) {
|
|
169
|
+
const lockfile = parseLockfile(text);
|
|
170
|
+
const classifications = classifyLockfile(lockfile.pods, options.policy);
|
|
171
|
+
const pods = await mapLimit(lockfile.pods, options.concurrency ?? 8, (pod) => analyzePod(pod, classifications.get(pod.name) ?? { status: 'unknown' }, options.fetcher));
|
|
172
|
+
const project = detectProjectKind(lockfile.pods, path);
|
|
173
|
+
return {
|
|
174
|
+
path,
|
|
175
|
+
project,
|
|
176
|
+
guide: GUIDES[project],
|
|
177
|
+
cocoapodsVersion: lockfile.cocoapodsVersion,
|
|
178
|
+
summary: summarize(pods),
|
|
179
|
+
pods,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** Wraps lockfile reports with the metadata every output format shows. */
|
|
183
|
+
export function buildReport(lockfiles, context) {
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: SCHEMA_VERSION,
|
|
186
|
+
tool: { name: 'podexit', version: VERSION },
|
|
187
|
+
generatedAt: (context.now ?? new Date()).toISOString(),
|
|
188
|
+
offline: context.offline,
|
|
189
|
+
timeline: TIMELINE,
|
|
190
|
+
policyDataVersion: context.policy.dataVersion,
|
|
191
|
+
notes: [BUILDS_KEEP_WORKING],
|
|
192
|
+
lockfiles,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Podfile.lock lives in each kind of project, relative to the folder podexit runs in:
|
|
3
|
+
* a native app keeps it next to the Podfile, Flutter and React Native apps under `ios/` (and
|
|
4
|
+
* `macos/` for Flutter desktop), and a Flutter plugin under its example app.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOCKFILE_LOCATIONS: readonly ['Podfile.lock', 'ios/Podfile.lock', 'macos/Podfile.lock', 'example/ios/Podfile.lock', 'example/macos/Podfile.lock'];
|
|
7
|
+
export declare class DiscoveryError extends Error {
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Turns the paths given on the command line (files or folders; the current folder when none)
|
|
12
|
+
* into absolute lockfile paths, without duplicates. Throws {@link DiscoveryError} for a path that
|
|
13
|
+
* does not exist or a folder without a lockfile in any of the {@link LOCKFILE_LOCATIONS}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findLockfiles(paths: readonly string[], cwd: string): Promise<string[]>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Where a Podfile.lock lives in each kind of project, relative to the folder podexit runs in:
|
|
5
|
+
* a native app keeps it next to the Podfile, Flutter and React Native apps under `ios/` (and
|
|
6
|
+
* `macos/` for Flutter desktop), and a Flutter plugin under its example app.
|
|
7
|
+
*/
|
|
8
|
+
export const LOCKFILE_LOCATIONS = [
|
|
9
|
+
'Podfile.lock',
|
|
10
|
+
'ios/Podfile.lock',
|
|
11
|
+
'macos/Podfile.lock',
|
|
12
|
+
'example/ios/Podfile.lock',
|
|
13
|
+
'example/macos/Podfile.lock',
|
|
14
|
+
];
|
|
15
|
+
export class DiscoveryError extends Error {
|
|
16
|
+
name = 'DiscoveryError';
|
|
17
|
+
}
|
|
18
|
+
async function kind(path) {
|
|
19
|
+
try {
|
|
20
|
+
const info = await stat(path);
|
|
21
|
+
if (info.isFile())
|
|
22
|
+
return 'file';
|
|
23
|
+
return info.isDirectory() ? 'directory' : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function lockfilesIn(directory) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const location of LOCKFILE_LOCATIONS) {
|
|
32
|
+
const candidate = join(directory, location);
|
|
33
|
+
if ((await kind(candidate)) === 'file')
|
|
34
|
+
found.push(candidate);
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Turns the paths given on the command line (files or folders; the current folder when none)
|
|
40
|
+
* into absolute lockfile paths, without duplicates. Throws {@link DiscoveryError} for a path that
|
|
41
|
+
* does not exist or a folder without a lockfile in any of the {@link LOCKFILE_LOCATIONS}.
|
|
42
|
+
*/
|
|
43
|
+
export async function findLockfiles(paths, cwd) {
|
|
44
|
+
const inputs = paths.length > 0 ? paths : ['.'];
|
|
45
|
+
const found = new Set();
|
|
46
|
+
for (const input of inputs) {
|
|
47
|
+
const absolute = resolve(cwd, input);
|
|
48
|
+
const type = await kind(absolute);
|
|
49
|
+
if (type === 'file') {
|
|
50
|
+
found.add(absolute);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (type === null)
|
|
54
|
+
throw new DiscoveryError(`${input}: no such file or folder.`);
|
|
55
|
+
const lockfiles = await lockfilesIn(absolute);
|
|
56
|
+
if (lockfiles.length === 0) {
|
|
57
|
+
const where = relative(cwd, absolute) || 'this folder';
|
|
58
|
+
throw new DiscoveryError(`No Podfile.lock in ${where} (looked for ${LOCKFILE_LOCATIONS.join(', ')}). Run \`pod install\` first, or pass the path to a Podfile.lock.`);
|
|
59
|
+
}
|
|
60
|
+
for (const lockfile of lockfiles)
|
|
61
|
+
found.add(lockfile);
|
|
62
|
+
}
|
|
63
|
+
return [...found];
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=discover.js.map
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from './run.js';
|
|
3
|
+
process.exitCode = await run(process.argv.slice(2), {
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
env: process.env,
|
|
6
|
+
stdout: (text) => process.stdout.write(text),
|
|
7
|
+
stderr: (text) => process.stderr.write(text),
|
|
8
|
+
stdoutIsTty: process.stdout.isTTY === true,
|
|
9
|
+
stderrIsTty: process.stderr.isTTY === true,
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Exit codes; documented in the README and `--help`. */
|
|
2
|
+
export declare const EXIT: {
|
|
3
|
+
readonly ok: 0;
|
|
4
|
+
readonly failure: 1;
|
|
5
|
+
readonly usage: 2;
|
|
6
|
+
};
|
|
7
|
+
/** Everything the CLI touches outside its arguments, so tests can run it in-process. */
|
|
8
|
+
export interface CliEnvironment {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
stdout: (text: string) => void;
|
|
12
|
+
stderr: (text: string) => void;
|
|
13
|
+
/** Whether stdout is a terminal; turns colour on for the tty format. */
|
|
14
|
+
stdoutIsTty: boolean;
|
|
15
|
+
/** Whether stderr is a terminal; progress lines go there only when it is. */
|
|
16
|
+
stderrIsTty: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Runs the CLI with the given arguments (without `node` and the script) and returns the exit code. */
|
|
19
|
+
export declare function run(argv: readonly string[], environment: CliEnvironment): Promise<number>;
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
import { Command, CommanderError, InvalidArgumentError, Option } from 'commander';
|
|
4
|
+
import { analyzeLockfile, buildReport } from '../analyze.js';
|
|
5
|
+
import { LockfileError } from '../core/lockfile.js';
|
|
6
|
+
import { loadPublisherPolicy } from '../data.js';
|
|
7
|
+
import { CachedFetcher, defaultCacheDirectory } from '../net/cache.js';
|
|
8
|
+
import { HttpFetcher, OfflineFetcher } from '../net/fetcher.js';
|
|
9
|
+
import { replaceControlCharactersExceptNewlines } from '../report/escape.js';
|
|
10
|
+
import { FORMATS, render } from '../report/index.js';
|
|
11
|
+
import { REPOSITORY_URL } from '../report/labels.js';
|
|
12
|
+
import { VERSION } from '../version.js';
|
|
13
|
+
import { DiscoveryError, findLockfiles } from './discover.js';
|
|
14
|
+
/** Exit codes; documented in the README and `--help`. */
|
|
15
|
+
export const EXIT = { ok: 0, failure: 1, usage: 2 };
|
|
16
|
+
const HELP_AFTER = `
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 report written
|
|
19
|
+
1 unexpected error
|
|
20
|
+
2 bad arguments, no Podfile.lock found, or a file that is not a Podfile.lock
|
|
21
|
+
|
|
22
|
+
Environment:
|
|
23
|
+
GITHUB_TOKEN, GH_TOKEN sent to GitHub only; needed for private upstream repositories
|
|
24
|
+
PODEXIT_CACHE_DIR cache folder (default: $XDG_CACHE_HOME/podexit or ~/.cache/podexit)
|
|
25
|
+
NO_COLOR, FORCE_COLOR turn terminal colours off or on
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
npx @mcamdali/podexit looks for Podfile.lock, ios/Podfile.lock, example/ios/Podfile.lock
|
|
29
|
+
npx @mcamdali/podexit ios/Podfile.lock --format markdown --output podexit.md
|
|
30
|
+
npx @mcamdali/podexit --format json > podexit.json
|
|
31
|
+
npx @mcamdali/podexit --offline answers only from the cache
|
|
32
|
+
|
|
33
|
+
Docs: ${REPOSITORY_URL}#readme`;
|
|
34
|
+
function positiveInteger(value) {
|
|
35
|
+
const number = Number(value);
|
|
36
|
+
if (!Number.isInteger(number) || number < 1 || number > 64) {
|
|
37
|
+
throw new InvalidArgumentError('Use a whole number from 1 to 64.');
|
|
38
|
+
}
|
|
39
|
+
return number;
|
|
40
|
+
}
|
|
41
|
+
function program(io) {
|
|
42
|
+
return new Command('podexit')
|
|
43
|
+
.description('Shows which CocoaPods dependencies stop getting updates when trunk goes read-only on December 2, 2026, and which already ship a Swift package.')
|
|
44
|
+
.version(VERSION, '-v, --version')
|
|
45
|
+
.argument('[paths...]', 'Podfile.lock files or project folders (default: current folder)')
|
|
46
|
+
.addOption(new Option('-f, --format <format>', 'output format').choices(FORMATS).default('tty'))
|
|
47
|
+
.option('-o, --output <file>', 'write the report to a file instead of stdout')
|
|
48
|
+
.option('-a, --all', 'tty format: also list pods from local paths, git and private specs', false)
|
|
49
|
+
.option('--offline', 'use cached answers only; never touch the network', false)
|
|
50
|
+
.option('--refresh', 'ignore cached answers and ask the network again', false)
|
|
51
|
+
.option('--cache-dir <dir>', 'cache folder', io.env.PODEXIT_CACHE_DIR || undefined)
|
|
52
|
+
.option('--no-color', 'plain text even in a terminal')
|
|
53
|
+
.option('--concurrency <n>', 'parallel pod lookups', positiveInteger, 8)
|
|
54
|
+
.addHelpText('after', HELP_AFTER)
|
|
55
|
+
.showHelpAfterError('(add --help for usage)')
|
|
56
|
+
.exitOverride()
|
|
57
|
+
.configureOutput({
|
|
58
|
+
writeOut: io.stdout,
|
|
59
|
+
writeErr: io.stderr,
|
|
60
|
+
outputError: (text, write) => write(text),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function useColor(options, io) {
|
|
64
|
+
if (options.format !== 'tty' || options.output || !options.color)
|
|
65
|
+
return false;
|
|
66
|
+
if (io.env.NO_COLOR)
|
|
67
|
+
return false;
|
|
68
|
+
if (io.env.FORCE_COLOR && io.env.FORCE_COLOR !== '0')
|
|
69
|
+
return true;
|
|
70
|
+
return io.stdoutIsTty;
|
|
71
|
+
}
|
|
72
|
+
function fetcherFor(options, io) {
|
|
73
|
+
const inner = options.offline
|
|
74
|
+
? new OfflineFetcher()
|
|
75
|
+
: new HttpFetcher({
|
|
76
|
+
userAgent: `@mcamdali/podexit/${VERSION} (+${REPOSITORY_URL})`,
|
|
77
|
+
githubToken: io.env.GITHUB_TOKEN || io.env.GH_TOKEN || undefined,
|
|
78
|
+
});
|
|
79
|
+
const directory = resolve(io.cwd, options.cacheDir ?? defaultCacheDirectory(io.env));
|
|
80
|
+
return new CachedFetcher(inner, { directory, refresh: options.refresh });
|
|
81
|
+
}
|
|
82
|
+
async function analyzeAll(paths, options, io) {
|
|
83
|
+
const lockfiles = await findLockfiles(paths, io.cwd);
|
|
84
|
+
const policy = loadPublisherPolicy();
|
|
85
|
+
const fetcher = fetcherFor(options, io);
|
|
86
|
+
const reports = [];
|
|
87
|
+
for (const path of lockfiles) {
|
|
88
|
+
const display = relative(io.cwd, path) || path;
|
|
89
|
+
if (io.stderrIsTty && !options.offline)
|
|
90
|
+
io.stderr(`Checking ${display}…\n`);
|
|
91
|
+
const text = await readFile(path, 'utf8');
|
|
92
|
+
try {
|
|
93
|
+
reports.push(await analyzeLockfile(text, display, {
|
|
94
|
+
fetcher,
|
|
95
|
+
policy,
|
|
96
|
+
concurrency: options.concurrency,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof LockfileError)
|
|
101
|
+
throw new LockfileError(`${display}: ${error.message}`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const report = buildReport(reports, { policy, offline: options.offline });
|
|
106
|
+
return render(report, options.format, { color: useColor(options, io), all: options.all });
|
|
107
|
+
}
|
|
108
|
+
/** Runs the CLI with the given arguments (without `node` and the script) and returns the exit code. */
|
|
109
|
+
export async function run(argv, environment) {
|
|
110
|
+
// Messages quote paths, arguments and lockfile entries ("Unexpected PODS entry: ..."); an escape
|
|
111
|
+
// sequence in one of them must not reach the terminal, so every write to stderr is cleaned here.
|
|
112
|
+
const io = {
|
|
113
|
+
...environment,
|
|
114
|
+
stderr: (text) => environment.stderr(replaceControlCharactersExceptNewlines(text)),
|
|
115
|
+
};
|
|
116
|
+
const command = program(io);
|
|
117
|
+
try {
|
|
118
|
+
await command.parseAsync([...argv], { from: 'user' });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof CommanderError)
|
|
122
|
+
return error.exitCode === 0 ? EXIT.ok : EXIT.usage;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const options = command.opts();
|
|
126
|
+
try {
|
|
127
|
+
const output = await analyzeAll(command.processedArgs[0], options, io);
|
|
128
|
+
if (options.output) {
|
|
129
|
+
await writeFile(resolve(io.cwd, options.output), output);
|
|
130
|
+
io.stderr(`Wrote ${options.output}\n`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
io.stdout(output);
|
|
134
|
+
}
|
|
135
|
+
return EXIT.ok;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof DiscoveryError || error instanceof LockfileError) {
|
|
139
|
+
io.stderr(`podexit: ${error.message}\n`);
|
|
140
|
+
return EXIT.usage;
|
|
141
|
+
}
|
|
142
|
+
io.stderr(`podexit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
143
|
+
return EXIT.failure;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LockedPod } from './lockfile.js';
|
|
2
|
+
import { type Publisher, type PublisherPolicy } from './policy.js';
|
|
3
|
+
/**
|
|
4
|
+
* What happens to updates for a pod.
|
|
5
|
+
*
|
|
6
|
+
* - `frozen-on-trunk`: published on CocoaPods trunk; after trunk goes read-only no new version
|
|
7
|
+
* can be published there, including security fixes.
|
|
8
|
+
* - `capped`: the publisher already stopped releasing to CocoaPods (for example Firebase after
|
|
9
|
+
* version 12), so the last version is reached before trunk closes.
|
|
10
|
+
* - `not-affected`: comes from a local path, a git repository, a podspec file or a private
|
|
11
|
+
* specs repository; trunk going read-only does not change how it is updated.
|
|
12
|
+
* - `unknown`: the lockfile does not say where the pod comes from.
|
|
13
|
+
*/
|
|
14
|
+
export type UpdateStatus = 'frozen-on-trunk' | 'capped' | 'not-affected' | 'unknown';
|
|
15
|
+
/**
|
|
16
|
+
* Whether the pod's project ships a Swift package.
|
|
17
|
+
*
|
|
18
|
+
* - `spm-available`: the upstream repository has a Package.swift (or the publisher documents one).
|
|
19
|
+
* - `no-spm`: the upstream GitHub repository has no Package.swift on its default branch.
|
|
20
|
+
* - `unknown`: no answer: the source is not on GitHub, or the network was not available.
|
|
21
|
+
* - `not-checked`: local and private sources, which podexit does not look up.
|
|
22
|
+
*/
|
|
23
|
+
export type SwiftPackageStatus = 'spm-available' | 'no-spm' | 'unknown' | 'not-checked';
|
|
24
|
+
export interface UpdateClassification {
|
|
25
|
+
status: UpdateStatus;
|
|
26
|
+
publisher?: Publisher;
|
|
27
|
+
/** For a pod capped as a subdependency: the listed pod that pulls it in. */
|
|
28
|
+
via?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Update status from the lockfile and the publisher policy alone; needs no network. */
|
|
31
|
+
export declare function classifyUpdates(pod: LockedPod, policy: PublisherPolicy): UpdateClassification;
|
|
32
|
+
/**
|
|
33
|
+
* Update status of every pod in a lockfile. Pods are classified one by one, then trunk pods that a
|
|
34
|
+
* capped pod depends on (directly or not) are capped too when the publisher's source says its
|
|
35
|
+
* stop covers subdependencies. Breadth-first from the listed pods, so `via` names the nearest one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyLockfile(pods: readonly LockedPod[], policy: PublisherPolicy): Map<string, UpdateClassification>;
|
|
38
|
+
/** One sentence for each update status, used by every report format. */
|
|
39
|
+
export declare function describeUpdates(status: UpdateStatus, pod: LockedPod, publisher?: Publisher, via?: string): string;
|