@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +254 -0
  3. package/README.es.md +254 -0
  4. package/README.fr.md +254 -0
  5. package/README.ja.md +254 -0
  6. package/README.ko.md +254 -0
  7. package/README.md +297 -2
  8. package/README.pt-BR.md +254 -0
  9. package/README.ru.md +254 -0
  10. package/README.tr.md +254 -0
  11. package/README.zh-CN.md +254 -0
  12. package/data/publisher-policy.json +42 -0
  13. package/data/publisher-policy.schema.json +96 -0
  14. package/dist/analyze.d.ts +20 -0
  15. package/dist/analyze.js +195 -0
  16. package/dist/cli/discover.d.ts +15 -0
  17. package/dist/cli/discover.js +65 -0
  18. package/dist/cli/main.d.ts +2 -0
  19. package/dist/cli/main.js +11 -0
  20. package/dist/cli/run.d.ts +19 -0
  21. package/dist/cli/run.js +146 -0
  22. package/dist/core/classify.d.ts +39 -0
  23. package/dist/core/classify.js +71 -0
  24. package/dist/core/lockfile.d.ts +47 -0
  25. package/dist/core/lockfile.js +164 -0
  26. package/dist/core/policy.d.ts +45 -0
  27. package/dist/core/policy.js +40 -0
  28. package/dist/core/project.d.ts +13 -0
  29. package/dist/core/project.js +25 -0
  30. package/dist/core/timeline.d.ts +16 -0
  31. package/dist/core/timeline.js +14 -0
  32. package/dist/core/versions.d.ts +11 -0
  33. package/dist/core/versions.js +57 -0
  34. package/dist/data.d.ts +3 -0
  35. package/dist/data.js +9 -0
  36. package/dist/index.d.ts +27 -0
  37. package/dist/index.js +28 -0
  38. package/dist/net/cache.d.ts +28 -0
  39. package/dist/net/cache.js +89 -0
  40. package/dist/net/cocoapods.d.ts +34 -0
  41. package/dist/net/cocoapods.js +83 -0
  42. package/dist/net/fetcher.d.ts +33 -0
  43. package/dist/net/fetcher.js +35 -0
  44. package/dist/net/github.d.ts +14 -0
  45. package/dist/net/github.js +42 -0
  46. package/dist/report/escape.d.ts +17 -0
  47. package/dist/report/escape.js +54 -0
  48. package/dist/report/index.d.ts +11 -0
  49. package/dist/report/index.js +21 -0
  50. package/dist/report/labels.d.ts +22 -0
  51. package/dist/report/labels.js +82 -0
  52. package/dist/report/markdown.d.ts +6 -0
  53. package/dist/report/markdown.js +75 -0
  54. package/dist/report/tty.d.ts +12 -0
  55. package/dist/report/tty.js +103 -0
  56. package/dist/report-model.d.ts +75 -0
  57. package/dist/report-model.js +3 -0
  58. package/dist/version.d.ts +2 -0
  59. package/dist/version.js +7 -0
  60. package/package.json +72 -4
@@ -0,0 +1,89 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { z } from 'zod';
6
+ const DAY_MS = 24 * 60 * 60 * 1000;
7
+ const CacheEntry = z.object({
8
+ url: z.string(),
9
+ method: z.enum(['GET', 'HEAD']),
10
+ status: z.number().int(),
11
+ fetchedAt: z.string(),
12
+ body: z.string(),
13
+ });
14
+ /** `$XDG_CACHE_HOME/podexit`, or `~/.cache/podexit`. */
15
+ export function defaultCacheDirectory(env = process.env) {
16
+ const base = env.XDG_CACHE_HOME || join(homedir(), '.cache');
17
+ return join(base, 'podexit');
18
+ }
19
+ /** Where a URL is stored: one JSON file per request, grouped by host so recordings are readable. */
20
+ export function cachePath(directory, url, method = 'GET') {
21
+ const host = new URL(url).hostname;
22
+ const key = createHash('sha256').update(`${method} ${url}`).digest('hex').slice(0, 24);
23
+ return join(directory, host, `${key}.json`);
24
+ }
25
+ // 404 is a real answer ("this repository has no Package.swift"); 5xx and 429 are not.
26
+ function isDefinitive(status) {
27
+ return (status >= 200 && status < 300) || status === 404 || status === 410;
28
+ }
29
+ /**
30
+ * Disk cache in front of another fetcher. The same files serve as the recorded responses that
31
+ * make `--offline` and the tests work without a network: with an {@link OfflineFetcher} behind
32
+ * it, every answer comes from disk regardless of age.
33
+ */
34
+ export class CachedFetcher {
35
+ inner;
36
+ options;
37
+ ttlMs;
38
+ now;
39
+ constructor(inner, options) {
40
+ this.inner = inner;
41
+ this.options = options;
42
+ this.ttlMs = options.ttlMs ?? DAY_MS;
43
+ this.now = options.now ?? (() => new Date());
44
+ }
45
+ async get(url, { method = 'GET' } = {}) {
46
+ const path = cachePath(this.options.directory, url, method);
47
+ const cached = await this.read(path);
48
+ const age = cached
49
+ ? this.now().getTime() - Date.parse(cached.fetchedAt)
50
+ : Number.POSITIVE_INFINITY;
51
+ if (cached && !this.options.refresh && age < this.ttlMs)
52
+ return cached;
53
+ const fresh = await this.inner.get(url, { method });
54
+ if (fresh && isDefinitive(fresh.status)) {
55
+ await this.write(path, {
56
+ url,
57
+ method,
58
+ status: fresh.status,
59
+ fetchedAt: this.now().toISOString(),
60
+ body: fresh.body,
61
+ });
62
+ return fresh;
63
+ }
64
+ // Offline, or the server had a bad moment: an old answer is better than none.
65
+ return cached ?? fresh;
66
+ }
67
+ async read(path) {
68
+ try {
69
+ const parsed = CacheEntry.safeParse(JSON.parse(await readFile(path, 'utf8')));
70
+ return parsed.success ? parsed.data : null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ async write(path, entry) {
77
+ try {
78
+ await mkdir(join(path, '..'), { recursive: true });
79
+ // Write then rename, so a parallel run never reads half a file.
80
+ const temporary = `${path}.${process.pid}.tmp`;
81
+ await writeFile(temporary, `${JSON.stringify(entry, null, 2)}\n`);
82
+ await rename(temporary, path);
83
+ }
84
+ catch {
85
+ // A read-only or full disk only costs the cache, not the run.
86
+ }
87
+ }
88
+ }
89
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import type { Fetcher } from './fetcher.js';
3
+ export declare const CDN = "https://cdn.cocoapods.org";
4
+ export declare const TRUNK_API = "https://trunk.cocoapods.org/api/v1";
5
+ /** The CDN shards pods by the first three hex digits of the MD5 of the pod name. */
6
+ export declare function shard(name: string): [string, string, string];
7
+ /** URL of the CDN index file that lists every published version of `name`. */
8
+ export declare function versionsIndexUrl(name: string): string;
9
+ export declare function podspecUrl(name: string, version: string): string;
10
+ export declare function trunkPodUrl(name: string): string;
11
+ /**
12
+ * Published versions of a pod on trunk, in CDN order (not sorted). Empty when trunk has never
13
+ * heard of the pod; null when the CDN could not be reached.
14
+ */
15
+ export declare function trunkVersions(fetcher: Fetcher, name: string): Promise<string[] | null>;
16
+ declare const Podspec: z.ZodObject<{
17
+ name: z.ZodString;
18
+ version: z.ZodString;
19
+ homepage: z.ZodOptional<z.ZodString>;
20
+ source: z.ZodOptional<z.ZodObject<{
21
+ git: z.ZodOptional<z.ZodString>;
22
+ tag: z.ZodOptional<z.ZodString>;
23
+ http: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$loose>>;
25
+ }, z.core.$loose>;
26
+ export type Podspec = z.infer<typeof Podspec>;
27
+ /** The published podspec of one version, or null when it is not available. */
28
+ export declare function fetchPodspec(fetcher: Fetcher, name: string, version: string): Promise<Podspec | null>;
29
+ /**
30
+ * Publication date of each version, from the trunk API (the CDN has no dates). Keys are versions,
31
+ * values ISO dates. Null when the API could not be reached.
32
+ */
33
+ export declare function releaseDates(fetcher: Fetcher, name: string): Promise<Map<string, string> | null>;
34
+ export {};
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ export const CDN = 'https://cdn.cocoapods.org';
4
+ export const TRUNK_API = 'https://trunk.cocoapods.org/api/v1';
5
+ /** The CDN shards pods by the first three hex digits of the MD5 of the pod name. */
6
+ export function shard(name) {
7
+ const hash = createHash('md5').update(name).digest('hex');
8
+ return [hash.charAt(0), hash.charAt(1), hash.charAt(2)];
9
+ }
10
+ /** URL of the CDN index file that lists every published version of `name`. */
11
+ export function versionsIndexUrl(name) {
12
+ return `${CDN}/all_pods_versions_${shard(name).join('_')}.txt`;
13
+ }
14
+ export function podspecUrl(name, version) {
15
+ const path = [...shard(name), name, version, `${name}.podspec.json`].map(encodeURIComponent);
16
+ return `${CDN}/Specs/${path.join('/')}`;
17
+ }
18
+ export function trunkPodUrl(name) {
19
+ return `${TRUNK_API}/pods/${encodeURIComponent(name)}`;
20
+ }
21
+ /**
22
+ * Published versions of a pod on trunk, in CDN order (not sorted). Empty when trunk has never
23
+ * heard of the pod; null when the CDN could not be reached.
24
+ */
25
+ export async function trunkVersions(fetcher, name) {
26
+ const response = await fetcher.get(versionsIndexUrl(name));
27
+ if (response?.status !== 200)
28
+ return null;
29
+ // Each line is "Name/1.0.0/1.0.1/...".
30
+ const line = response.body.split('\n').find((candidate) => candidate.startsWith(`${name}/`));
31
+ return line ? line.trim().split('/').slice(1).filter(Boolean) : [];
32
+ }
33
+ const Podspec = z.looseObject({
34
+ name: z.string(),
35
+ version: z.string(),
36
+ homepage: z.string().optional(),
37
+ source: z
38
+ .looseObject({
39
+ git: z.string().optional(),
40
+ tag: z.string().optional(),
41
+ http: z.string().optional(),
42
+ })
43
+ .optional(),
44
+ });
45
+ /** The published podspec of one version, or null when it is not available. */
46
+ export async function fetchPodspec(fetcher, name, version) {
47
+ const response = await fetcher.get(podspecUrl(name, version));
48
+ if (response?.status !== 200)
49
+ return null;
50
+ try {
51
+ const parsed = Podspec.safeParse(JSON.parse(response.body));
52
+ return parsed.success ? parsed.data : null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ const TrunkPod = z.object({
59
+ versions: z.array(z.object({ name: z.string(), created_at: z.string() })),
60
+ });
61
+ /**
62
+ * Publication date of each version, from the trunk API (the CDN has no dates). Keys are versions,
63
+ * values ISO dates. Null when the API could not be reached.
64
+ */
65
+ export async function releaseDates(fetcher, name) {
66
+ const response = await fetcher.get(trunkPodUrl(name));
67
+ if (response?.status !== 200)
68
+ return null;
69
+ try {
70
+ const parsed = TrunkPod.safeParse(JSON.parse(response.body));
71
+ if (!parsed.success)
72
+ return null;
73
+ // "2024-11-25 15:15:33 UTC" -> "2024-11-25T15:15:33Z"
74
+ return new Map(parsed.data.versions.map(({ name: version, created_at }) => [
75
+ version,
76
+ new Date(created_at.replace(' UTC', 'Z').replace(' ', 'T')).toISOString(),
77
+ ]));
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ //# sourceMappingURL=cocoapods.js.map
@@ -0,0 +1,33 @@
1
+ /** The answer to one request; only the status and text body are kept. */
2
+ export interface Response {
3
+ status: number;
4
+ body: string;
5
+ }
6
+ export interface RequestOptions {
7
+ /** `HEAD` for existence checks, where only the status matters. */
8
+ method?: 'GET' | 'HEAD';
9
+ }
10
+ /**
11
+ * Everything podexit reads from the network goes through a Fetcher, so tests and `--offline`
12
+ * runs can answer from recorded responses. `null` means the answer is not available (no
13
+ * network, a timeout, or nothing recorded); callers report that as "unknown" instead of failing.
14
+ */
15
+ export interface Fetcher {
16
+ get(url: string, options?: RequestOptions): Promise<Response | null>;
17
+ }
18
+ export interface HttpFetcherOptions {
19
+ userAgent: string;
20
+ /** Sent to github.com hosts only; raises the GitHub rate limit and allows private repositories. */
21
+ githubToken?: string | undefined;
22
+ timeoutMs?: number;
23
+ }
24
+ /** Fetcher backed by the platform `fetch`. Redirects are followed (the CDN redirects to jsDelivr). */
25
+ export declare class HttpFetcher implements Fetcher {
26
+ private readonly options;
27
+ constructor(options: HttpFetcherOptions);
28
+ get(url: string, { method }?: RequestOptions): Promise<Response | null>;
29
+ }
30
+ /** Fetcher that never touches the network; used with `--offline` behind the cache. */
31
+ export declare class OfflineFetcher implements Fetcher {
32
+ get(): Promise<Response | null>;
33
+ }
@@ -0,0 +1,35 @@
1
+ const GITHUB_HOSTS = new Set(['api.github.com', 'raw.githubusercontent.com']);
2
+ const DEFAULT_TIMEOUT_MS = 15_000;
3
+ /** Fetcher backed by the platform `fetch`. Redirects are followed (the CDN redirects to jsDelivr). */
4
+ export class HttpFetcher {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async get(url, { method = 'GET' } = {}) {
10
+ const headers = { 'user-agent': this.options.userAgent };
11
+ if (this.options.githubToken && GITHUB_HOSTS.has(new URL(url).hostname)) {
12
+ headers.authorization = `Bearer ${this.options.githubToken}`;
13
+ }
14
+ try {
15
+ const response = await fetch(url, {
16
+ method,
17
+ headers,
18
+ redirect: 'follow',
19
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
20
+ });
21
+ const body = method === 'HEAD' ? '' : await response.text();
22
+ return { status: response.status, body };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ }
29
+ /** Fetcher that never touches the network; used with `--offline` behind the cache. */
30
+ export class OfflineFetcher {
31
+ async get() {
32
+ return null;
33
+ }
34
+ }
35
+ //# sourceMappingURL=fetcher.js.map
@@ -0,0 +1,14 @@
1
+ import type { Fetcher } from './fetcher.js';
2
+ export interface GitHubRepository {
3
+ owner: string;
4
+ name: string;
5
+ }
6
+ /** Owner and name from a GitHub URL in any of the forms podspecs use; null for other hosts. */
7
+ export declare function parseGitHubUrl(url: string | undefined): GitHubRepository | null;
8
+ export declare function repositoryUrl({ owner, name }: GitHubRepository): string;
9
+ /**
10
+ * Whether `Package.swift` exists at `ref` (a branch, tag or `HEAD` for the default branch).
11
+ * Uses raw.githubusercontent.com, which does not count against the REST API's 60 requests per
12
+ * hour. Null when the answer is not available.
13
+ */
14
+ export declare function hasPackageManifest(fetcher: Fetcher, repository: GitHubRepository, ref?: string): Promise<boolean | null>;
@@ -0,0 +1,42 @@
1
+ const PATTERNS = [
2
+ // https://github.com/Alamofire/Alamofire.git, http://www.github.com/x/y/, git://github.com/x/y
3
+ /^(?:git\+)?(?:https?|git|ssh):\/\/(?:[^@/]+@)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+?)(?:\.git)?\/?(?:[#?].*)?$/i,
4
+ // git@github.com:Alamofire/Alamofire.git
5
+ /^[^@\s]+@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i,
6
+ ];
7
+ /** Owner and name from a GitHub URL in any of the forms podspecs use; null for other hosts. */
8
+ export function parseGitHubUrl(url) {
9
+ if (!url)
10
+ return null;
11
+ for (const pattern of PATTERNS) {
12
+ const match = pattern.exec(url.trim());
13
+ if (match?.[1] && match[2])
14
+ return { owner: match[1], name: match[2] };
15
+ }
16
+ return null;
17
+ }
18
+ export function repositoryUrl({ owner, name }) {
19
+ return `https://github.com/${owner}/${name}`;
20
+ }
21
+ /**
22
+ * Whether `Package.swift` exists at `ref` (a branch, tag or `HEAD` for the default branch).
23
+ * Uses raw.githubusercontent.com, which does not count against the REST API's 60 requests per
24
+ * hour. Null when the answer is not available.
25
+ */
26
+ export async function hasPackageManifest(fetcher, repository, ref = 'HEAD') {
27
+ // Tags such as `CocoaPods-12.4.0` pass through; a branch name may contain slashes.
28
+ const owner = encodeURIComponent(repository.owner);
29
+ const name = encodeURIComponent(repository.name);
30
+ const path = `${owner}/${name}/${encodeURI(ref)}`;
31
+ const response = await fetcher.get(`https://raw.githubusercontent.com/${path}/Package.swift`, {
32
+ method: 'HEAD',
33
+ });
34
+ if (!response)
35
+ return null;
36
+ if (response.status === 200)
37
+ return true;
38
+ if (response.status === 404)
39
+ return false;
40
+ return null;
41
+ }
42
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1,17 @@
1
+ /** Replaces control characters with U+FFFD, so they show up instead of acting on the terminal. */
2
+ export declare const replaceControlCharacters: (text: string) => string;
3
+ /** For messages on stderr: replaces control characters with U+FFFD and keeps line breaks. */
4
+ export declare const replaceControlCharactersExceptNewlines: (text: string) => string;
5
+ /** A copy of a report (or any part of one) with every string passed through replaceControlCharacters. */
6
+ export declare function withoutControlCharacters<T>(value: T): T;
7
+ /**
8
+ * Markdown code span for text from the lockfile or the registries. Nothing inside a code span is
9
+ * markup (no HTML, links, autolinks or @mentions), so the text only has to keep the span closed:
10
+ * the fence is one backtick longer than the longest run of backticks in the text, and a space pads
11
+ * text that starts or ends with a backtick or a space (Markdown strips one from each side).
12
+ */
13
+ export declare function markdownCode(text: string): string;
14
+ /** GitHub splits table rows on `|` before anything else, code spans included. */
15
+ export declare const escapeTableCell: (text: string) => string;
16
+ /** An http(s) URL that is safe as a Markdown link destination, or null for anything else. */
17
+ export declare function markdownUrl(text: string): string | null;
@@ -0,0 +1,54 @@
1
+ // Pod names, versions and paths come from the lockfile, the file system and the registries. The
2
+ // helpers here make sure none of them can act on the terminal or on rendered Markdown.
3
+ // C0 controls (tab and newline included), DEL and C1 controls. An escape sequence in a pod name
4
+ // would otherwise reach the terminal as is and could recolour or rewrite what is on screen.
5
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching control characters is the point.
6
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
7
+ /** Replaces control characters with U+FFFD, so they show up instead of acting on the terminal. */
8
+ export const replaceControlCharacters = (text) => text.replace(CONTROL_CHARACTERS, '\ufffd');
9
+ // The same characters except the newline, for messages that may span lines.
10
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching control characters is the point.
11
+ const CONTROL_CHARACTERS_EXCEPT_NEWLINE = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g;
12
+ /** For messages on stderr: replaces control characters with U+FFFD and keeps line breaks. */
13
+ export const replaceControlCharactersExceptNewlines = (text) => text.replace(CONTROL_CHARACTERS_EXCEPT_NEWLINE, '\ufffd');
14
+ /** A copy of a report (or any part of one) with every string passed through replaceControlCharacters. */
15
+ export function withoutControlCharacters(value) {
16
+ if (typeof value === 'string')
17
+ return replaceControlCharacters(value);
18
+ if (Array.isArray(value))
19
+ return value.map(withoutControlCharacters);
20
+ if (value === null || typeof value !== 'object')
21
+ return value;
22
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, withoutControlCharacters(item)]));
23
+ }
24
+ const BACKTICK_RUNS = /`+/g;
25
+ /**
26
+ * Markdown code span for text from the lockfile or the registries. Nothing inside a code span is
27
+ * markup (no HTML, links, autolinks or @mentions), so the text only has to keep the span closed:
28
+ * the fence is one backtick longer than the longest run of backticks in the text, and a space pads
29
+ * text that starts or ends with a backtick or a space (Markdown strips one from each side).
30
+ */
31
+ export function markdownCode(text) {
32
+ if (!text)
33
+ return '';
34
+ const longestRun = Math.max(0, ...(text.match(BACKTICK_RUNS) ?? []).map((run) => run.length));
35
+ const fence = '`'.repeat(longestRun + 1);
36
+ const padding = /^[ `]|[ `]$/.test(text) && text.trim() !== '' ? ' ' : '';
37
+ return `${fence}${padding}${text}${padding}${fence}`;
38
+ }
39
+ /** GitHub splits table rows on `|` before anything else, code spans included. */
40
+ export const escapeTableCell = (text) => text.replace(/\|/g, '\\|');
41
+ // Characters a link destination may keep. The rest is percent-encoded, so a URL cannot close the
42
+ // link with `)`, open a new one, start an HTML tag or bring an entity such as `&#47;`.
43
+ const URL_UNSAFE = /[^A-Za-z0-9\-._~:/?#@!$+,;=%]/g;
44
+ const percentEncode = (character) => [...new TextEncoder().encode(character)]
45
+ .map((byte) => `%${byte.toString(16).toUpperCase().padStart(2, '0')}`)
46
+ .join('');
47
+ /** An http(s) URL that is safe as a Markdown link destination, or null for anything else. */
48
+ export function markdownUrl(text) {
49
+ const url = URL.parse(text);
50
+ if (url?.protocol !== 'https:' && url?.protocol !== 'http:')
51
+ return null;
52
+ return url.href.replace(URL_UNSAFE, percentEncode);
53
+ }
54
+ //# sourceMappingURL=escape.js.map
@@ -0,0 +1,11 @@
1
+ import type { Report } from '../analyze.js';
2
+ import { type TtyOptions } from './tty.js';
3
+ export declare const FORMATS: readonly ['tty', 'json', 'markdown'];
4
+ export type Format = (typeof FORMATS)[number];
5
+ /**
6
+ * Renders a report in one of the supported formats. This is the one way out for a report: every
7
+ * string in it has its control characters replaced here, whatever the format, before anything
8
+ * reaches a terminal, a file or a pull request comment.
9
+ */
10
+ export declare function render(input: Report, format: Format, tty?: TtyOptions): string;
11
+ export type { TtyOptions };
@@ -0,0 +1,21 @@
1
+ import { withoutControlCharacters } from './escape.js';
2
+ import { renderMarkdown } from './markdown.js';
3
+ import { renderTty } from './tty.js';
4
+ export const FORMATS = ['tty', 'json', 'markdown'];
5
+ /**
6
+ * Renders a report in one of the supported formats. This is the one way out for a report: every
7
+ * string in it has its control characters replaced here, whatever the format, before anything
8
+ * reaches a terminal, a file or a pull request comment.
9
+ */
10
+ export function render(input, format, tty = { color: false, all: false }) {
11
+ const report = withoutControlCharacters(input);
12
+ switch (format) {
13
+ case 'json':
14
+ return `${JSON.stringify(report, null, 2)}\n`;
15
+ case 'markdown':
16
+ return renderMarkdown(report);
17
+ case 'tty':
18
+ return renderTty(report, tty);
19
+ }
20
+ }
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import type { PodReport, Summary } from '../analyze.js';
2
+ export declare const REPOSITORY_URL = "https://github.com/muhittincamdali/podexit";
3
+ /** Short label for the UPDATES column. */
4
+ export declare function updatesLabel(pod: PodReport): string;
5
+ /** Short label for the SWIFT PACKAGE column. */
6
+ export declare function swiftPackageLabel(pod: PodReport): string;
7
+ /** Display order: capped, frozen, unknown, not affected; Podfile pods before transitive ones. */
8
+ export declare function orderPods(pods: readonly PodReport[]): PodReport[];
9
+ /** "FirebaseAnalytics" or "FirebaseAnalytics +2" for pods pulled in by others. */
10
+ export declare function requiredByLabel(pod: PodReport): string;
11
+ export declare function guideUrl(path: string): string;
12
+ export declare const plural: (count: number, word: string) => string;
13
+ /** Two summary lines shared by the terminal and Markdown reports. */
14
+ export declare function summaryLines(summary: Summary): [string, string];
15
+ export declare function podsHeadline(summary: Summary): string;
16
+ export declare const PROJECT_LABELS: {
17
+ readonly native: 'native iOS app';
18
+ readonly 'flutter-app': 'Flutter app';
19
+ readonly 'flutter-plugin': 'Flutter plugin example app';
20
+ readonly 'react-native': 'React Native app';
21
+ readonly expo: 'Expo app';
22
+ };
@@ -0,0 +1,82 @@
1
+ export const REPOSITORY_URL = 'https://github.com/muhittincamdali/podexit';
2
+ /** Short label for the UPDATES column. */
3
+ export function updatesLabel(pod) {
4
+ switch (pod.updates.status) {
5
+ case 'frozen-on-trunk':
6
+ return 'frozen after Dec 2';
7
+ case 'capped':
8
+ if (pod.updates.via)
9
+ return `capped with ${pod.updates.publisher?.name ?? '?'}`;
10
+ return `capped at ${pod.updates.publisher?.lastMajorOnCocoaPods ?? '?'}.x`;
11
+ case 'not-affected':
12
+ return 'not affected';
13
+ case 'unknown':
14
+ return 'unknown source';
15
+ }
16
+ }
17
+ /** Short label for the SWIFT PACKAGE column. */
18
+ export function swiftPackageLabel(pod) {
19
+ const { status, via, atLockedVersion } = pod.swiftPackage;
20
+ switch (status) {
21
+ case 'spm-available':
22
+ if (via === 'publisher')
23
+ return `yes (${pod.updates.publisher?.name ?? 'publisher'})`;
24
+ return atLockedVersion === false ? 'yes, newer versions' : 'yes';
25
+ case 'no-spm':
26
+ return 'no';
27
+ case 'unknown':
28
+ return 'unknown';
29
+ case 'not-checked':
30
+ return '-';
31
+ }
32
+ }
33
+ const STATUS_ORDER = {
34
+ capped: 0,
35
+ 'frozen-on-trunk': 1,
36
+ unknown: 2,
37
+ 'not-affected': 3,
38
+ };
39
+ /** Display order: capped, frozen, unknown, not affected; Podfile pods before transitive ones. */
40
+ export function orderPods(pods) {
41
+ return [...pods].sort((a, b) => STATUS_ORDER[a.updates.status] - STATUS_ORDER[b.updates.status] ||
42
+ Number(b.direct) - Number(a.direct) ||
43
+ a.name.localeCompare(b.name, 'en'));
44
+ }
45
+ /** "FirebaseAnalytics" or "FirebaseAnalytics +2" for pods pulled in by others. */
46
+ export function requiredByLabel(pod) {
47
+ if (pod.direct || pod.requiredBy.length === 0)
48
+ return '';
49
+ const [first, ...rest] = pod.requiredBy;
50
+ return rest.length > 0 ? `${first} +${rest.length}` : (first ?? '');
51
+ }
52
+ export function guideUrl(path) {
53
+ return `${REPOSITORY_URL}/blob/main/${path}`;
54
+ }
55
+ export const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
56
+ /** Two summary lines shared by the terminal and Markdown reports. */
57
+ export function summaryLines(summary) {
58
+ return [
59
+ [
60
+ `${summary.frozenOnTrunk} frozen on trunk`,
61
+ `${summary.capped} capped by the publisher`,
62
+ `${summary.notAffected} not affected`,
63
+ ...(summary.unknown ? [`${summary.unknown} without a source`] : []),
64
+ ].join(' · '),
65
+ [
66
+ `${plural(summary.spmAvailable, 'pod')} with a Swift package`,
67
+ `${summary.noSpm} without`,
68
+ `${summary.spmUnknown} unknown`,
69
+ ].join(' · '),
70
+ ];
71
+ }
72
+ export function podsHeadline(summary) {
73
+ return `${plural(summary.pods, 'pod')}, ${summary.direct} in the Podfile`;
74
+ }
75
+ export const PROJECT_LABELS = {
76
+ native: 'native iOS app',
77
+ 'flutter-app': 'Flutter app',
78
+ 'flutter-plugin': 'Flutter plugin example app',
79
+ 'react-native': 'React Native app',
80
+ expo: 'Expo app',
81
+ };
82
+ //# sourceMappingURL=labels.js.map
@@ -0,0 +1,6 @@
1
+ import type { Report } from '../analyze.js';
2
+ /**
3
+ * GitHub-flavoured Markdown, for pull request comments, issues and MIGRATION notes. Called through
4
+ * `render`, which has already replaced control characters in the report.
5
+ */
6
+ export declare function renderMarkdown(report: Report): string;
@@ -0,0 +1,75 @@
1
+ import { guideUrl, orderPods, PROJECT_LABELS, plural, podsHeadline, requiredByLabel, summaryLines, swiftPackageLabel, updatesLabel, } from './labels.js';
2
+ import { escapeTableCell, markdownCode, markdownUrl } from './escape.js';
3
+ // Names, versions and paths come from the lockfile and the registries, so they go into code spans,
4
+ // where Markdown and HTML have no effect. Labels and links are podexit's own text.
5
+ const code = (text) => escapeTableCell(markdownCode(text));
6
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
7
+ function latestCell(pod) {
8
+ const { latestOnTrunk, latestReleasedAt } = pod.updates;
9
+ if (!latestOnTrunk)
10
+ return '';
11
+ const date = latestReleasedAt?.slice(0, 10) ?? '';
12
+ return DATE.test(date) ? `${code(latestOnTrunk)} (${date})` : code(latestOnTrunk);
13
+ }
14
+ function swiftPackageCell(pod) {
15
+ const label = swiftPackageLabel(pod);
16
+ const url = pod.swiftPackage.repository ? markdownUrl(pod.swiftPackage.repository) : null;
17
+ return url && pod.swiftPackage.status !== 'not-checked' ? `[${label}](${url})` : label;
18
+ }
19
+ function podTable(pods) {
20
+ return [
21
+ '| Pod | Locked | Latest on trunk | Updates | Swift package | Pulled in by |',
22
+ '| --- | --- | --- | --- | --- | --- |',
23
+ ...pods.map((pod) => [
24
+ code(pod.name),
25
+ code(pod.version),
26
+ latestCell(pod),
27
+ updatesLabel(pod),
28
+ swiftPackageCell(pod),
29
+ code(requiredByLabel(pod)),
30
+ ]
31
+ .join(' | ')
32
+ .replace(/^/, '| ')
33
+ .replace(/$/, ' |')),
34
+ ];
35
+ }
36
+ function lockfileSection(lockfile) {
37
+ const ordered = orderPods(lockfile.pods);
38
+ const trunk = ordered.filter((pod) => pod.updates.status !== 'not-affected');
39
+ const other = ordered.filter((pod) => pod.updates.status === 'not-affected');
40
+ const [updates, packages] = summaryLines(lockfile.summary);
41
+ const lines = [
42
+ `## ${markdownCode(lockfile.path)}`,
43
+ '',
44
+ `${PROJECT_LABELS[lockfile.project]}, ${podsHeadline(lockfile.summary)}.`,
45
+ '',
46
+ `- ${updates}`,
47
+ `- ${packages}`,
48
+ `- Guide: [${lockfile.guide.title}](${guideUrl(lockfile.guide.path)})`,
49
+ '',
50
+ ];
51
+ lines.push(...(trunk.length > 0 ? podTable(trunk) : ['No pods from CocoaPods trunk.']), '');
52
+ if (other.length > 0) {
53
+ lines.push('<details>', `<summary>${plural(other.length, 'pod')} from local paths, git or private specs (not affected)</summary>`, '', ...podTable(other), '', '</details>', '');
54
+ }
55
+ return lines;
56
+ }
57
+ /**
58
+ * GitHub-flavoured Markdown, for pull request comments, issues and MIGRATION notes. Called through
59
+ * `render`, which has already replaced control characters in the report.
60
+ */
61
+ export function renderMarkdown(report) {
62
+ const { timeline } = report;
63
+ const lines = [
64
+ '# podexit report',
65
+ '',
66
+ `CocoaPods trunk goes read-only on **${timeline.readOnly}**, after a trial run from ${timeline.trialRun.from} to ${timeline.trialRun.to} ([announcement](${timeline.source})). ${report.notes.join(' ')}`,
67
+ '',
68
+ ];
69
+ for (const lockfile of report.lockfiles)
70
+ lines.push(...lockfileSection(lockfile));
71
+ const offline = report.offline ? ', offline' : '';
72
+ lines.push(`<sub>podexit ${report.tool.version}, ${report.generatedAt.slice(0, 10)}${offline}, publisher data ${report.policyDataVersion}</sub>`);
73
+ return `${lines.join('\n')}\n`;
74
+ }
75
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1,12 @@
1
+ import type { Report } from '../analyze.js';
2
+ export interface TtyOptions {
3
+ /** ANSI colours; the CLI turns them on for terminals unless NO_COLOR is set. */
4
+ color: boolean;
5
+ /** Also list pods that come from local paths, git or private specs. */
6
+ all: boolean;
7
+ }
8
+ /**
9
+ * Plain-text table for terminals. Colours are optional; the text is the same either way. Called
10
+ * through `render`, which has already replaced control characters in the report.
11
+ */
12
+ export declare function renderTty(report: Report, options: TtyOptions): string;