react-native-doctor-ci 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,1677 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli-main.ts
27
+ var import_node_path4 = require("path");
28
+ var import_node_util = require("util");
29
+
30
+ // src/changed-deps.ts
31
+ function diffDependencies(base, current) {
32
+ if (base === null) return current.map((entry) => entry.name);
33
+ const baseSpecs = new Map(base.map((entry) => [entry.name, entry.spec]));
34
+ return current.filter((entry) => baseSpecs.get(entry.name) !== entry.spec).map((entry) => entry.name);
35
+ }
36
+
37
+ // src/cache.ts
38
+ var fs = __toESM(require("fs"), 1);
39
+ var path = __toESM(require("path"), 1);
40
+ var CACHE_VERSION = 1;
41
+ var TTL_MS = 24 * 60 * 60 * 1e3;
42
+ function isDegraded(entry) {
43
+ const transientReasons = /* @__PURE__ */ new Set([
44
+ "github-rate-limited",
45
+ "github-error",
46
+ "npm-lookup-failed"
47
+ ]);
48
+ const dep = entry.enriched;
49
+ const signals = [
50
+ dep.npm.deprecated,
51
+ dep.npm.hasCodegenConfig,
52
+ dep.npm.hasReactNativePeerDep,
53
+ dep.npm.hasNativeDirsHint,
54
+ dep.github.archived,
55
+ dep.github.pushedAt,
56
+ dep.lastPublish
57
+ ];
58
+ return signals.some((s) => !s.known && transientReasons.has(s.reason));
59
+ }
60
+ async function readCacheFile(cacheDir) {
61
+ const cacheFile = path.join(cacheDir, ".rn-doctor-cache.json");
62
+ try {
63
+ const content = await fs.promises.readFile(cacheFile, "utf-8");
64
+ const parsed = JSON.parse(content);
65
+ if (parsed.version !== CACHE_VERSION) {
66
+ return { version: CACHE_VERSION, entries: {} };
67
+ }
68
+ return parsed;
69
+ } catch {
70
+ return { version: CACHE_VERSION, entries: {} };
71
+ }
72
+ }
73
+ async function writeCacheFile(cacheDir, cache) {
74
+ const cacheFile = path.join(cacheDir, ".rn-doctor-cache.json");
75
+ await fs.promises.writeFile(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
76
+ }
77
+ function getFreshEntry(cache, packageName, now = /* @__PURE__ */ new Date()) {
78
+ const entry = cache.entries[packageName];
79
+ if (!entry) {
80
+ return void 0;
81
+ }
82
+ const age = now.getTime() - new Date(entry.fetchedAt).getTime();
83
+ if (age >= TTL_MS || isDegraded(entry)) {
84
+ return void 0;
85
+ }
86
+ return entry;
87
+ }
88
+ function putEntry(cache, entry) {
89
+ return {
90
+ version: CACHE_VERSION,
91
+ entries: {
92
+ ...cache.entries,
93
+ [entry.enriched.name]: entry
94
+ }
95
+ };
96
+ }
97
+ function isEntryDegraded(entry) {
98
+ return isDegraded(entry);
99
+ }
100
+
101
+ // src/concurrency.ts
102
+ async function mapWithConcurrency(items, fn, concurrency = 8) {
103
+ if (items.length === 0) {
104
+ return [];
105
+ }
106
+ const results = Array.from({ length: items.length });
107
+ let index = 0;
108
+ const worker = async () => {
109
+ let currentIndex = index++;
110
+ while (currentIndex < items.length) {
111
+ results[currentIndex] = await fn(items[currentIndex]);
112
+ currentIndex = index++;
113
+ }
114
+ };
115
+ const workers = Array(Math.min(concurrency, items.length)).fill(null).map(() => worker());
116
+ await Promise.all(workers);
117
+ return results;
118
+ }
119
+
120
+ // src/http.ts
121
+ async function fetchJson(url, options = {}) {
122
+ const controller = new AbortController();
123
+ const timeoutMs = options.timeout ?? 1e4;
124
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
125
+ try {
126
+ const response = await fetch(url, {
127
+ headers: {
128
+ "user-agent": "rn-doctor/0.0.0",
129
+ ...options.headers
130
+ },
131
+ signal: controller.signal
132
+ });
133
+ clearTimeout(timeoutHandle);
134
+ if (response.status === 404) {
135
+ return { status: "not-found" };
136
+ }
137
+ if (response.status === 403 || response.status === 429) {
138
+ return { status: "rate-limited" };
139
+ }
140
+ if (!response.ok) {
141
+ return { status: "error", message: `HTTP ${response.status}` };
142
+ }
143
+ const data = await response.json();
144
+ return { status: "ok", data };
145
+ } catch (err) {
146
+ clearTimeout(timeoutHandle);
147
+ if (err instanceof Error && err.name === "AbortError") {
148
+ return { status: "error", message: "Request timeout" };
149
+ }
150
+ if (err instanceof SyntaxError) {
151
+ return { status: "error", message: "Invalid JSON response" };
152
+ }
153
+ return {
154
+ status: "error",
155
+ message: err instanceof Error ? err.message : "Unknown error"
156
+ };
157
+ }
158
+ }
159
+
160
+ // src/sources/directory.ts
161
+ async function checkLibraries(packageNames) {
162
+ if (packageNames.length === 0) {
163
+ return { status: "ok", data: {} };
164
+ }
165
+ const chunks = [];
166
+ for (let i = 0; i < packageNames.length; i += 200) {
167
+ chunks.push(Array.from(packageNames.slice(i, i + 200)));
168
+ }
169
+ const results = {};
170
+ for (const chunk of chunks) {
171
+ const url = new URL("https://reactnative.directory/api/libraries/check");
172
+ url.searchParams.set("packages", chunk.join(","));
173
+ const outcome = await fetchJson(url.toString());
174
+ if (outcome.status !== "ok") {
175
+ return outcome;
176
+ }
177
+ Object.assign(results, outcome.data);
178
+ }
179
+ return { status: "ok", data: results };
180
+ }
181
+ async function fetchLibraryDetail(packageName) {
182
+ const url = new URL("https://reactnative.directory/api/library");
183
+ url.searchParams.set("name", packageName);
184
+ const outcome = await fetchJson(url.toString());
185
+ if (outcome.status !== "ok") {
186
+ return outcome;
187
+ }
188
+ if (Object.keys(outcome.data).length === 0) {
189
+ return { status: "ok", data: {} };
190
+ }
191
+ return outcome;
192
+ }
193
+ async function fetchLibraryDetails(packageNames, concurrency = 8) {
194
+ const outcomes = await mapWithConcurrency(
195
+ packageNames,
196
+ (name) => fetchLibraryDetail(name),
197
+ concurrency
198
+ );
199
+ const results = {};
200
+ for (let i = 0; i < outcomes.length; i++) {
201
+ const outcome = outcomes[i];
202
+ if (outcome?.status === "ok") {
203
+ results[packageNames[i]] = outcome.data;
204
+ }
205
+ }
206
+ return results;
207
+ }
208
+
209
+ // src/sources/npm.ts
210
+ async function fetchNpmLatestManifest(packageName) {
211
+ const encoded = encodeURIComponent(packageName);
212
+ const url = `https://registry.npmjs.org/${encoded}/latest`;
213
+ return fetchJson(url);
214
+ }
215
+ async function searchNpmForPackage(packageName) {
216
+ const url = new URL("https://registry.npmjs.org/-/v1/search");
217
+ url.searchParams.set("text", packageName);
218
+ url.searchParams.set("size", "1");
219
+ const outcome = await fetchJson(url.toString());
220
+ if (outcome.status !== "ok") {
221
+ return outcome;
222
+ }
223
+ const topResult = outcome.data.objects?.[0];
224
+ if (!topResult) {
225
+ return { status: "error", message: "No search results" };
226
+ }
227
+ return {
228
+ status: "ok",
229
+ data: {
230
+ name: topResult.package.name,
231
+ date: topResult.package.date
232
+ }
233
+ };
234
+ }
235
+ function parseGithubUrl(repoUrl) {
236
+ if (!repoUrl) {
237
+ return void 0;
238
+ }
239
+ const match = repoUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
240
+ if (!match || !match[1] || !match[2]) {
241
+ return void 0;
242
+ }
243
+ return { owner: match[1], repo: match[2] };
244
+ }
245
+
246
+ // src/sources/github.ts
247
+ var GitHubCircuitBreaker = class {
248
+ tripped = false;
249
+ /**
250
+ * Check if the circuit breaker is currently tripped.
251
+ */
252
+ isTripped() {
253
+ return this.tripped;
254
+ }
255
+ /**
256
+ * Trip the circuit breaker (call this on a 403/429 response).
257
+ */
258
+ trip() {
259
+ this.tripped = true;
260
+ }
261
+ };
262
+ async function fetchGithubRepo(owner, repo, token) {
263
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
264
+ const headers = {};
265
+ if (token) {
266
+ headers["authorization"] = `Bearer ${token}`;
267
+ }
268
+ return fetchJson(url, { headers });
269
+ }
270
+ function parseGithubUrl2(url) {
271
+ if (!url) {
272
+ return void 0;
273
+ }
274
+ const match = url.match(
275
+ /(?:https:\/\/github\.com|git@github\.com:)\/?([^/]+)\/([^/.]+)(?:\.git|\/|$)/i
276
+ );
277
+ if (!match || !match[1] || !match[2]) {
278
+ return void 0;
279
+ }
280
+ return { owner: match[1], repo: match[2] };
281
+ }
282
+
283
+ // src/enrich.ts
284
+ function computeNewArchTier(dep) {
285
+ const { directoryVerdict, hasCodegenConfig } = dep;
286
+ if (directoryVerdict === "supported" || directoryVerdict === "new-arch-only") {
287
+ return "supported";
288
+ }
289
+ if (directoryVerdict === "unsupported") {
290
+ return "unsupported";
291
+ }
292
+ if (hasCodegenConfig.known && hasCodegenConfig.value) {
293
+ return "passWithNote";
294
+ }
295
+ return "unknown";
296
+ }
297
+ async function enrichDependencies(names, options = {}) {
298
+ const cacheDir = options.cacheDir || process.cwd();
299
+ const concurrency = options.concurrency ?? 8;
300
+ const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
301
+ const warnings = [];
302
+ const enriched = [];
303
+ let cache = { version: 1, entries: {} };
304
+ if (!options.noCache) {
305
+ try {
306
+ cache = await readCacheFile(cacheDir);
307
+ } catch {
308
+ warnings.push({
309
+ source: "cache",
310
+ message: "Failed to read cache file; proceeding without cache"
311
+ });
312
+ }
313
+ }
314
+ const cached = [];
315
+ const toFetch = [];
316
+ for (const name of names) {
317
+ const entry = options.noCache ? void 0 : getFreshEntry(cache, name);
318
+ if (entry) {
319
+ cached.push(entry.enriched);
320
+ } else {
321
+ toFetch.push(name);
322
+ }
323
+ }
324
+ enriched.push(...cached);
325
+ if (toFetch.length === 0) {
326
+ return { dependencies: enriched, warnings };
327
+ }
328
+ const directoryCheckOutcome = await checkLibraries(toFetch);
329
+ const directoryCheckData = directoryCheckOutcome.status === "ok" ? directoryCheckOutcome.data : {};
330
+ if (directoryCheckOutcome.status === "error") {
331
+ warnings.push({
332
+ source: "directory",
333
+ message: `Failed to check RN Directory: ${directoryCheckOutcome.message}`
334
+ });
335
+ }
336
+ const npmManifests = await mapWithConcurrency(
337
+ toFetch,
338
+ async (name) => {
339
+ const outcome = await fetchNpmLatestManifest(name);
340
+ return { name, outcome };
341
+ },
342
+ concurrency
343
+ );
344
+ const npmData = {};
345
+ for (const { name, outcome } of npmManifests) {
346
+ if (outcome.status === "ok") {
347
+ npmData[name] = outcome.data;
348
+ } else if (outcome.status === "not-found") {
349
+ npmData[name] = { found: false };
350
+ } else if (outcome.status === "error") {
351
+ npmData[name] = { error: outcome.message };
352
+ } else if (outcome.status === "rate-limited") {
353
+ npmData[name] = { rateLimited: true };
354
+ }
355
+ }
356
+ const dirListed = toFetch.filter((name) => directoryCheckData[name]);
357
+ const directoryDetails = await (dirListed.length > 0 ? fetchLibraryDetails(dirListed, concurrency) : Promise.resolve({}));
358
+ const dirNotListed = toFetch.filter((name) => !directoryCheckData[name]);
359
+ const npmSearchResults = {};
360
+ for (const name of dirNotListed) {
361
+ const outcome = await searchNpmForPackage(name);
362
+ if (outcome.status === "ok" && outcome.data.name === name) {
363
+ npmSearchResults[name] = outcome.data;
364
+ }
365
+ }
366
+ const githubBreaker = new GitHubCircuitBreaker();
367
+ const githubCalls = [];
368
+ const packagesWithRepoUrl = /* @__PURE__ */ new Set();
369
+ for (const name of toFetch) {
370
+ let repoUrl;
371
+ const dirDetail = directoryDetails[name];
372
+ if (dirDetail && typeof dirDetail === "object" && "githubUrl" in dirDetail) {
373
+ repoUrl = dirDetail.githubUrl;
374
+ }
375
+ if (!repoUrl && npmData[name]?.repository?.url) {
376
+ repoUrl = npmData[name].repository.url;
377
+ }
378
+ if (!repoUrl) {
379
+ continue;
380
+ }
381
+ packagesWithRepoUrl.add(name);
382
+ let parsed = parseGithubUrl2(repoUrl);
383
+ if (!parsed) {
384
+ parsed = parseGithubUrl(repoUrl);
385
+ }
386
+ if (parsed) {
387
+ githubCalls.push({ name, owner: parsed.owner, repo: parsed.repo });
388
+ }
389
+ }
390
+ const githubData = {};
391
+ for (const { name, owner, repo } of githubCalls) {
392
+ if (githubBreaker.isTripped()) {
393
+ break;
394
+ }
395
+ const outcome = await fetchGithubRepo(owner, repo, githubToken);
396
+ if (outcome.status === "ok") {
397
+ githubData[name] = outcome.data;
398
+ } else if (outcome.status === "rate-limited") {
399
+ githubBreaker.trip();
400
+ warnings.push({
401
+ source: "github",
402
+ message: "GitHub API rate-limited after checking dependencies; remaining packages fall back to cached GitHub data or unknown"
403
+ });
404
+ break;
405
+ } else if (outcome.status === "error") {
406
+ githubData[name] = { error: outcome.message };
407
+ }
408
+ }
409
+ for (const name of toFetch) {
410
+ const npm = npmData[name] || {};
411
+ const dirCheck = directoryCheckData[name] || {};
412
+ const listedInDirectory = Object.prototype.hasOwnProperty.call(directoryCheckData, name);
413
+ const dirDetail = directoryDetails[name] || {};
414
+ const github = githubData[name];
415
+ const npmFound = npm.found !== false && !npm.rateLimited && !npm.error;
416
+ const npmNotFound = npm.found === false;
417
+ const deprecated = npmFound ? {
418
+ known: true,
419
+ value: {
420
+ deprecated: Boolean(npm.deprecated),
421
+ message: npm.deprecated ? String(npm.deprecated) : null
422
+ },
423
+ source: "npm"
424
+ } : { known: false, reason: npmNotFound ? "npm-not-found" : "npm-lookup-failed" };
425
+ const hasCodegenConfig = npmFound ? { known: true, value: Boolean(npm.codegenConfig), source: "npm" } : { known: false, reason: npmNotFound ? "npm-not-found" : "npm-lookup-failed" };
426
+ const hasReactNativePeerDep = npmFound ? {
427
+ known: true,
428
+ value: Boolean(npm.peerDependencies?.["react-native"]),
429
+ source: "npm"
430
+ } : { known: false, reason: npmNotFound ? "npm-not-found" : "npm-lookup-failed" };
431
+ const hasNativeDirsHint = npmFound ? {
432
+ known: true,
433
+ value: Boolean(
434
+ npm.files && Array.isArray(npm.files) && npm.files.some((f) => /^(android|ios)(\/.*)?$/.test(f))
435
+ ),
436
+ source: "npm"
437
+ } : { known: false, reason: npmNotFound ? "npm-not-found" : "npm-lookup-failed" };
438
+ let archivedSignal;
439
+ let pushedAtSignal;
440
+ let githubSource = null;
441
+ if (github && !github.error) {
442
+ archivedSignal = { known: true, value: github.archived === true, source: "github-api" };
443
+ if (github.pushed_at) {
444
+ pushedAtSignal = { known: true, value: github.pushed_at, source: "github-api" };
445
+ } else {
446
+ pushedAtSignal = { known: false, reason: "github-error" };
447
+ }
448
+ githubSource = "github-api";
449
+ } else if (dirDetail && typeof dirDetail === "object" && "github" in dirDetail) {
450
+ const dir = dirDetail;
451
+ if (dir.github?.isArchived !== void 0) {
452
+ archivedSignal = { known: true, value: dir.github.isArchived, source: "directory-fallback" };
453
+ if (dir.github.stats?.pushedAt) {
454
+ pushedAtSignal = { known: true, value: dir.github.stats.pushedAt, source: "directory-fallback" };
455
+ } else {
456
+ pushedAtSignal = { known: false, reason: "github-error" };
457
+ }
458
+ githubSource = "directory-fallback";
459
+ } else {
460
+ archivedSignal = { known: false, reason: githubBreaker.isTripped() ? "github-rate-limited" : "no-github-token" };
461
+ pushedAtSignal = { known: false, reason: githubBreaker.isTripped() ? "github-rate-limited" : "no-github-token" };
462
+ }
463
+ } else {
464
+ const noGithubReason = githubBreaker.isTripped() ? "github-rate-limited" : packagesWithRepoUrl.has(name) ? "no-github-token" : "no-repo-url";
465
+ archivedSignal = { known: false, reason: noGithubReason };
466
+ pushedAtSignal = { known: false, reason: noGithubReason };
467
+ }
468
+ let githubUrl = null;
469
+ if (dirDetail && typeof dirDetail === "object" && "githubUrl" in dirDetail) {
470
+ githubUrl = dirDetail.githubUrl || null;
471
+ } else if (npm.repository?.url) {
472
+ const parsed = parseGithubUrl(npm.repository.url) || parseGithubUrl2(npm.repository.url);
473
+ if (parsed) {
474
+ githubUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
475
+ }
476
+ }
477
+ let lastPublish;
478
+ if (dirDetail && typeof dirDetail === "object" && "npm" in dirDetail) {
479
+ const dir = dirDetail;
480
+ if (dir.npm?.latestReleaseDate) {
481
+ lastPublish = { known: true, value: { date: dir.npm.latestReleaseDate }, source: "directory" };
482
+ } else if (npmSearchResults[name]?.date) {
483
+ lastPublish = { known: true, value: { date: npmSearchResults[name].date }, source: "npm-search" };
484
+ } else {
485
+ lastPublish = { known: false, reason: "not-in-directory" };
486
+ }
487
+ } else if (npmSearchResults[name]?.date) {
488
+ lastPublish = { known: true, value: { date: npmSearchResults[name].date }, source: "npm-search" };
489
+ } else {
490
+ lastPublish = { known: false, reason: "not-in-directory" };
491
+ }
492
+ const rnNativeReasons = [];
493
+ if (listedInDirectory) {
494
+ rnNativeReasons.push("directory-listed");
495
+ }
496
+ if (hasReactNativePeerDep.known && hasReactNativePeerDep.value) {
497
+ rnNativeReasons.push("peer-dependency");
498
+ }
499
+ if (hasNativeDirsHint.known && hasNativeDirsHint.value) {
500
+ rnNativeReasons.push("native-files-hint");
501
+ }
502
+ const isRnNative = listedInDirectory || hasReactNativePeerDep.known && hasReactNativePeerDep.value || hasNativeDirsHint.known && hasNativeDirsHint.value;
503
+ const newArchTier = computeNewArchTier({
504
+ directoryVerdict: dirCheck.newArchitecture || null,
505
+ hasCodegenConfig
506
+ });
507
+ const depWarnings = [];
508
+ if (npmNotFound) {
509
+ depWarnings.push({
510
+ source: "npm",
511
+ dependency: name,
512
+ message: `Package not found on npm registry \u2014 verify the package name is correct`
513
+ });
514
+ }
515
+ if (npm.error) {
516
+ depWarnings.push({
517
+ source: "npm",
518
+ dependency: name,
519
+ message: `Failed to fetch npm data: ${npm.error}`
520
+ });
521
+ }
522
+ if (github?.error) {
523
+ depWarnings.push({
524
+ source: "github",
525
+ dependency: name,
526
+ message: `Failed to fetch GitHub data: ${github.error}`
527
+ });
528
+ }
529
+ const enrichedDep = {
530
+ name,
531
+ warnings: depWarnings,
532
+ npm: {
533
+ found: npmFound && !npmNotFound,
534
+ latestVersion: npmFound ? npm.version : null,
535
+ deprecated,
536
+ hasCodegenConfig,
537
+ hasReactNativePeerDep,
538
+ hasNativeDirsHint,
539
+ repositoryUrl: npm.repository?.url ? String(npm.repository.url).replace(/^git\+/, "") : null
540
+ },
541
+ directory: {
542
+ listed: listedInDirectory,
543
+ unmaintained: Boolean(dirCheck.unmaintained),
544
+ newArchitectureRaw: dirCheck.newArchitecture || null,
545
+ // Directory-sourced URL only; the npm-derived fallback belongs to `github.repoUrl`.
546
+ githubUrl: dirDetail && typeof dirDetail === "object" ? dirDetail.githubUrl || null : null,
547
+ lastPublishedAt: dirDetail && typeof dirDetail === "object" && dirDetail.npm?.latestReleaseDate || null,
548
+ // `?? null` (not `|| null`) so a genuine `false` (repo not archived) survives.
549
+ githubArchived: dirDetail && typeof dirDetail === "object" && typeof dirDetail.github?.isArchived === "boolean" ? dirDetail.github.isArchived : null,
550
+ githubPushedAt: dirDetail && typeof dirDetail === "object" && dirDetail.github?.stats?.pushedAt || null,
551
+ matchingScoreModifiers: dirDetail && typeof dirDetail === "object" && dirDetail.matchingScoreModifiers || []
552
+ },
553
+ github: {
554
+ archived: archivedSignal,
555
+ pushedAt: pushedAtSignal,
556
+ repoUrl: githubUrl,
557
+ source: githubSource
558
+ },
559
+ isRnNative,
560
+ rnNativeReasons,
561
+ newArch: {
562
+ tier: newArchTier,
563
+ evidence: {
564
+ directoryVerdict: dirCheck.newArchitecture || null,
565
+ hasCodegenConfig: hasCodegenConfig.known ? hasCodegenConfig.value : null
566
+ }
567
+ },
568
+ lastPublish
569
+ };
570
+ enriched.push(enrichedDep);
571
+ if (!options.noCache) {
572
+ const candidate = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), enriched: enrichedDep };
573
+ if (!isEntryDegraded(candidate)) {
574
+ cache = putEntry(cache, candidate);
575
+ }
576
+ }
577
+ }
578
+ if (!options.noCache && toFetch.length > 0) {
579
+ try {
580
+ await writeCacheFile(cacheDir, cache);
581
+ } catch {
582
+ warnings.push({
583
+ source: "cache",
584
+ message: "Failed to write cache file"
585
+ });
586
+ }
587
+ }
588
+ return { dependencies: enriched, warnings };
589
+ }
590
+
591
+ // src/git.ts
592
+ var import_node_child_process = require("child_process");
593
+ var GitError = class extends Error {
594
+ constructor(message) {
595
+ super(message);
596
+ this.name = "GitError";
597
+ }
598
+ };
599
+ function createGitRunner() {
600
+ return (args, cwd) => new Promise((resolve2, reject) => {
601
+ (0, import_node_child_process.execFile)(
602
+ "git",
603
+ [...args],
604
+ { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
605
+ (err, stdout, stderr) => {
606
+ if (err && err.code === "ENOENT") {
607
+ reject(
608
+ new GitError("git executable not found on PATH \u2014 --changed-only requires git.")
609
+ );
610
+ return;
611
+ }
612
+ const code = err ? err.code ?? 1 : 0;
613
+ resolve2({ code: typeof code === "number" ? code : 1, stdout, stderr });
614
+ }
615
+ );
616
+ });
617
+ }
618
+ async function resolveBaseCommit(git, cwd, baseRef) {
619
+ const result = await git(["merge-base", "HEAD", baseRef], cwd);
620
+ if (result.code === 0) {
621
+ const sha = result.stdout.trim();
622
+ if (sha !== "") return sha;
623
+ }
624
+ const stderr = result.stderr;
625
+ if (/not a git repository/i.test(stderr)) {
626
+ throw new GitError(
627
+ "--changed-only requires running inside a git repository (or drop the flag to check all dependencies)."
628
+ );
629
+ }
630
+ if (/not a valid object name|unknown revision|bad revision|needed a single revision/i.test(stderr)) {
631
+ throw new GitError(
632
+ `base ref "${baseRef}" was not found. Fetch it first (git fetch origin) or pass --base <ref>. In GitHub Actions, check out with fetch-depth: 0.`
633
+ );
634
+ }
635
+ if (stderr.trim() === "") {
636
+ throw new GitError(
637
+ `no merge base between HEAD and "${baseRef}" \u2014 histories are unrelated or the clone is too shallow. In GitHub Actions, check out with fetch-depth: 0.`
638
+ );
639
+ }
640
+ throw new GitError(`git merge-base HEAD ${baseRef} failed \u2014 ${stderr.trim()}`);
641
+ }
642
+ async function readFileAtCommit(git, cwd, commit, relPath) {
643
+ const posixPath = relPath.replaceAll("\\", "/");
644
+ const result = await git(["show", `${commit}:./${posixPath}`], cwd);
645
+ if (result.code === 0) {
646
+ let text = result.stdout;
647
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
648
+ return text;
649
+ }
650
+ if (/does not exist in|exists on disk, but not in|but not in the working tree|path .* does not exist/i.test(result.stderr)) {
651
+ return null;
652
+ }
653
+ throw new GitError(`git show ${commit}:./${posixPath} failed \u2014 ${result.stderr.trim()}`);
654
+ }
655
+
656
+ // src/package-json.ts
657
+ var import_promises = require("fs/promises");
658
+ var import_node_path = require("path");
659
+ var ManifestError = class extends Error {
660
+ constructor(message) {
661
+ super(message);
662
+ this.name = "ManifestError";
663
+ }
664
+ };
665
+ function listDependencyEntries(parsed, where = "package.json") {
666
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
667
+ throw new ManifestError(`${where} is not a JSON object.`);
668
+ }
669
+ const deps = parsed["dependencies"];
670
+ if (deps === void 0) return [];
671
+ if (typeof deps !== "object" || deps === null || Array.isArray(deps)) {
672
+ throw new ManifestError(`${where} has a "dependencies" field that is not an object.`);
673
+ }
674
+ return Object.entries(deps).map(([name, spec]) => {
675
+ if (typeof spec !== "string") {
676
+ throw new ManifestError(`${where} has a non-string version for dependency "${name}".`);
677
+ }
678
+ return { name, spec };
679
+ });
680
+ }
681
+ function entriesFromManifestText(text, where) {
682
+ const stripped = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
683
+ let parsed;
684
+ try {
685
+ parsed = JSON.parse(stripped);
686
+ } catch (err) {
687
+ throw new ManifestError(
688
+ `${where} is not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`
689
+ );
690
+ }
691
+ return listDependencyEntries(parsed, where);
692
+ }
693
+ async function readManifestAt(path2, missingMessage) {
694
+ let text;
695
+ try {
696
+ text = await (0, import_promises.readFile)(path2, "utf8");
697
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
698
+ } catch (err) {
699
+ const code = err.code;
700
+ if (code === "ENOENT") {
701
+ throw new ManifestError(missingMessage ?? `No package.json found at ${path2}.`);
702
+ }
703
+ throw new ManifestError(
704
+ `Could not read ${path2} \u2014 ${err instanceof Error ? err.message : String(err)}`
705
+ );
706
+ }
707
+ let parsed;
708
+ try {
709
+ parsed = JSON.parse(text);
710
+ } catch (err) {
711
+ throw new ManifestError(
712
+ `${path2} is not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`
713
+ );
714
+ }
715
+ const entries = listDependencyEntries(parsed, path2);
716
+ return { path: path2, text, dependencies: entries.map((entry) => entry.name), entries };
717
+ }
718
+ async function readPackageJson(cwd) {
719
+ return readManifestAt(
720
+ (0, import_node_path.join)(cwd, "package.json"),
721
+ `No package.json found in ${cwd}. Run rn-doctor from the project root (the directory containing package.json).`
722
+ );
723
+ }
724
+ function findDependencyLine(text, name) {
725
+ let line = 1;
726
+ let depth = 0;
727
+ let inDependencies = false;
728
+ let i = 0;
729
+ while (i < text.length) {
730
+ const ch = text[i];
731
+ if (ch === "\n") {
732
+ line++;
733
+ i++;
734
+ continue;
735
+ }
736
+ if (ch === '"') {
737
+ const stringLine = line;
738
+ let value = "";
739
+ i++;
740
+ while (i < text.length) {
741
+ const c = text[i];
742
+ if (c === "\\") {
743
+ value += text.slice(i, i + 2);
744
+ i += 2;
745
+ continue;
746
+ }
747
+ if (c === "\n") line++;
748
+ if (c === '"') {
749
+ i++;
750
+ break;
751
+ }
752
+ value += c;
753
+ i++;
754
+ }
755
+ let j = i;
756
+ while (j < text.length && (text[j] === " " || text[j] === " " || text[j] === "\r" || text[j] === "\n")) j++;
757
+ const isKey = text[j] === ":";
758
+ if (isKey) {
759
+ if (depth === 1 && value === "dependencies") {
760
+ let k = j + 1;
761
+ while (k < text.length && (text[k] === " " || text[k] === " " || text[k] === "\r" || text[k] === "\n")) k++;
762
+ if (text[k] === "{") inDependencies = true;
763
+ } else if (inDependencies && depth === 2 && value === name) {
764
+ return stringLine;
765
+ }
766
+ }
767
+ continue;
768
+ }
769
+ if (ch === "{" || ch === "[") {
770
+ depth++;
771
+ } else if (ch === "}" || ch === "]") {
772
+ depth--;
773
+ if (inDependencies && depth <= 1) inDependencies = false;
774
+ }
775
+ i++;
776
+ }
777
+ return null;
778
+ }
779
+
780
+ // src/policy.ts
781
+ var DEFAULT_POLICY = {
782
+ rules: {
783
+ newArchitecture: "error",
784
+ newArchUnknown: "warn",
785
+ lastPublish: { warnMonths: 12, errorMonths: 24 },
786
+ githubArchived: "error",
787
+ npmDeprecated: "error",
788
+ directoryUnmaintained: "warn"
789
+ },
790
+ scope: "rn-native-only",
791
+ allow: []
792
+ };
793
+ var MEAN_MONTH_MS = 365.2425 / 12 * 24 * 60 * 60 * 1e3;
794
+ function allowStateFor(name, allow, now) {
795
+ const entry = allow.find((a) => a.package === name);
796
+ if (!entry) return { kind: "none" };
797
+ if (entry.expires === null) return { kind: "active", entry };
798
+ const expiryEnd = Date.parse(`${entry.expires}T23:59:59.999Z`);
799
+ return now.getTime() <= expiryEnd ? { kind: "active", entry } : { kind: "expired", entry };
800
+ }
801
+ function allowHint(name) {
802
+ return `If this is intentional, allowlist it in .rn-doctor.yml: { package: ${name}, reason: "<why>", expires: YYYY-MM-DD }.`;
803
+ }
804
+ function directoryUrl(name) {
805
+ return `https://reactnative.directory/?search=${encodeURIComponent(name)}`;
806
+ }
807
+ function npmUrl(name) {
808
+ return `https://www.npmjs.com/package/${name}`;
809
+ }
810
+ function evaluateRules(dep, rules, now) {
811
+ const hits = [];
812
+ const name = dep.name;
813
+ if (rules.newArchitecture !== "off") {
814
+ if (dep.newArch.tier === "unsupported") {
815
+ hits.push({
816
+ rule: "newArchitecture",
817
+ severity: rules.newArchitecture,
818
+ message: `${name} does not support the React Native New Architecture (RN Directory verdict: unsupported). Look for a maintained alternative or check the repo for New Architecture plans. ${allowHint(name)}`,
819
+ evidenceUrl: directoryUrl(name)
820
+ });
821
+ } else if (dep.newArch.tier === "passWithNote") {
822
+ hits.push({
823
+ rule: "newArchitecture",
824
+ severity: "note",
825
+ message: `${name} is not listed in the RN Directory, but its npm package ships a codegenConfig, which indicates New Architecture support. Treating as a pass; verify manually if this dependency is critical.`,
826
+ evidenceUrl: npmUrl(name)
827
+ });
828
+ }
829
+ }
830
+ if (rules.newArchUnknown !== "off" && dep.newArch.tier === "unknown") {
831
+ hits.push({
832
+ rule: "newArchUnknown",
833
+ severity: rules.newArchUnknown,
834
+ message: `New Architecture support for ${name} is unknown: it is not listed in the RN Directory and its npm package has no codegen hints. Verify support manually (check the repo README/releases). ${allowHint(name)}`,
835
+ evidenceUrl: directoryUrl(name)
836
+ });
837
+ }
838
+ if (rules.lastPublish !== "off" && dep.lastPublish.known) {
839
+ const publishedAt = Date.parse(dep.lastPublish.value.date);
840
+ if (!Number.isNaN(publishedAt)) {
841
+ const ageMonths = (now.getTime() - publishedAt) / MEAN_MONTH_MS;
842
+ const { warnMonths, errorMonths } = rules.lastPublish;
843
+ const band = ageMonths >= errorMonths ? "error" : ageMonths >= warnMonths ? "warn" : null;
844
+ if (band !== null) {
845
+ const threshold = band === "error" ? errorMonths : warnMonths;
846
+ hits.push({
847
+ rule: "lastPublish",
848
+ severity: band,
849
+ message: `${name} was last published ${String(Math.floor(ageMonths))} months ago (${dep.lastPublish.value.date.slice(0, 10)}), exceeding the ${String(threshold)}-month threshold. Check whether it is abandoned; consider a maintained fork, or let renovate surface a replacement. ` + allowHint(name),
850
+ evidenceUrl: npmUrl(name)
851
+ });
852
+ }
853
+ }
854
+ }
855
+ if (rules.githubArchived !== "off" && dep.github.archived.known && dep.github.archived.value) {
856
+ hits.push({
857
+ rule: "githubArchived",
858
+ severity: rules.githubArchived,
859
+ message: `The GitHub repository for ${name} is archived (read-only): no fixes or releases will land. Replace it or pin a fork. ${allowHint(name)}`,
860
+ evidenceUrl: dep.github.repoUrl ?? npmUrl(name)
861
+ });
862
+ }
863
+ if (rules.npmDeprecated !== "off" && dep.npm.deprecated.known && dep.npm.deprecated.value.deprecated) {
864
+ const upstream = dep.npm.deprecated.value.message;
865
+ hits.push({
866
+ rule: "npmDeprecated",
867
+ severity: rules.npmDeprecated,
868
+ message: `${name} is deprecated on npm` + (upstream ? `: "${upstream}"` : "") + `. Follow the deprecation notice to migrate. ${allowHint(name)}`,
869
+ evidenceUrl: npmUrl(name)
870
+ });
871
+ }
872
+ if (rules.directoryUnmaintained !== "off" && dep.directory.listed && dep.directory.unmaintained) {
873
+ hits.push({
874
+ rule: "directoryUnmaintained",
875
+ severity: rules.directoryUnmaintained,
876
+ message: `${name} is flagged as unmaintained in the RN Directory. Plan a migration to a maintained alternative. ${allowHint(name)}`,
877
+ evidenceUrl: directoryUrl(name)
878
+ });
879
+ }
880
+ return hits;
881
+ }
882
+ function evaluatePolicy(dependencies, policy, options = {}) {
883
+ const now = options.now ?? /* @__PURE__ */ new Date();
884
+ const findings = [];
885
+ for (const dep of dependencies) {
886
+ if (policy.scope === "rn-native-only" && !dep.isRnNative) continue;
887
+ const allowState = allowStateFor(dep.name, policy.allow, now);
888
+ for (const hit of evaluateRules(dep, policy.rules, now)) {
889
+ if (hit.severity === "note" || allowState.kind === "none") {
890
+ findings.push({ package: dep.name, ...hit, suppressedBy: null });
891
+ } else if (allowState.kind === "active") {
892
+ findings.push({
893
+ package: dep.name,
894
+ ...hit,
895
+ suppressedBy: {
896
+ reason: allowState.entry.reason,
897
+ expires: allowState.entry.expires
898
+ }
899
+ });
900
+ } else {
901
+ const { entry } = allowState;
902
+ findings.push({
903
+ package: dep.name,
904
+ rule: hit.rule,
905
+ severity: "error",
906
+ message: `${hit.message} NOTE: the allowlist entry for ${dep.name}` + (entry.reason ? ` ("${entry.reason}")` : "") + ` expired on ${entry.expires ?? "?"} \u2014 escalated to error. Renew the entry with a new expiry, or remove the dependency.`,
907
+ evidenceUrl: hit.evidenceUrl,
908
+ suppressedBy: null
909
+ });
910
+ }
911
+ }
912
+ }
913
+ return findings;
914
+ }
915
+
916
+ // src/policy-file.ts
917
+ var import_promises2 = require("fs/promises");
918
+ var import_node_path2 = require("path");
919
+ var import_yaml = require("yaml");
920
+ var DEFAULT_POLICY_FILENAME = ".rn-doctor.yml";
921
+ var PolicyError = class extends Error {
922
+ constructor(message) {
923
+ super(message);
924
+ this.name = "PolicyError";
925
+ }
926
+ };
927
+ var RULE_SEVERITIES = ["error", "warn", "off"];
928
+ var SCOPES = ["rn-native-only", "all-deps"];
929
+ var RULE_KEYS = [
930
+ "newArchitecture",
931
+ "newArchUnknown",
932
+ "lastPublish",
933
+ "githubArchived",
934
+ "npmDeprecated",
935
+ "directoryUnmaintained"
936
+ ];
937
+ var TOP_LEVEL_KEYS = ["rules", "scope", "allow"];
938
+ function isRecord(value) {
939
+ return typeof value === "object" && value !== null && !Array.isArray(value);
940
+ }
941
+ function isList(value) {
942
+ return Array.isArray(value);
943
+ }
944
+ function fail(where, problem) {
945
+ throw new PolicyError(`Invalid policy${where}: ${problem}`);
946
+ }
947
+ function parseSeverity(value, key, where) {
948
+ if (typeof value === "string" && RULE_SEVERITIES.includes(value)) {
949
+ return value;
950
+ }
951
+ return fail(where, `rules.${key} must be one of ${RULE_SEVERITIES.join(" | ")}, got ${JSON.stringify(value)}`);
952
+ }
953
+ function parseLastPublish(value, where) {
954
+ if (value === "off") return "off";
955
+ if (isRecord(value)) {
956
+ const keys = Object.keys(value);
957
+ const unknown = keys.filter((k) => k !== "warnMonths" && k !== "errorMonths");
958
+ if (unknown.length > 0) {
959
+ fail(where, `rules.lastPublish has unknown key(s): ${unknown.join(", ")} (expected warnMonths, errorMonths)`);
960
+ }
961
+ const { warnMonths, errorMonths } = value;
962
+ if (typeof warnMonths !== "number" || typeof errorMonths !== "number") {
963
+ fail(where, `rules.lastPublish.warnMonths and .errorMonths must both be numbers`);
964
+ }
965
+ if (warnMonths < 0 || errorMonths < 0 || errorMonths < warnMonths) {
966
+ fail(where, `rules.lastPublish thresholds must be >= 0 with errorMonths >= warnMonths`);
967
+ }
968
+ return { warnMonths, errorMonths };
969
+ }
970
+ return fail(where, `rules.lastPublish must be "off" or { warnMonths, errorMonths }, got ${JSON.stringify(value)}`);
971
+ }
972
+ function parseRules(value, where) {
973
+ if (!isRecord(value)) {
974
+ return fail(where, `"rules" must be a mapping, got ${JSON.stringify(value)}`);
975
+ }
976
+ const unknown = Object.keys(value).filter((k) => !RULE_KEYS.includes(k));
977
+ if (unknown.length > 0) {
978
+ fail(where, `unknown rule(s): ${unknown.join(", ")} (known rules: ${RULE_KEYS.join(", ")})`);
979
+ }
980
+ const defaults = DEFAULT_POLICY.rules;
981
+ const severity = (key, fallback) => key in value ? parseSeverity(value[key], key, where) : fallback;
982
+ return {
983
+ newArchitecture: severity("newArchitecture", defaults.newArchitecture),
984
+ newArchUnknown: severity("newArchUnknown", defaults.newArchUnknown),
985
+ lastPublish: "lastPublish" in value ? parseLastPublish(value.lastPublish, where) : defaults.lastPublish,
986
+ githubArchived: severity("githubArchived", defaults.githubArchived),
987
+ npmDeprecated: severity("npmDeprecated", defaults.npmDeprecated),
988
+ directoryUnmaintained: severity("directoryUnmaintained", defaults.directoryUnmaintained)
989
+ };
990
+ }
991
+ var EXPIRES_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
992
+ function parseAllowEntry(value, index, where) {
993
+ const at = `allow[${String(index)}]`;
994
+ if (!isRecord(value)) {
995
+ return fail(where, `${at} must be a mapping with a "package" key, got ${JSON.stringify(value)}`);
996
+ }
997
+ const unknown = Object.keys(value).filter((k) => k !== "package" && k !== "reason" && k !== "expires");
998
+ if (unknown.length > 0) {
999
+ fail(where, `${at} has unknown key(s): ${unknown.join(", ")} (expected package, reason, expires)`);
1000
+ }
1001
+ const pkg = value.package;
1002
+ if (typeof pkg !== "string" || pkg.length === 0) {
1003
+ fail(where, `${at}.package must be a non-empty string`);
1004
+ }
1005
+ const reason = value.reason;
1006
+ if (reason !== void 0 && typeof reason !== "string") {
1007
+ fail(where, `${at}.reason must be a string when present`);
1008
+ }
1009
+ let expires = value.expires;
1010
+ if (expires instanceof Date && !Number.isNaN(expires.getTime())) {
1011
+ expires = expires.toISOString().slice(0, 10);
1012
+ }
1013
+ if (expires !== void 0 && (typeof expires !== "string" || !EXPIRES_PATTERN.test(expires))) {
1014
+ fail(where, `${at}.expires must be a YYYY-MM-DD date when present, got ${JSON.stringify(value.expires)}`);
1015
+ }
1016
+ return {
1017
+ package: pkg,
1018
+ reason: typeof reason === "string" ? reason : null,
1019
+ expires: typeof expires === "string" ? expires : null
1020
+ };
1021
+ }
1022
+ function parsePolicy(yamlText, filePath) {
1023
+ const where = filePath ? ` (${filePath})` : "";
1024
+ let raw;
1025
+ try {
1026
+ raw = (0, import_yaml.parse)(yamlText);
1027
+ } catch (err) {
1028
+ return fail(where, `YAML syntax error \u2014 ${err instanceof Error ? err.message : String(err)}`);
1029
+ }
1030
+ if (raw === null || raw === void 0) return DEFAULT_POLICY;
1031
+ if (!isRecord(raw)) {
1032
+ return fail(where, `top level must be a mapping, got ${JSON.stringify(raw)}`);
1033
+ }
1034
+ const unknown = Object.keys(raw).filter((k) => !TOP_LEVEL_KEYS.includes(k));
1035
+ if (unknown.length > 0) {
1036
+ fail(where, `unknown top-level key(s): ${unknown.join(", ")} (expected ${TOP_LEVEL_KEYS.join(", ")})`);
1037
+ }
1038
+ const rules = "rules" in raw ? parseRules(raw.rules, where) : DEFAULT_POLICY.rules;
1039
+ let scope = DEFAULT_POLICY.scope;
1040
+ if ("scope" in raw) {
1041
+ const value = raw.scope;
1042
+ if (typeof value !== "string" || !SCOPES.includes(value)) {
1043
+ fail(where, `"scope" must be one of ${SCOPES.join(" | ")}, got ${JSON.stringify(value)}`);
1044
+ }
1045
+ scope = value;
1046
+ }
1047
+ let allow = DEFAULT_POLICY.allow;
1048
+ if ("allow" in raw) {
1049
+ const value = raw.allow;
1050
+ if (!isList(value)) {
1051
+ return fail(where, `"allow" must be a list of entries, got ${JSON.stringify(value)}`);
1052
+ }
1053
+ allow = value.map((entry, i) => parseAllowEntry(entry, i, where));
1054
+ }
1055
+ return { rules, scope, allow };
1056
+ }
1057
+ async function loadPolicy(path2, cwd = process.cwd()) {
1058
+ const explicit = path2 !== void 0;
1059
+ const filePath = explicit ? (0, import_node_path2.resolve)(cwd, path2) : (0, import_node_path2.resolve)(cwd, DEFAULT_POLICY_FILENAME);
1060
+ let text;
1061
+ try {
1062
+ text = await (0, import_promises2.readFile)(filePath, "utf8");
1063
+ } catch (err) {
1064
+ const code = err.code;
1065
+ if (code === "ENOENT" && !explicit) return DEFAULT_POLICY;
1066
+ throw new PolicyError(
1067
+ explicit && code === "ENOENT" ? `Policy file not found: ${filePath}. Check the --policy path.` : `Could not read policy file ${filePath}: ${err instanceof Error ? err.message : String(err)}`
1068
+ );
1069
+ }
1070
+ return parsePolicy(text, filePath);
1071
+ }
1072
+
1073
+ // src/report-annotations.ts
1074
+ function escapeData(value) {
1075
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
1076
+ }
1077
+ function escapeProperty(value) {
1078
+ return escapeData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
1079
+ }
1080
+ function commandFor(f) {
1081
+ if (f.suppressedBy !== null || f.severity === "note") return "notice";
1082
+ return f.severity === "error" ? "error" : "warning";
1083
+ }
1084
+ function renderAnnotations(report, lineOf) {
1085
+ const lines = [];
1086
+ for (const f of report.findings) {
1087
+ const command = commandFor(f);
1088
+ const line = lineOf(f.file, f.package);
1089
+ const properties = [
1090
+ `file=${escapeProperty(f.file)}`,
1091
+ ...line !== null ? [`line=${String(line)}`] : [],
1092
+ `title=${escapeProperty(`rn-doctor: ${f.rule} (${f.package})`)}`
1093
+ ].join(",");
1094
+ const suffix = f.suppressedBy !== null ? ` [allowed${f.suppressedBy.reason ? `: ${f.suppressedBy.reason}` : ""}${f.suppressedBy.expires ? `, expires ${f.suppressedBy.expires}` : ""}]` : "";
1095
+ lines.push(`::${command} ${properties}::${escapeData(f.message + suffix)}`);
1096
+ }
1097
+ return lines.length > 0 ? `${lines.join("\n")}
1098
+ ` : "";
1099
+ }
1100
+
1101
+ // src/report.ts
1102
+ function summarize(findings) {
1103
+ let errors = 0;
1104
+ let warnings = 0;
1105
+ let notes = 0;
1106
+ let suppressed = 0;
1107
+ for (const f of findings) {
1108
+ if (f.suppressedBy !== null) {
1109
+ suppressed++;
1110
+ } else if (f.severity === "error") {
1111
+ errors++;
1112
+ } else if (f.severity === "warn") {
1113
+ warnings++;
1114
+ } else {
1115
+ notes++;
1116
+ }
1117
+ }
1118
+ return { errors, warnings, notes, suppressed };
1119
+ }
1120
+ function computeExitCode(findings) {
1121
+ return findings.some((f) => f.severity === "error" && f.suppressedBy === null) ? 1 : 0;
1122
+ }
1123
+
1124
+ // src/report-json.ts
1125
+ function renderJson(report) {
1126
+ const summary = summarize(report.findings);
1127
+ const doc = {
1128
+ version: 1,
1129
+ summary: {
1130
+ checked: report.checkedCount,
1131
+ errors: summary.errors,
1132
+ warnings: summary.warnings,
1133
+ notes: summary.notes,
1134
+ suppressed: summary.suppressed
1135
+ },
1136
+ findings: report.findings.map((f) => ({
1137
+ file: f.file,
1138
+ package: f.package,
1139
+ rule: f.rule,
1140
+ severity: f.severity,
1141
+ message: f.message,
1142
+ evidenceUrl: f.evidenceUrl,
1143
+ suppressedBy: f.suppressedBy === null ? null : { reason: f.suppressedBy.reason, expires: f.suppressedBy.expires }
1144
+ })),
1145
+ warnings: report.warnings.map((w) => ({
1146
+ ...w.dependency !== void 0 ? { dependency: w.dependency } : {},
1147
+ source: w.source,
1148
+ message: w.message
1149
+ }))
1150
+ };
1151
+ return `${JSON.stringify(doc, null, 2)}
1152
+ `;
1153
+ }
1154
+
1155
+ // src/report-pretty.ts
1156
+ var ESC = String.fromCharCode(27);
1157
+ var ANSI = {
1158
+ red: "31",
1159
+ yellow: "33",
1160
+ cyan: "36",
1161
+ green: "32",
1162
+ dim: "2",
1163
+ bold: "1"
1164
+ };
1165
+ function paint(code, text, on) {
1166
+ return on ? `${ESC}[${code}m${text}${ESC}[0m` : text;
1167
+ }
1168
+ function badge(f, color) {
1169
+ if (f.suppressedBy !== null) return paint(ANSI.dim, `allowed(${f.severity})`, color);
1170
+ if (f.severity === "error") return paint(ANSI.red, "error", color);
1171
+ if (f.severity === "warn") return paint(ANSI.yellow, "warn", color);
1172
+ return paint(ANSI.cyan, "note", color);
1173
+ }
1174
+ function plural(n, word) {
1175
+ return `${String(n)} ${word}${n === 1 ? "" : "s"}`;
1176
+ }
1177
+ function renderPretty(report, options) {
1178
+ const { color } = options;
1179
+ const lines = [];
1180
+ const multiManifest = (report.manifestCount ?? 1) > 1;
1181
+ let currentFile = null;
1182
+ for (const f of report.findings) {
1183
+ if (multiManifest && f.file !== currentFile) {
1184
+ currentFile = f.file;
1185
+ lines.push(paint(ANSI.dim, `${f.file}:`, color));
1186
+ lines.push("");
1187
+ }
1188
+ lines.push(`${badge(f, color)} ${paint(ANSI.bold, f.package, color)} [${f.rule}]`);
1189
+ lines.push(` ${f.message}`);
1190
+ if (f.suppressedBy !== null) {
1191
+ const reason = f.suppressedBy.reason ?? "no reason given";
1192
+ const expires = f.suppressedBy.expires ? `, expires ${f.suppressedBy.expires}` : "";
1193
+ lines.push(paint(ANSI.dim, ` allowed by .rn-doctor.yml: ${reason}${expires}`, color));
1194
+ }
1195
+ if (f.evidenceUrl !== null) {
1196
+ lines.push(paint(ANSI.dim, ` evidence: ${f.evidenceUrl}`, color));
1197
+ }
1198
+ lines.push("");
1199
+ }
1200
+ if (report.warnings.length > 0) {
1201
+ lines.push("Data warnings (missing or degraded lookups - findings may be incomplete):");
1202
+ for (const w of report.warnings) {
1203
+ const scope = w.dependency !== void 0 ? `${w.dependency}: ` : "";
1204
+ lines.push(paint(ANSI.dim, ` - [${w.source}] ${scope}${w.message}`, color));
1205
+ }
1206
+ lines.push("");
1207
+ }
1208
+ const s = summarize(report.findings);
1209
+ const n = report.checkedCount;
1210
+ const checked = `${String(n)} ${n === 1 ? "dependency" : "dependencies"}` + (multiManifest ? ` in ${String(report.manifestCount)} manifests` : "");
1211
+ if (s.errors === 0 && s.warnings === 0 && s.notes === 0 && s.suppressed === 0) {
1212
+ lines.push(paint(ANSI.green, `No findings across ${checked}.`, color));
1213
+ } else {
1214
+ const parts = [
1215
+ paint(ANSI.red, plural(s.errors, "error"), color),
1216
+ paint(ANSI.yellow, plural(s.warnings, "warning"), color),
1217
+ plural(s.notes, "note"),
1218
+ `${String(s.suppressed)} allowed`
1219
+ ];
1220
+ lines.push(`${parts.join(", ")} across ${checked}.`);
1221
+ }
1222
+ return `${lines.join("\n")}
1223
+ `;
1224
+ }
1225
+
1226
+ // src/version.ts
1227
+ var VERSION = "0.0.0";
1228
+
1229
+ // src/report-sarif.ts
1230
+ var INFORMATION_URI = "https://www.npmjs.com/package/react-native-doctor-ci";
1231
+ var RULE_IDS = [
1232
+ "newArchitecture",
1233
+ "newArchUnknown",
1234
+ "lastPublish",
1235
+ "githubArchived",
1236
+ "npmDeprecated",
1237
+ "directoryUnmaintained"
1238
+ ];
1239
+ var RULE_DESCRIPTIONS = {
1240
+ newArchitecture: "The package does not support the React Native New Architecture, per the React Native Directory.",
1241
+ newArchUnknown: "New Architecture support for the package could not be determined from the React Native Directory or npm codegen hints.",
1242
+ lastPublish: "The package's latest npm publish is older than the configured staleness threshold.",
1243
+ githubArchived: "The package's GitHub repository is archived (read-only).",
1244
+ npmDeprecated: "The package is marked deprecated on the npm registry.",
1245
+ directoryUnmaintained: "The React Native Directory flags the package as unmaintained."
1246
+ };
1247
+ function sarifLevel(severity) {
1248
+ return severity === "warn" ? "warning" : severity;
1249
+ }
1250
+ function renderSarif(report, options = {}) {
1251
+ const lineOf = options.lineOf ?? (() => null);
1252
+ const summary = summarize(report.findings);
1253
+ const results = report.findings.map((f) => {
1254
+ const line = lineOf(f.file, f.package);
1255
+ return {
1256
+ ruleId: f.rule,
1257
+ ruleIndex: RULE_IDS.indexOf(f.rule),
1258
+ level: sarifLevel(f.severity),
1259
+ message: { text: f.message },
1260
+ locations: [
1261
+ {
1262
+ physicalLocation: {
1263
+ artifactLocation: { uri: f.file },
1264
+ ...line !== null ? { region: { startLine: line } } : {}
1265
+ }
1266
+ }
1267
+ ],
1268
+ ...f.suppressedBy !== null ? {
1269
+ suppressions: [
1270
+ {
1271
+ kind: "external",
1272
+ status: "accepted",
1273
+ justification: `Allowlisted in .rn-doctor.yml` + (f.suppressedBy.reason ? `: ${f.suppressedBy.reason}` : "") + (f.suppressedBy.expires ? ` (expires ${f.suppressedBy.expires})` : "")
1274
+ }
1275
+ ]
1276
+ } : {},
1277
+ ...f.evidenceUrl !== null ? { hostedViewerUri: f.evidenceUrl } : {}
1278
+ };
1279
+ });
1280
+ const doc = {
1281
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1282
+ version: "2.1.0",
1283
+ runs: [
1284
+ {
1285
+ tool: {
1286
+ driver: {
1287
+ name: "rn-doctor",
1288
+ informationUri: INFORMATION_URI,
1289
+ version: VERSION,
1290
+ rules: RULE_IDS.map((id) => ({
1291
+ id,
1292
+ shortDescription: { text: RULE_DESCRIPTIONS[id] },
1293
+ helpUri: INFORMATION_URI
1294
+ }))
1295
+ }
1296
+ },
1297
+ invocations: [
1298
+ {
1299
+ executionSuccessful: true,
1300
+ toolExecutionNotifications: report.warnings.map((w) => ({
1301
+ level: "warning",
1302
+ message: {
1303
+ text: (w.dependency !== void 0 ? `${w.dependency}: ` : "") + w.message
1304
+ }
1305
+ }))
1306
+ }
1307
+ ],
1308
+ results,
1309
+ properties: {
1310
+ checked: report.checkedCount,
1311
+ errors: summary.errors,
1312
+ warnings: summary.warnings,
1313
+ notes: summary.notes,
1314
+ suppressed: summary.suppressed
1315
+ }
1316
+ }
1317
+ ]
1318
+ };
1319
+ return `${JSON.stringify(doc, null, 2)}
1320
+ `;
1321
+ }
1322
+
1323
+ // src/workspaces.ts
1324
+ var import_promises3 = require("fs/promises");
1325
+ var import_node_path3 = require("path");
1326
+ var import_yaml2 = require("yaml");
1327
+ var WorkspaceError = class extends Error {
1328
+ constructor(message) {
1329
+ super(message);
1330
+ this.name = "WorkspaceError";
1331
+ }
1332
+ };
1333
+ function segmentToRegExp(segment) {
1334
+ const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/\\\\]*");
1335
+ return new RegExp(`^${escaped}$`);
1336
+ }
1337
+ async function isDirectory(path2) {
1338
+ try {
1339
+ return (await (0, import_promises3.stat)(path2)).isDirectory();
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ async function listSubdirs(dir) {
1345
+ let entries;
1346
+ try {
1347
+ entries = await (0, import_promises3.readdir)(dir, { withFileTypes: true });
1348
+ } catch {
1349
+ return [];
1350
+ }
1351
+ return entries.filter((e) => e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")).map((e) => e.name);
1352
+ }
1353
+ async function walkDirs(dir) {
1354
+ const out = [dir];
1355
+ for (const name of await listSubdirs(dir)) {
1356
+ out.push(...await walkDirs((0, import_node_path3.join)(dir, name)));
1357
+ }
1358
+ return out;
1359
+ }
1360
+ async function expandPattern(rootDir, pattern) {
1361
+ const segments = pattern.replaceAll("\\", "/").replace(/\/+$/, "").split("/").filter((s) => s !== "" && s !== ".");
1362
+ let dirs = [rootDir];
1363
+ for (const [index, segment] of segments.entries()) {
1364
+ if (segment === "**") {
1365
+ const rest = segments.slice(index + 1).join("/");
1366
+ const under = (await Promise.all(dirs.map((d) => walkDirs(d)))).flat();
1367
+ if (rest === "") return under;
1368
+ return (await Promise.all(under.map((d) => expandPattern(d, rest)))).flat();
1369
+ }
1370
+ if (segment.includes("*")) {
1371
+ const re = segmentToRegExp(segment);
1372
+ const next = [];
1373
+ for (const dir of dirs) {
1374
+ for (const name of await listSubdirs(dir)) {
1375
+ if (re.test(name)) next.push((0, import_node_path3.join)(dir, name));
1376
+ }
1377
+ }
1378
+ dirs = next;
1379
+ } else {
1380
+ const next = [];
1381
+ for (const dir of dirs) {
1382
+ const candidate = (0, import_node_path3.join)(dir, segment);
1383
+ if (await isDirectory(candidate)) next.push(candidate);
1384
+ }
1385
+ dirs = next;
1386
+ }
1387
+ if (dirs.length === 0) return [];
1388
+ }
1389
+ return dirs;
1390
+ }
1391
+ function relPosix(rootDir, dir) {
1392
+ return (0, import_node_path3.relative)(rootDir, dir).split(import_node_path3.sep).join("/");
1393
+ }
1394
+ async function expandWorkspacePatterns(rootDir, patterns) {
1395
+ const included = /* @__PURE__ */ new Map();
1396
+ const exclusions = [];
1397
+ for (const pattern of patterns) {
1398
+ if (pattern.startsWith("!")) {
1399
+ exclusions.push(pattern.slice(1));
1400
+ continue;
1401
+ }
1402
+ for (const dir of await expandPattern(rootDir, pattern)) {
1403
+ const rel = relPosix(rootDir, dir);
1404
+ if (rel !== "") included.set(rel, dir);
1405
+ }
1406
+ }
1407
+ for (const exclusion of exclusions) {
1408
+ const excluded = new Set(
1409
+ (await expandPattern(rootDir, exclusion)).map((dir) => relPosix(rootDir, dir))
1410
+ );
1411
+ for (const rel of [...included.keys()]) {
1412
+ if (excluded.has(rel)) included.delete(rel);
1413
+ }
1414
+ }
1415
+ return [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, dir]) => dir);
1416
+ }
1417
+ async function readPnpmWorkspacePatterns(rootDir) {
1418
+ let text;
1419
+ try {
1420
+ text = await (0, import_promises3.readFile)((0, import_node_path3.join)(rootDir, "pnpm-workspace.yaml"), "utf8");
1421
+ } catch {
1422
+ return null;
1423
+ }
1424
+ let parsed;
1425
+ try {
1426
+ parsed = (0, import_yaml2.parse)(text);
1427
+ } catch (err) {
1428
+ throw new WorkspaceError(
1429
+ `pnpm-workspace.yaml is not valid YAML \u2014 ${err instanceof Error ? err.message : String(err)}`
1430
+ );
1431
+ }
1432
+ if (typeof parsed !== "object" || parsed === null) return null;
1433
+ const packages = parsed["packages"];
1434
+ if (packages === void 0) return null;
1435
+ if (!Array.isArray(packages) || !packages.every((p) => typeof p === "string")) {
1436
+ throw new WorkspaceError('pnpm-workspace.yaml has a "packages" field that is not a list of strings.');
1437
+ }
1438
+ return packages;
1439
+ }
1440
+ function readManifestWorkspacePatterns(parsed) {
1441
+ if (typeof parsed !== "object" || parsed === null) return null;
1442
+ let field = parsed["workspaces"];
1443
+ if (field === void 0) return null;
1444
+ if (typeof field === "object" && field !== null && !Array.isArray(field)) {
1445
+ field = field["packages"];
1446
+ if (field === void 0) return null;
1447
+ }
1448
+ if (!Array.isArray(field) || !field.every((p) => typeof p === "string")) {
1449
+ throw new WorkspaceError('package.json has a "workspaces" field that is not a list of strings.');
1450
+ }
1451
+ return field;
1452
+ }
1453
+ async function discoverWorkspaces(rootDir, rootManifestParsed) {
1454
+ const patterns = await readPnpmWorkspacePatterns(rootDir) ?? readManifestWorkspacePatterns(rootManifestParsed);
1455
+ if (patterns === null) {
1456
+ throw new WorkspaceError(
1457
+ '--workspaces requires a "workspaces" field in package.json or a packages list in pnpm-workspace.yaml.'
1458
+ );
1459
+ }
1460
+ const result = [{ dir: rootDir, manifestRelPath: "package.json" }];
1461
+ for (const dir of await expandWorkspacePatterns(rootDir, patterns)) {
1462
+ try {
1463
+ await (0, import_promises3.stat)((0, import_node_path3.join)(dir, "package.json"));
1464
+ } catch {
1465
+ continue;
1466
+ }
1467
+ result.push({ dir, manifestRelPath: `${relPosix(rootDir, dir)}/package.json` });
1468
+ }
1469
+ return result;
1470
+ }
1471
+
1472
+ // src/cli-main.ts
1473
+ var USAGE = `rn-doctor - React Native dependency health gate for CI
1474
+
1475
+ Usage:
1476
+ rn-doctor [options]
1477
+
1478
+ Checks the "dependencies" of the package.json in the current directory
1479
+ against the policy in .rn-doctor.yml (or the built-in default policy).
1480
+
1481
+ Options:
1482
+ --json Output a machine-readable JSON report (stable-ordered)
1483
+ --sarif Output a SARIF 2.1.0 report (for code-scanning upload)
1484
+ --policy <path> Path to the policy file (default: .rn-doctor.yml if present)
1485
+ --changed-only Check only dependencies added or changed vs the base ref
1486
+ (compares against the merge-base of HEAD and the base)
1487
+ --base <ref> Base ref for --changed-only (default: origin/main)
1488
+ --workspaces Also check every workspace package.json (npm/yarn
1489
+ "workspaces" or pnpm-workspace.yaml), grouped by manifest
1490
+ --no-cache Bypass the enrichment cache (read and write)
1491
+ --annotations Force GitHub annotations on (default: auto in GitHub Actions)
1492
+ --no-annotations Force GitHub annotations off
1493
+ -v, --version Print the version and exit
1494
+ -h, --help Show this help and exit
1495
+
1496
+ Exit codes:
1497
+ 0 clean (no policy errors; warnings, notes and allowlisted findings are ok)
1498
+ 1 policy errors found
1499
+ 2 tool failure (bad flags, unreadable package.json, invalid policy file,
1500
+ git failure under --changed-only, missing workspace configuration)
1501
+ `;
1502
+ function parseCliArgs(argv) {
1503
+ const { values } = (0, import_node_util.parseArgs)({
1504
+ args: [...argv],
1505
+ strict: true,
1506
+ allowPositionals: false,
1507
+ options: {
1508
+ json: { type: "boolean", default: false },
1509
+ sarif: { type: "boolean", default: false },
1510
+ policy: { type: "string" },
1511
+ "changed-only": { type: "boolean", default: false },
1512
+ // No parseArgs default: --base without --changed-only must be detectable.
1513
+ base: { type: "string" },
1514
+ workspaces: { type: "boolean", default: false },
1515
+ // Node 20 parseArgs has no allowNegative; model --no-* as literal flags.
1516
+ "no-cache": { type: "boolean", default: false },
1517
+ annotations: { type: "boolean", default: false },
1518
+ "no-annotations": { type: "boolean", default: false },
1519
+ version: { type: "boolean", short: "v", default: false },
1520
+ help: { type: "boolean", short: "h", default: false }
1521
+ }
1522
+ });
1523
+ return {
1524
+ json: values.json,
1525
+ sarif: values.sarif,
1526
+ policy: values.policy,
1527
+ changedOnly: values["changed-only"],
1528
+ base: values.base,
1529
+ workspaces: values.workspaces,
1530
+ noCache: values["no-cache"],
1531
+ annotations: values.annotations,
1532
+ noAnnotations: values["no-annotations"],
1533
+ version: values.version,
1534
+ help: values.help
1535
+ };
1536
+ }
1537
+ async function runCli(argv, io) {
1538
+ let flags;
1539
+ try {
1540
+ flags = parseCliArgs(argv);
1541
+ } catch (err) {
1542
+ io.stderr.write(`rn-doctor: ${err instanceof Error ? err.message : String(err)}
1543
+
1544
+ ${USAGE}`);
1545
+ return 2;
1546
+ }
1547
+ if (flags.help) {
1548
+ io.stdout.write(USAGE);
1549
+ return 0;
1550
+ }
1551
+ if (flags.version) {
1552
+ io.stdout.write(`${VERSION}
1553
+ `);
1554
+ return 0;
1555
+ }
1556
+ if (flags.json && flags.sarif) {
1557
+ io.stderr.write("rn-doctor: --json and --sarif are mutually exclusive.\n");
1558
+ return 2;
1559
+ }
1560
+ if (flags.annotations && flags.noAnnotations) {
1561
+ io.stderr.write("rn-doctor: --annotations and --no-annotations are mutually exclusive.\n");
1562
+ return 2;
1563
+ }
1564
+ if (flags.base !== void 0 && !flags.changedOnly) {
1565
+ io.stderr.write("rn-doctor: --base requires --changed-only.\n");
1566
+ return 2;
1567
+ }
1568
+ try {
1569
+ const rootManifest = await readPackageJson(io.cwd);
1570
+ const policy = await loadPolicy(flags.policy, io.cwd);
1571
+ const runWarnings = [];
1572
+ let scans;
1573
+ if (flags.workspaces) {
1574
+ const dirs = await discoverWorkspaces(io.cwd, JSON.parse(rootManifest.text));
1575
+ if (dirs.length === 1) {
1576
+ runWarnings.push({
1577
+ source: "workspaces",
1578
+ message: "The workspace configuration matched no directories; checking the root manifest only."
1579
+ });
1580
+ }
1581
+ scans = await Promise.all(
1582
+ dirs.map(async (w) => ({
1583
+ relPath: w.manifestRelPath,
1584
+ manifest: w.manifestRelPath === "package.json" ? rootManifest : await readManifestAt((0, import_node_path4.join)(w.dir, "package.json"))
1585
+ }))
1586
+ );
1587
+ } else {
1588
+ scans = [{ relPath: "package.json", manifest: rootManifest }];
1589
+ }
1590
+ let checks;
1591
+ if (flags.changedOnly) {
1592
+ const git = io.git ?? createGitRunner();
1593
+ const baseRef = flags.base ?? "origin/main";
1594
+ const baseCommit = await resolveBaseCommit(git, io.cwd, baseRef);
1595
+ checks = await Promise.all(
1596
+ scans.map(async ({ relPath, manifest }) => {
1597
+ const baseText = await readFileAtCommit(git, io.cwd, baseCommit, relPath);
1598
+ let baseEntries = null;
1599
+ if (baseText !== null) {
1600
+ try {
1601
+ baseEntries = entriesFromManifestText(baseText, `${relPath} at ${baseRef}`);
1602
+ } catch (err) {
1603
+ if (!(err instanceof ManifestError)) throw err;
1604
+ runWarnings.push({
1605
+ source: "git",
1606
+ message: `${relPath} at ${baseRef} could not be parsed (${err.message}); checking all of its dependencies.`
1607
+ });
1608
+ }
1609
+ }
1610
+ return { relPath, names: diffDependencies(baseEntries, manifest.entries) };
1611
+ })
1612
+ );
1613
+ } else {
1614
+ checks = scans.map(({ relPath, manifest }) => ({ relPath, names: manifest.dependencies }));
1615
+ }
1616
+ const union = [...new Set(checks.flatMap((c) => c.names))];
1617
+ const enriched = union.length > 0 ? await enrichDependencies(union, {
1618
+ noCache: flags.noCache,
1619
+ cacheDir: io.cwd,
1620
+ githubToken: io.env.GITHUB_TOKEN
1621
+ }) : { dependencies: [], warnings: [] };
1622
+ const findings = evaluatePolicy(enriched.dependencies, policy, { now: io.now });
1623
+ const located = [];
1624
+ for (const { relPath, names } of checks) {
1625
+ const declared = new Set(names);
1626
+ for (const f of findings) {
1627
+ if (declared.has(f.package)) located.push({ ...f, file: relPath });
1628
+ }
1629
+ }
1630
+ const report = {
1631
+ findings: located,
1632
+ warnings: [...runWarnings, ...enriched.warnings],
1633
+ checkedCount: checks.reduce((sum, c) => sum + c.names.length, 0),
1634
+ ...flags.workspaces ? { manifestCount: scans.length } : {}
1635
+ };
1636
+ const texts = new Map(scans.map(({ relPath, manifest }) => [relPath, manifest.text]));
1637
+ const lineOf = (file, name) => {
1638
+ const text = texts.get(file);
1639
+ return text === void 0 ? null : findDependencyLine(text, name);
1640
+ };
1641
+ if (flags.json) {
1642
+ io.stdout.write(renderJson(report));
1643
+ } else if (flags.sarif) {
1644
+ io.stdout.write(renderSarif(report, { lineOf }));
1645
+ } else {
1646
+ const color = Boolean(io.stdout.isTTY) && !io.env.NO_COLOR;
1647
+ io.stdout.write(renderPretty(report, { color }));
1648
+ const emitAnnotations = flags.annotations || io.env.GITHUB_ACTIONS === "true" && !flags.noAnnotations;
1649
+ if (emitAnnotations) {
1650
+ io.stdout.write(renderAnnotations(report, lineOf));
1651
+ }
1652
+ }
1653
+ return computeExitCode(report.findings);
1654
+ } catch (err) {
1655
+ if (err instanceof ManifestError || err instanceof PolicyError || err instanceof GitError || err instanceof WorkspaceError) {
1656
+ io.stderr.write(`rn-doctor: ${err.message}
1657
+ `);
1658
+ return 2;
1659
+ }
1660
+ io.stderr.write(
1661
+ `rn-doctor: unexpected error - ${err instanceof Error ? err.stack ?? err.message : String(err)}
1662
+ `
1663
+ );
1664
+ return 2;
1665
+ }
1666
+ }
1667
+
1668
+ // src/cli.ts
1669
+ void runCli(process.argv.slice(2), {
1670
+ stdout: process.stdout,
1671
+ stderr: process.stderr,
1672
+ cwd: process.cwd(),
1673
+ env: process.env
1674
+ }).then((code) => {
1675
+ process.exitCode = code;
1676
+ });
1677
+ //# sourceMappingURL=cli.cjs.map