@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,71 @@
|
|
|
1
|
+
import { publisherFor } from './policy.js';
|
|
2
|
+
const NOT_AFFECTED = new Set(['path', 'git', 'podspec', 'spec-repo']);
|
|
3
|
+
/** Update status from the lockfile and the publisher policy alone; needs no network. */
|
|
4
|
+
export function classifyUpdates(pod, policy) {
|
|
5
|
+
if (NOT_AFFECTED.has(pod.source.kind))
|
|
6
|
+
return { status: 'not-affected' };
|
|
7
|
+
if (pod.source.kind !== 'trunk')
|
|
8
|
+
return { status: 'unknown' };
|
|
9
|
+
const publisher = publisherFor(policy, pod.name);
|
|
10
|
+
return publisher ? { status: 'capped', publisher } : { status: 'frozen-on-trunk' };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Update status of every pod in a lockfile. Pods are classified one by one, then trunk pods that a
|
|
14
|
+
* capped pod depends on (directly or not) are capped too when the publisher's source says its
|
|
15
|
+
* stop covers subdependencies. Breadth-first from the listed pods, so `via` names the nearest one.
|
|
16
|
+
*/
|
|
17
|
+
export function classifyLockfile(pods, policy) {
|
|
18
|
+
const result = new Map(pods.map((pod) => [pod.name, classifyUpdates(pod, policy)]));
|
|
19
|
+
const byName = new Map(pods.map((pod) => [pod.name, pod]));
|
|
20
|
+
const queue = pods
|
|
21
|
+
.filter((pod) => result.get(pod.name)?.publisher?.includesSubdependencies)
|
|
22
|
+
.map((pod) => ({ pod, root: pod.name }));
|
|
23
|
+
for (let next = queue.shift(); next; next = queue.shift()) {
|
|
24
|
+
const { pod, root } = next;
|
|
25
|
+
const publisher = result.get(root)?.publisher;
|
|
26
|
+
if (!publisher)
|
|
27
|
+
continue;
|
|
28
|
+
for (const name of pod.dependencies) {
|
|
29
|
+
const dependency = byName.get(name);
|
|
30
|
+
if (!dependency || result.get(name)?.status !== 'frozen-on-trunk')
|
|
31
|
+
continue;
|
|
32
|
+
result.set(name, { status: 'capped', publisher, via: root });
|
|
33
|
+
queue.push({ pod: dependency, root });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/** One sentence for each update status, used by every report format. */
|
|
39
|
+
export function describeUpdates(status, pod, publisher, via) {
|
|
40
|
+
switch (status) {
|
|
41
|
+
case 'frozen-on-trunk':
|
|
42
|
+
return 'Published on trunk: no new versions after trunk goes read-only on 2026-12-02.';
|
|
43
|
+
case 'capped':
|
|
44
|
+
if (publisher && via) {
|
|
45
|
+
return `Subdependency of ${via}: ${publisher.name} stops publishing its pods and their subdependencies to CocoaPods (${publisher.stoppedPublishing}).`;
|
|
46
|
+
}
|
|
47
|
+
return publisher
|
|
48
|
+
? `${publisher.name} stopped at ${publisher.lastMajorOnCocoaPods}.x on CocoaPods (${publisher.stoppedPublishing}); newer versions ship through Swift Package Manager.`
|
|
49
|
+
: 'The publisher stopped releasing to CocoaPods.';
|
|
50
|
+
case 'not-affected':
|
|
51
|
+
return describeSource(pod);
|
|
52
|
+
case 'unknown':
|
|
53
|
+
return 'The lockfile does not list a source for this pod.';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function describeSource(pod) {
|
|
57
|
+
const { source } = pod;
|
|
58
|
+
switch (source.kind) {
|
|
59
|
+
case 'path':
|
|
60
|
+
return `Local path (${source.path}); updated with the package that provides it, not through trunk.`;
|
|
61
|
+
case 'git':
|
|
62
|
+
return `Git repository (${source.url}); trunk going read-only does not affect it.`;
|
|
63
|
+
case 'podspec':
|
|
64
|
+
return `Podspec file (${source.url}); trunk going read-only does not affect it.`;
|
|
65
|
+
case 'spec-repo':
|
|
66
|
+
return `Private specs repository (${source.url}); trunk going read-only does not affect it.`;
|
|
67
|
+
default:
|
|
68
|
+
return 'Not published through trunk.';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where CocoaPods resolved a pod from, read from `SPEC REPOS` and `EXTERNAL SOURCES`.
|
|
3
|
+
*
|
|
4
|
+
* - `trunk`: the public CocoaPods Specs repository (CDN or the old git mirror). These pods are
|
|
5
|
+
* the ones that stop getting new versions when trunk goes read-only.
|
|
6
|
+
* - `spec-repo`: a private or third-party specs repository.
|
|
7
|
+
* - `path`, `git`, `podspec`: an external source declared in the Podfile, for example a Flutter
|
|
8
|
+
* plugin under `.symlinks/plugins` or a React Native module under `node_modules`.
|
|
9
|
+
*/
|
|
10
|
+
export type SourceKind = 'trunk' | 'spec-repo' | 'path' | 'git' | 'podspec' | 'unknown';
|
|
11
|
+
export interface PodSource {
|
|
12
|
+
kind: SourceKind;
|
|
13
|
+
/** Specs repository URL (`spec-repo`), git URL (`git`) or podspec location (`podspec`). */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** Local path for `path` sources, as written in the lockfile. */
|
|
16
|
+
path?: string;
|
|
17
|
+
tag?: string;
|
|
18
|
+
branch?: string;
|
|
19
|
+
commit?: string;
|
|
20
|
+
}
|
|
21
|
+
/** One pod from `PODS`, with its subspecs folded into the root pod. */
|
|
22
|
+
export interface LockedPod {
|
|
23
|
+
/** Root pod name: `Firebase` for `Firebase/Analytics`. */
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
/** Subspecs in use, e.g. `["Analytics", "CoreOnly"]`. */
|
|
27
|
+
subspecs: string[];
|
|
28
|
+
/** Root names of the pods this pod depends on. */
|
|
29
|
+
dependencies: string[];
|
|
30
|
+
/** Root names of the pods that depend on this pod. */
|
|
31
|
+
requiredBy: string[];
|
|
32
|
+
/** Listed in the Podfile (`DEPENDENCIES`), as opposed to pulled in by another pod. */
|
|
33
|
+
direct: boolean;
|
|
34
|
+
source: PodSource;
|
|
35
|
+
}
|
|
36
|
+
export interface Lockfile {
|
|
37
|
+
pods: LockedPod[];
|
|
38
|
+
cocoapodsVersion: string | null;
|
|
39
|
+
podfileChecksum: string | null;
|
|
40
|
+
}
|
|
41
|
+
export declare class LockfileError extends Error {
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
/** `Firebase/Analytics` -> `Firebase`. */
|
|
45
|
+
export declare function rootName(name: string): string;
|
|
46
|
+
/** Parses the text of a `Podfile.lock`. Throws {@link LockfileError} when it is not one. */
|
|
47
|
+
export declare function parseLockfile(text: string): Lockfile;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
|
+
export class LockfileError extends Error {
|
|
3
|
+
name = 'LockfileError';
|
|
4
|
+
}
|
|
5
|
+
const NOT_A_LOCKFILE = 'This file has no PODS section, so it is not a Podfile.lock.';
|
|
6
|
+
const PODS_KEY = /^PODS:/m;
|
|
7
|
+
const TRUNK_REPOSITORIES = [
|
|
8
|
+
/^trunk$/i,
|
|
9
|
+
/^https?:\/\/(?:www\.)?github\.com\/cocoapods\/specs(?:\.git)?\/?$/i,
|
|
10
|
+
];
|
|
11
|
+
const ENTRY = /^(?<name>.+?) \((?<version>[^)]+)\)$/;
|
|
12
|
+
/** `Firebase/Analytics` -> `Firebase`. */
|
|
13
|
+
export function rootName(name) {
|
|
14
|
+
return name.split('/')[0] ?? name;
|
|
15
|
+
}
|
|
16
|
+
function dependencyName(requirement) {
|
|
17
|
+
// "FirebaseCore (~> 12.4.0)" or "Flutter (from `Flutter`)" or "Alamofire"
|
|
18
|
+
return rootName(requirement.replace(/\s*\(.*\)\s*$/, '').trim());
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function stringList(value) {
|
|
24
|
+
return Array.isArray(value)
|
|
25
|
+
? value.filter((item) => typeof item === 'string')
|
|
26
|
+
: [];
|
|
27
|
+
}
|
|
28
|
+
function rawPods(value) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
throw new LockfileError('PODS is missing or is not a list.');
|
|
31
|
+
return value.map((item) => {
|
|
32
|
+
if (typeof item === 'string')
|
|
33
|
+
return { entry: item, dependencies: [] };
|
|
34
|
+
if (isRecord(item)) {
|
|
35
|
+
const [entry, dependencies] = Object.entries(item)[0] ?? [];
|
|
36
|
+
if (entry)
|
|
37
|
+
return { entry, dependencies: stringList(dependencies) };
|
|
38
|
+
}
|
|
39
|
+
throw new LockfileError(`Unexpected PODS entry: ${JSON.stringify(item)}`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function specRepos(value) {
|
|
43
|
+
const sources = new Map();
|
|
44
|
+
if (!isRecord(value))
|
|
45
|
+
return sources;
|
|
46
|
+
for (const [repository, pods] of Object.entries(value)) {
|
|
47
|
+
const trunk = TRUNK_REPOSITORIES.some((pattern) => pattern.test(repository));
|
|
48
|
+
const source = trunk ? { kind: 'trunk' } : { kind: 'spec-repo', url: repository };
|
|
49
|
+
for (const pod of stringList(pods))
|
|
50
|
+
sources.set(pod, source);
|
|
51
|
+
}
|
|
52
|
+
return sources;
|
|
53
|
+
}
|
|
54
|
+
function optionalString(record, key) {
|
|
55
|
+
const value = record[key];
|
|
56
|
+
return typeof value === 'string' ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
function externalSource(options) {
|
|
59
|
+
const path = optionalString(options, ':path');
|
|
60
|
+
if (path !== undefined)
|
|
61
|
+
return { kind: 'path', path };
|
|
62
|
+
const git = optionalString(options, ':git');
|
|
63
|
+
if (git !== undefined) {
|
|
64
|
+
const source = { kind: 'git', url: git };
|
|
65
|
+
for (const key of ['tag', 'branch', 'commit']) {
|
|
66
|
+
const value = optionalString(options, `:${key}`);
|
|
67
|
+
if (value !== undefined)
|
|
68
|
+
source[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return source;
|
|
71
|
+
}
|
|
72
|
+
const podspec = optionalString(options, ':podspec');
|
|
73
|
+
if (podspec !== undefined)
|
|
74
|
+
return { kind: 'podspec', url: podspec };
|
|
75
|
+
return { kind: 'unknown' };
|
|
76
|
+
}
|
|
77
|
+
function externalSources(value, checkout) {
|
|
78
|
+
const sources = new Map();
|
|
79
|
+
if (!isRecord(value))
|
|
80
|
+
return sources;
|
|
81
|
+
const pinned = isRecord(checkout) ? checkout : {};
|
|
82
|
+
for (const [pod, options] of Object.entries(value)) {
|
|
83
|
+
if (!isRecord(options))
|
|
84
|
+
continue;
|
|
85
|
+
const source = externalSource(options);
|
|
86
|
+
// CHECKOUT OPTIONS records the commit a branch or tag resolved to at install time.
|
|
87
|
+
const resolved = pinned[pod];
|
|
88
|
+
if (source.kind === 'git' && isRecord(resolved)) {
|
|
89
|
+
const commit = optionalString(resolved, ':commit');
|
|
90
|
+
if (commit !== undefined)
|
|
91
|
+
source.commit = commit;
|
|
92
|
+
}
|
|
93
|
+
sources.set(pod, source);
|
|
94
|
+
}
|
|
95
|
+
return sources;
|
|
96
|
+
}
|
|
97
|
+
function scalar(value) {
|
|
98
|
+
return typeof value === 'string' || typeof value === 'number' ? String(value) : null;
|
|
99
|
+
}
|
|
100
|
+
/** Parses the text of a `Podfile.lock`. Throws {@link LockfileError} when it is not one. */
|
|
101
|
+
export function parseLockfile(text) {
|
|
102
|
+
let document;
|
|
103
|
+
try {
|
|
104
|
+
// The failsafe schema keeps every scalar a string, so `COCOAPODS: 1.10` stays "1.10".
|
|
105
|
+
document = parseYaml(text, { schema: 'failsafe' });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// A README or a CI config often fails as YAML long before it would fail as a lockfile; the
|
|
109
|
+
// parser's message ("Implicit keys need to be on a single line") says nothing useful then.
|
|
110
|
+
if (!PODS_KEY.test(text))
|
|
111
|
+
throw new LockfileError(NOT_A_LOCKFILE);
|
|
112
|
+
const reason = error.message.split('\n')[0];
|
|
113
|
+
throw new LockfileError(`This Podfile.lock is not valid YAML: ${reason}`);
|
|
114
|
+
}
|
|
115
|
+
if (!isRecord(document) || !('PODS' in document))
|
|
116
|
+
throw new LockfileError(NOT_A_LOCKFILE);
|
|
117
|
+
const repos = specRepos(document['SPEC REPOS']);
|
|
118
|
+
const external = externalSources(document['EXTERNAL SOURCES'], document['CHECKOUT OPTIONS']);
|
|
119
|
+
const direct = new Set(stringList(document.DEPENDENCIES).map(dependencyName));
|
|
120
|
+
const pods = new Map();
|
|
121
|
+
for (const { entry, dependencies } of rawPods(document.PODS)) {
|
|
122
|
+
const match = ENTRY.exec(entry.trim());
|
|
123
|
+
if (!match?.groups)
|
|
124
|
+
throw new LockfileError(`Unexpected PODS entry: ${entry}`);
|
|
125
|
+
const fullName = match.groups.name ?? '';
|
|
126
|
+
const name = rootName(fullName);
|
|
127
|
+
const pod = pods.get(name) ?? {
|
|
128
|
+
name,
|
|
129
|
+
version: match.groups.version ?? '',
|
|
130
|
+
subspecs: [],
|
|
131
|
+
dependencies: [],
|
|
132
|
+
requiredBy: [],
|
|
133
|
+
direct: direct.has(name),
|
|
134
|
+
source: external.get(name) ?? repos.get(name) ?? { kind: 'unknown' },
|
|
135
|
+
};
|
|
136
|
+
const subspec = fullName.slice(name.length + 1);
|
|
137
|
+
if (subspec && !pod.subspecs.includes(subspec))
|
|
138
|
+
pod.subspecs.push(subspec);
|
|
139
|
+
for (const dependency of dependencies.map(dependencyName)) {
|
|
140
|
+
if (dependency !== name && !pod.dependencies.includes(dependency))
|
|
141
|
+
pod.dependencies.push(dependency);
|
|
142
|
+
}
|
|
143
|
+
pods.set(name, pod);
|
|
144
|
+
}
|
|
145
|
+
for (const pod of pods.values()) {
|
|
146
|
+
for (const dependency of pod.dependencies) {
|
|
147
|
+
const target = pods.get(dependency);
|
|
148
|
+
if (target && !target.requiredBy.includes(pod.name))
|
|
149
|
+
target.requiredBy.push(pod.name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const sorted = [...pods.values()].sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
153
|
+
for (const pod of sorted) {
|
|
154
|
+
pod.subspecs.sort();
|
|
155
|
+
pod.dependencies.sort();
|
|
156
|
+
pod.requiredBy.sort();
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
pods: sorted,
|
|
160
|
+
cocoapodsVersion: scalar(document.COCOAPODS),
|
|
161
|
+
podfileChecksum: scalar(document['PODFILE CHECKSUM']),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=lockfile.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A publisher that stopped releasing to CocoaPods before trunk itself goes read-only, with the
|
|
4
|
+
* page that says so. Every entry must cite its source.
|
|
5
|
+
*/
|
|
6
|
+
export declare const Publisher: z.ZodObject<{
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
pods: z.ZodArray<z.ZodString>;
|
|
10
|
+
lastMajorOnCocoaPods: z.ZodNumber;
|
|
11
|
+
stoppedPublishing: z.ZodString;
|
|
12
|
+
summary: z.ZodString;
|
|
13
|
+
includesSubdependencies: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
swiftPackage: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
url: z.ZodURL;
|
|
16
|
+
note: z.ZodString;
|
|
17
|
+
}, z.core.$strip>>;
|
|
18
|
+
source: z.ZodURL;
|
|
19
|
+
checked: z.ZodISODate;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type Publisher = z.infer<typeof Publisher>;
|
|
22
|
+
export declare const PublisherPolicy: z.ZodObject<{
|
|
23
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
24
|
+
dataVersion: z.ZodString;
|
|
25
|
+
publishers: z.ZodArray<z.ZodObject<{
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
pods: z.ZodArray<z.ZodString>;
|
|
29
|
+
lastMajorOnCocoaPods: z.ZodNumber;
|
|
30
|
+
stoppedPublishing: z.ZodString;
|
|
31
|
+
summary: z.ZodString;
|
|
32
|
+
includesSubdependencies: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
swiftPackage: z.ZodOptional<z.ZodObject<{
|
|
34
|
+
url: z.ZodURL;
|
|
35
|
+
note: z.ZodString;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
source: z.ZodURL;
|
|
38
|
+
checked: z.ZodISODate;
|
|
39
|
+
}, z.core.$strip>>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type PublisherPolicy = z.infer<typeof PublisherPolicy>;
|
|
42
|
+
/** Validates policy data; throws a zod error naming the offending field. */
|
|
43
|
+
export declare function parsePublisherPolicy(data: unknown): PublisherPolicy;
|
|
44
|
+
/** The publisher whose policy covers a root pod name, if any. */
|
|
45
|
+
export declare function publisherFor(policy: PublisherPolicy, podName: string): Publisher | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const httpsUrl = z.url({ protocol: /^https$/ });
|
|
3
|
+
const isoDate = z.iso.date();
|
|
4
|
+
/**
|
|
5
|
+
* A publisher that stopped releasing to CocoaPods before trunk itself goes read-only, with the
|
|
6
|
+
* page that says so. Every entry must cite its source.
|
|
7
|
+
*/
|
|
8
|
+
export const Publisher = z.object({
|
|
9
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
10
|
+
name: z.string().min(1),
|
|
11
|
+
pods: z.array(z.string().min(1)).min(1),
|
|
12
|
+
lastMajorOnCocoaPods: z.number().int().nonnegative(),
|
|
13
|
+
/** Month the last CocoaPods release is expected, `YYYY-MM`. */
|
|
14
|
+
stoppedPublishing: z.string().regex(/^\d{4}-\d{2}$/),
|
|
15
|
+
summary: z.string().min(1),
|
|
16
|
+
/**
|
|
17
|
+
* True when the source says the stop also covers the pods these pods depend on (Firebase:
|
|
18
|
+
* "all of the following pods and their subdependencies"). Those pods are then capped too.
|
|
19
|
+
*/
|
|
20
|
+
includesSubdependencies: z.boolean().optional(),
|
|
21
|
+
swiftPackage: z.object({ url: httpsUrl, note: z.string().min(1) }).optional(),
|
|
22
|
+
source: httpsUrl,
|
|
23
|
+
/** Date someone last read the source and confirmed the entry. */
|
|
24
|
+
checked: isoDate,
|
|
25
|
+
});
|
|
26
|
+
export const PublisherPolicy = z.object({
|
|
27
|
+
$schema: z.string().optional(),
|
|
28
|
+
/** Version of the data, separate from the package version: `YYYY.MM.DD` of the last change. */
|
|
29
|
+
dataVersion: z.string().regex(/^\d{4}\.\d{2}\.\d{2}$/),
|
|
30
|
+
publishers: z.array(Publisher),
|
|
31
|
+
});
|
|
32
|
+
/** Validates policy data; throws a zod error naming the offending field. */
|
|
33
|
+
export function parsePublisherPolicy(data) {
|
|
34
|
+
return PublisherPolicy.parse(data);
|
|
35
|
+
}
|
|
36
|
+
/** The publisher whose policy covers a root pod name, if any. */
|
|
37
|
+
export function publisherFor(policy, podName) {
|
|
38
|
+
return policy.publishers.find((publisher) => publisher.pods.includes(podName));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LockedPod } from './lockfile.js';
|
|
2
|
+
/** The kind of project a Podfile.lock belongs to; it decides which migration guide applies. */
|
|
3
|
+
export type ProjectKind = 'native' | 'flutter-app' | 'flutter-plugin' | 'react-native' | 'expo';
|
|
4
|
+
export declare const GUIDES: Record<ProjectKind, {
|
|
5
|
+
title: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Detects the project kind from the pods CocoaPods installed: Flutter apps have a `Flutter` pod
|
|
10
|
+
* from a local path, React Native apps have `React-Core`, and Expo apps add `ExpoModulesCore`.
|
|
11
|
+
* A Flutter lockfile under `example/ios/` is a plugin's example app.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectProjectKind(pods: readonly LockedPod[], lockfilePath?: string): ProjectKind;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const GUIDES = {
|
|
2
|
+
native: { title: 'Native iOS: CocoaPods to SwiftPM', path: 'docs/guides/native-ios.md' },
|
|
3
|
+
'flutter-app': { title: 'Flutter apps and plugins', path: 'docs/guides/flutter.md' },
|
|
4
|
+
'flutter-plugin': { title: 'Flutter apps and plugins', path: 'docs/guides/flutter.md' },
|
|
5
|
+
'react-native': { title: 'React Native and Expo', path: 'docs/guides/react-native-expo.md' },
|
|
6
|
+
expo: { title: 'React Native and Expo', path: 'docs/guides/react-native-expo.md' },
|
|
7
|
+
};
|
|
8
|
+
const EXAMPLE_APP = /(?:^|[\\/])example[\\/]ios[\\/]Podfile\.lock$/;
|
|
9
|
+
/**
|
|
10
|
+
* Detects the project kind from the pods CocoaPods installed: Flutter apps have a `Flutter` pod
|
|
11
|
+
* from a local path, React Native apps have `React-Core`, and Expo apps add `ExpoModulesCore`.
|
|
12
|
+
* A Flutter lockfile under `example/ios/` is a plugin's example app.
|
|
13
|
+
*/
|
|
14
|
+
export function detectProjectKind(pods, lockfilePath = '') {
|
|
15
|
+
const names = new Set(pods.map(({ name }) => name));
|
|
16
|
+
const flutter = pods.some(({ name, source }) => name === 'Flutter' && source.kind === 'path');
|
|
17
|
+
if (flutter)
|
|
18
|
+
return EXAMPLE_APP.test(lockfilePath) ? 'flutter-plugin' : 'flutter-app';
|
|
19
|
+
if (names.has('ExpoModulesCore') || names.has('Expo'))
|
|
20
|
+
return 'expo';
|
|
21
|
+
if (names.has('React-Core') || names.has('React'))
|
|
22
|
+
return 'react-native';
|
|
23
|
+
return 'native';
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Dates from the CocoaPods announcement. They are the maintainers' plan, not a guarantee. */
|
|
2
|
+
export declare const TIMELINE: {
|
|
3
|
+
/** "I will trigger a test run, giving automation a chance to break early." */
|
|
4
|
+
readonly trialRun: {
|
|
5
|
+
readonly from: '2026-11-01';
|
|
6
|
+
readonly to: '2026-11-07';
|
|
7
|
+
};
|
|
8
|
+
/** "I will switch trunk to not accept new Podspecs permanently." */
|
|
9
|
+
readonly readOnly: '2026-12-02';
|
|
10
|
+
readonly source: 'https://blog.cocoapods.org/CocoaPods-Specs-Repo/';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Printed with every report, so "frozen" is never read as "broken". From the same announcement:
|
|
14
|
+
* the Specs repo and CDN keep serving, and existing builds keep working.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BUILDS_KEEP_WORKING = "Builds keep working: pods already on trunk stay installable after December 2, 2026. Trunk only stops accepting new versions.";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Dates from the CocoaPods announcement. They are the maintainers' plan, not a guarantee. */
|
|
2
|
+
export const TIMELINE = {
|
|
3
|
+
/** "I will trigger a test run, giving automation a chance to break early." */
|
|
4
|
+
trialRun: { from: '2026-11-01', to: '2026-11-07' },
|
|
5
|
+
/** "I will switch trunk to not accept new Podspecs permanently." */
|
|
6
|
+
readOnly: '2026-12-02',
|
|
7
|
+
source: 'https://blog.cocoapods.org/CocoaPods-Specs-Repo/',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Printed with every report, so "frozen" is never read as "broken". From the same announcement:
|
|
11
|
+
* the Specs repo and CDN keep serving, and existing builds keep working.
|
|
12
|
+
*/
|
|
13
|
+
export const BUILDS_KEEP_WORKING = 'Builds keep working: pods already on trunk stay installable after December 2, 2026. Trunk only stops accepting new versions.';
|
|
14
|
+
//# sourceMappingURL=timeline.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** True when the version has a letter segment, e.g. `5.0.0-beta.1` or `2.0.0.rc.1`. */
|
|
2
|
+
export declare function isPrerelease(version: string): boolean;
|
|
3
|
+
/** Orders two CocoaPods versions: negative when `left` is older, positive when newer. */
|
|
4
|
+
export declare function compareVersions(left: string, right: string): number;
|
|
5
|
+
/**
|
|
6
|
+
* The newest release in a list of versions. Pre-releases count only when the list has nothing
|
|
7
|
+
* else, so a `6.0.0-beta.1` does not hide the `5.10.2` most projects should compare against.
|
|
8
|
+
*/
|
|
9
|
+
export declare function latestVersion(versions: readonly string[]): string | null;
|
|
10
|
+
/** Major version number, e.g. 12 for `12.4.0`; null when the version does not start with one. */
|
|
11
|
+
export declare function majorVersion(version: string): number | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// CocoaPods versions look like semver ("5.10.2", "5.0.0-beta.1") but also allow more segments
|
|
2
|
+
// ("2024.10.14.00") and dots before a pre-release tag ("5.0.0.beta.1"). Comparison follows the
|
|
3
|
+
// rules of RubyGems' Gem::Version, which CocoaPods uses: numeric segments compare as numbers,
|
|
4
|
+
// and a segment with letters marks a pre-release that sorts before the release.
|
|
5
|
+
function segments(version) {
|
|
6
|
+
return version
|
|
7
|
+
.trim()
|
|
8
|
+
.split(/[.-]/)
|
|
9
|
+
.filter((part) => part !== '')
|
|
10
|
+
.flatMap((part) => part.match(/\d+|[a-z]+/gi) ?? [])
|
|
11
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part.toLowerCase()));
|
|
12
|
+
}
|
|
13
|
+
/** True when the version has a letter segment, e.g. `5.0.0-beta.1` or `2.0.0.rc.1`. */
|
|
14
|
+
export function isPrerelease(version) {
|
|
15
|
+
return segments(version).some((segment) => typeof segment === 'string');
|
|
16
|
+
}
|
|
17
|
+
function compareSegment(left, right) {
|
|
18
|
+
if (left === right)
|
|
19
|
+
return 0;
|
|
20
|
+
// A missing segment counts as 0 against a number ("1.0" == "1.0.0") and as a release against a
|
|
21
|
+
// pre-release tag ("1.0" > "1.0.beta").
|
|
22
|
+
if (left === undefined)
|
|
23
|
+
return typeof right === 'string' ? 1 : compareSegment(0, right);
|
|
24
|
+
if (right === undefined)
|
|
25
|
+
return -compareSegment(right, left);
|
|
26
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
27
|
+
return Math.sign(left - right);
|
|
28
|
+
if (typeof left === 'string' && typeof right === 'string')
|
|
29
|
+
return left < right ? -1 : 1;
|
|
30
|
+
return typeof left === 'string' ? -1 : 1;
|
|
31
|
+
}
|
|
32
|
+
/** Orders two CocoaPods versions: negative when `left` is older, positive when newer. */
|
|
33
|
+
export function compareVersions(left, right) {
|
|
34
|
+
const a = segments(left);
|
|
35
|
+
const b = segments(right);
|
|
36
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
37
|
+
const result = compareSegment(a[index], b[index]);
|
|
38
|
+
if (result !== 0)
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The newest release in a list of versions. Pre-releases count only when the list has nothing
|
|
45
|
+
* else, so a `6.0.0-beta.1` does not hide the `5.10.2` most projects should compare against.
|
|
46
|
+
*/
|
|
47
|
+
export function latestVersion(versions) {
|
|
48
|
+
const releases = versions.filter((version) => !isPrerelease(version));
|
|
49
|
+
const pool = releases.length > 0 ? releases : versions;
|
|
50
|
+
return pool.reduce((latest, version) => latest === null || compareVersions(version, latest) > 0 ? version : latest, null);
|
|
51
|
+
}
|
|
52
|
+
/** Major version number, e.g. 12 for `12.4.0`; null when the version does not start with one. */
|
|
53
|
+
export function majorVersion(version) {
|
|
54
|
+
const first = segments(version)[0];
|
|
55
|
+
return typeof first === 'number' ? first : null;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=versions.js.map
|
package/dist/data.d.ts
ADDED
package/dist/data.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { parsePublisherPolicy } from './core/policy.js';
|
|
3
|
+
// data/ sits next to src/ and dist/, so this resolves from source and from the published package.
|
|
4
|
+
const POLICY_URL = new URL('../data/publisher-policy.json', import.meta.url);
|
|
5
|
+
/** Reads and validates the publisher policy shipped in the package's data/ folder. */
|
|
6
|
+
export function loadPublisherPolicy(url = POLICY_URL) {
|
|
7
|
+
return parsePublisherPolicy(JSON.parse(readFileSync(url, 'utf8')));
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=data.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic API. The CLI is a thin layer over these functions; the JSON report format is
|
|
3
|
+
* `schemaVersion` 0 and may change before 1.0.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { analyzeLockfile, buildReport, CachedFetcher, HttpFetcher, loadPublisherPolicy, render } from '@mcamdali/podexit';
|
|
7
|
+
*
|
|
8
|
+
* const policy = loadPublisherPolicy();
|
|
9
|
+
* const fetcher = new CachedFetcher(new HttpFetcher({ userAgent: 'my-tool' }), { directory: '.cache' });
|
|
10
|
+
* const lockfile = await analyzeLockfile(text, 'ios/Podfile.lock', { fetcher, policy });
|
|
11
|
+
* console.log(render(buildReport([lockfile], { policy, offline: false }), 'markdown'));
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { type AnalyzeOptions, analyzeLockfile, buildReport, type LockfileReport, type PodReport, type Report, type ReportContext, SCHEMA_VERSION, type Summary, type SwiftPackageReport, summarize, type UpdatesReport, } from './analyze.js';
|
|
17
|
+
export { classifyLockfile, classifyUpdates, describeUpdates, type SwiftPackageStatus, type UpdateClassification, type UpdateStatus, } from './core/classify.js';
|
|
18
|
+
export { type LockedPod, type Lockfile, LockfileError, type PodSource, parseLockfile, rootName, type SourceKind, } from './core/lockfile.js';
|
|
19
|
+
export { parsePublisherPolicy, Publisher, PublisherPolicy, publisherFor } from './core/policy.js';
|
|
20
|
+
export { detectProjectKind, GUIDES, type ProjectKind } from './core/project.js';
|
|
21
|
+
export { BUILDS_KEEP_WORKING, TIMELINE } from './core/timeline.js';
|
|
22
|
+
export { compareVersions, isPrerelease, latestVersion, majorVersion } from './core/versions.js';
|
|
23
|
+
export { loadPublisherPolicy } from './data.js';
|
|
24
|
+
export { type CacheOptions, CachedFetcher, defaultCacheDirectory } from './net/cache.js';
|
|
25
|
+
export { type Fetcher, HttpFetcher, type HttpFetcherOptions, OfflineFetcher, type RequestOptions, type Response, } from './net/fetcher.js';
|
|
26
|
+
export { FORMATS, type Format, render, type TtyOptions } from './report/index.js';
|
|
27
|
+
export { VERSION } from './version.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic API. The CLI is a thin layer over these functions; the JSON report format is
|
|
3
|
+
* `schemaVersion` 0 and may change before 1.0.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { analyzeLockfile, buildReport, CachedFetcher, HttpFetcher, loadPublisherPolicy, render } from '@mcamdali/podexit';
|
|
7
|
+
*
|
|
8
|
+
* const policy = loadPublisherPolicy();
|
|
9
|
+
* const fetcher = new CachedFetcher(new HttpFetcher({ userAgent: 'my-tool' }), { directory: '.cache' });
|
|
10
|
+
* const lockfile = await analyzeLockfile(text, 'ios/Podfile.lock', { fetcher, policy });
|
|
11
|
+
* console.log(render(buildReport([lockfile], { policy, offline: false }), 'markdown'));
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { analyzeLockfile, buildReport, SCHEMA_VERSION, summarize, } from './analyze.js';
|
|
17
|
+
export { classifyLockfile, classifyUpdates, describeUpdates, } from './core/classify.js';
|
|
18
|
+
export { LockfileError, parseLockfile, rootName, } from './core/lockfile.js';
|
|
19
|
+
export { parsePublisherPolicy, Publisher, PublisherPolicy, publisherFor } from './core/policy.js';
|
|
20
|
+
export { detectProjectKind, GUIDES } from './core/project.js';
|
|
21
|
+
export { BUILDS_KEEP_WORKING, TIMELINE } from './core/timeline.js';
|
|
22
|
+
export { compareVersions, isPrerelease, latestVersion, majorVersion } from './core/versions.js';
|
|
23
|
+
export { loadPublisherPolicy } from './data.js';
|
|
24
|
+
export { CachedFetcher, defaultCacheDirectory } from './net/cache.js';
|
|
25
|
+
export { HttpFetcher, OfflineFetcher, } from './net/fetcher.js';
|
|
26
|
+
export { FORMATS, render } from './report/index.js';
|
|
27
|
+
export { VERSION } from './version.js';
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Fetcher, RequestOptions, Response } from './fetcher.js';
|
|
2
|
+
export interface CacheOptions {
|
|
3
|
+
directory: string;
|
|
4
|
+
/** Entries younger than this are used without asking the network. Default: 24 hours. */
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
/** Ignore cached entries and fetch again (the fresh answers are still stored). */
|
|
7
|
+
refresh?: boolean;
|
|
8
|
+
now?: () => Date;
|
|
9
|
+
}
|
|
10
|
+
/** `$XDG_CACHE_HOME/podexit`, or `~/.cache/podexit`. */
|
|
11
|
+
export declare function defaultCacheDirectory(env?: NodeJS.ProcessEnv): string;
|
|
12
|
+
/** Where a URL is stored: one JSON file per request, grouped by host so recordings are readable. */
|
|
13
|
+
export declare function cachePath(directory: string, url: string, method?: 'GET' | 'HEAD'): string;
|
|
14
|
+
/**
|
|
15
|
+
* Disk cache in front of another fetcher. The same files serve as the recorded responses that
|
|
16
|
+
* make `--offline` and the tests work without a network: with an {@link OfflineFetcher} behind
|
|
17
|
+
* it, every answer comes from disk regardless of age.
|
|
18
|
+
*/
|
|
19
|
+
export declare class CachedFetcher implements Fetcher {
|
|
20
|
+
private readonly inner;
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly ttlMs;
|
|
23
|
+
private readonly now;
|
|
24
|
+
constructor(inner: Fetcher, options: CacheOptions);
|
|
25
|
+
get(url: string, { method }?: RequestOptions): Promise<Response | null>;
|
|
26
|
+
private read;
|
|
27
|
+
private write;
|
|
28
|
+
}
|