@colrealpro/react-luau-doctor 0.18.0 → 0.18.1

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.js CHANGED
@@ -11,7 +11,7 @@ var package_default = {
11
11
  publishConfig: {
12
12
  access: "public"
13
13
  },
14
- version: "0.18.0",
14
+ version: "0.18.1",
15
15
  description: "Static analysis for React-Luau hooks, effects, rendering, and performance.",
16
16
  license: "MIT",
17
17
  type: "module",
@@ -9904,7 +9904,6 @@ var GITLAB_WORKFLOW = ".gitlab-ci.yml";
9904
9904
  var SUMMARY_MARKER = "<!-- react-luau-doctor:summary -->";
9905
9905
  var REVIEW_MARKER = "<!-- react-luau-doctor:review -->";
9906
9906
  var MAX_REVIEW_COMMENTS = 20;
9907
- var MAX_RESOLVE_THREADS_PER_REQUEST = 100;
9908
9907
  var MAX_GITHUB_RETRIES = 2;
9909
9908
  var MAX_RATE_LIMIT_WAIT_MS = 60000;
9910
9909
  var PACKAGE_SPEC = `${package_default.name}@${package_default.version}`;
@@ -9920,10 +9919,8 @@ on:
9920
9919
  branches: [main]
9921
9920
 
9922
9921
  permissions:
9923
- # GitHub currently requires Contents: write for the resolveReviewThread GraphQL mutation.
9924
- contents: ${settings.reviewComments ? "write" : "read"}
9922
+ contents: read
9925
9923
  pull-requests: write
9926
- issues: write
9927
9924
  statuses: write
9928
9925
 
9929
9926
  concurrency:
@@ -10332,23 +10329,6 @@ async function githubApi(repo, endpoint, options = {}) {
10332
10329
  const apiBase = (process.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
10333
10330
  return githubRequest(`${apiBase}/repos/${repo}${endpoint}`, options);
10334
10331
  }
10335
- function githubGraphQLUrl() {
10336
- if (process.env.GITHUB_GRAPHQL_URL)
10337
- return process.env.GITHUB_GRAPHQL_URL;
10338
- const apiBase = (process.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
10339
- return apiBase.endsWith("/api/v3") ? `${apiBase.slice(0, -"/v3".length)}/graphql` : `${apiBase}/graphql`;
10340
- }
10341
- async function githubGraphQL(query, variables) {
10342
- const response = await githubRequest(githubGraphQLUrl(), {
10343
- method: "POST",
10344
- body: { query, variables }
10345
- });
10346
- if (response.errors?.length)
10347
- throw new Error(`GitHub GraphQL: ${response.errors.map((error) => error.message ?? "unknown error").join("; ")}`);
10348
- if (!response.data)
10349
- throw new Error("GitHub GraphQL returned no data");
10350
- return response.data;
10351
- }
10352
10332
  function repoRelativeDiagnosticPath(directory, diagnosticFile) {
10353
10333
  const absoluteDirectory = path11.resolve(directory);
10354
10334
  const repoRoot = findGitRoot(absoluteDirectory);
@@ -10458,54 +10438,32 @@ ${diagnostic.message}${diagnostic.help ? `
10458
10438
 
10459
10439
  ${diagnostic.help}` : ""}`;
10460
10440
  }
10461
- async function listReviewThreads(repo, pullNumber) {
10462
- const separator = repo.indexOf("/");
10463
- if (separator < 1 || separator === repo.length - 1)
10464
- throw new Error(`Invalid GitHub repository name: ${repo}`);
10465
- const owner = repo.slice(0, separator);
10466
- const name = repo.slice(separator + 1);
10467
- const threads = [];
10468
- let cursor = null;
10469
- for (;; ) {
10470
- const data = await githubGraphQL(`
10471
- query DoctorReviewThreads($owner: String!, $name: String!, $pull: Int!, $cursor: String) {
10472
- repository(owner: $owner, name: $name) {
10473
- pullRequest(number: $pull) {
10474
- reviewThreads(first: 100, after: $cursor) {
10475
- nodes {
10476
- id
10477
- isResolved
10478
- isOutdated
10479
- path
10480
- comments(first: 1) {
10481
- nodes {
10482
- body
10483
- author { __typename }
10484
- }
10485
- }
10486
- }
10487
- pageInfo { hasNextPage endCursor }
10488
- }
10489
- }
10490
- }
10491
- }
10492
- `, { owner, name, pull: pullNumber, cursor });
10493
- const connection = data.repository?.pullRequest?.reviewThreads;
10494
- if (!connection)
10495
- throw new Error(`Could not load review threads for pull request ${pullNumber}`);
10496
- threads.push(...connection.nodes);
10497
- if (!connection.pageInfo.hasNextPage)
10498
- return threads;
10499
- cursor = connection.pageInfo.endCursor;
10500
- if (!cursor)
10501
- throw new Error("GitHub review thread pagination returned no cursor");
10502
- }
10503
- }
10504
- function doctorThreadFingerprint(thread) {
10505
- const comment = thread.comments.nodes[0];
10506
- if (comment?.author?.__typename !== "Bot" || !comment.body.startsWith(REVIEW_MARKER))
10441
+ var ARCHIVED_REVIEW_BODY = "\u2705 React-Luau Doctor: findings from this review are no longer current. Active findings, if any, are shown in newer reviews.";
10442
+ async function listReviewComments(repo, pullNumber) {
10443
+ const comments = [];
10444
+ for (let page = 1;; page += 1) {
10445
+ const batch = await githubApi(repo, `/pulls/${pullNumber}/comments?per_page=100&page=${page}`);
10446
+ comments.push(...batch);
10447
+ if (batch.length < 100)
10448
+ return comments;
10449
+ }
10450
+ }
10451
+ async function listReviews(repo, pullNumber) {
10452
+ const reviews = [];
10453
+ for (let page = 1;; page += 1) {
10454
+ const batch = await githubApi(repo, `/pulls/${pullNumber}/reviews?per_page=100&page=${page}`);
10455
+ reviews.push(...batch);
10456
+ if (batch.length < 100)
10457
+ return reviews;
10458
+ }
10459
+ }
10460
+ function isDoctorReview(review) {
10461
+ return review.user?.type === "Bot" && (review.body?.includes("React-Luau Doctor") ?? false);
10462
+ }
10463
+ function doctorReviewCommentFingerprint(comment) {
10464
+ if (comment.user?.type !== "Bot" || !comment.body.startsWith(REVIEW_MARKER))
10507
10465
  return null;
10508
- return fingerprintFromReviewBody(comment.body, thread.path);
10466
+ return fingerprintFromReviewBody(comment.body, comment.path);
10509
10467
  }
10510
10468
  function takeCount(counts, key) {
10511
10469
  const count = counts.get(key) ?? 0;
@@ -10514,58 +10472,64 @@ function takeCount(counts, key) {
10514
10472
  counts.set(key, count - 1);
10515
10473
  return true;
10516
10474
  }
10517
- async function resolveReviewThreads(threadIds) {
10518
- for (let offset = 0;offset < threadIds.length; offset += MAX_RESOLVE_THREADS_PER_REQUEST) {
10519
- const batch = threadIds.slice(offset, offset + MAX_RESOLVE_THREADS_PER_REQUEST);
10520
- const declarations = batch.map((_, index) => `$thread${index}: ID!`).join(", ");
10521
- const mutations = batch.map((_, index) => `thread${index}: resolveReviewThread(input: { threadId: $thread${index} }) { thread { id isResolved } }`).join(`
10522
- `);
10523
- const variables = Object.fromEntries(batch.map((id, index) => [`thread${index}`, id]));
10524
- await githubGraphQL(`mutation ResolveDoctorThreads(${declarations}) { ${mutations} }`, variables);
10525
- if (offset + batch.length < threadIds.length)
10526
- await wait(1000);
10527
- }
10475
+ function incrementCount(counts, key) {
10476
+ counts.set(key, (counts.get(key) ?? 0) + 1);
10528
10477
  }
10529
- async function manageReviewComments(repo, pullNumber, currentReport, newDiagnostics, directory, newBase) {
10530
- const threads = await listReviewThreads(repo, pullNumber);
10478
+ async function manageReviewComments(repo, pullNumber, currentReport, newDiagnostics, directory, newBase, pullBase) {
10479
+ const [allReviewComments, reviews] = await Promise.all([
10480
+ listReviewComments(repo, pullNumber),
10481
+ listReviews(repo, pullNumber)
10482
+ ]);
10483
+ const reviewComments = allReviewComments.map((comment) => ({ comment, fingerprint: doctorReviewCommentFingerprint(comment) })).filter((entry) => entry.fingerprint !== null);
10531
10484
  const activeCounts = new Map;
10532
- for (const diagnostic of currentReport.diagnostics) {
10533
- const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10534
- activeCounts.set(fingerprint, (activeCounts.get(fingerprint) ?? 0) + 1);
10535
- }
10485
+ for (const diagnostic of currentReport.diagnostics)
10486
+ incrementCount(activeCounts, diagnosticReviewFingerprint(directory, diagnostic));
10536
10487
  const newCounts = new Map;
10537
- for (const diagnostic of newDiagnostics) {
10538
- const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10539
- newCounts.set(fingerprint, (newCounts.get(fingerprint) ?? 0) + 1);
10540
- }
10541
- const resolvedThreadIds = [];
10542
- for (const thread of threads) {
10543
- if (thread.isResolved)
10544
- continue;
10545
- const fingerprint = doctorThreadFingerprint(thread);
10546
- if (!fingerprint)
10547
- continue;
10548
- if (thread.isOutdated && (newCounts.get(fingerprint) ?? 0) > 0) {
10549
- resolvedThreadIds.push(thread.id);
10488
+ for (const diagnostic of newDiagnostics)
10489
+ incrementCount(newCounts, diagnosticReviewFingerprint(directory, diagnostic));
10490
+ const replacementCounts = new Map;
10491
+ const staleComments = [];
10492
+ const orderedComments = reviewComments.slice().sort((left, right) => Number(left.comment.position === null) - Number(right.comment.position === null));
10493
+ for (const { comment, fingerprint } of orderedComments) {
10494
+ if (comment.position !== null && takeCount(activeCounts, fingerprint)) {
10495
+ takeCount(newCounts, fingerprint);
10550
10496
  continue;
10551
10497
  }
10552
- if (takeCount(activeCounts, fingerprint)) {
10498
+ const needsReplacement = takeCount(activeCounts, fingerprint);
10499
+ if (needsReplacement) {
10500
+ incrementCount(replacementCounts, fingerprint);
10553
10501
  takeCount(newCounts, fingerprint);
10554
- continue;
10555
10502
  }
10556
- resolvedThreadIds.push(thread.id);
10503
+ staleComments.push({ comment, fingerprint, needsReplacement });
10504
+ }
10505
+ const newLineMap = changedLineMap(directory, newBase);
10506
+ const pullLineMap = newBase === pullBase ? newLineMap : changedLineMap(directory, pullBase);
10507
+ const candidates = [];
10508
+ for (const diagnostic of newDiagnostics) {
10509
+ if (!touchesChangedLine(diagnostic, newLineMap.get(diagnostic.file) ?? []))
10510
+ continue;
10511
+ const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10512
+ if (!takeCount(newCounts, fingerprint))
10513
+ continue;
10514
+ candidates.push({ diagnostic, replacement: false });
10557
10515
  }
10558
- const lineMap = changedLineMap(directory, newBase);
10559
- const comments = newDiagnostics.filter((diagnostic) => touchesChangedLine(diagnostic, lineMap.get(diagnostic.file) ?? [])).filter((diagnostic) => {
10516
+ for (const diagnostic of currentReport.diagnostics) {
10517
+ if (!touchesChangedLine(diagnostic, pullLineMap.get(diagnostic.file) ?? []))
10518
+ continue;
10560
10519
  const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10561
- return takeCount(newCounts, fingerprint);
10562
- }).slice(0, MAX_REVIEW_COMMENTS).map((diagnostic) => ({
10520
+ if (!takeCount(replacementCounts, fingerprint))
10521
+ continue;
10522
+ candidates.push({ diagnostic, replacement: true });
10523
+ }
10524
+ const selected = candidates.slice(0, MAX_REVIEW_COMMENTS);
10525
+ const comments = selected.map(({ diagnostic }) => ({
10563
10526
  path: repoRelativeDiagnosticPath(directory, diagnostic.file),
10564
10527
  line: diagnostic.location.line,
10565
10528
  side: "RIGHT",
10566
10529
  body: reviewCommentBody(directory, diagnostic)
10567
10530
  }));
10568
10531
  const failures = [];
10532
+ let postedNewComments = true;
10569
10533
  if (comments.length > 0) {
10570
10534
  try {
10571
10535
  await githubApi(repo, `/pulls/${pullNumber}/reviews`, {
@@ -10573,14 +10537,45 @@ async function manageReviewComments(repo, pullNumber, currentReport, newDiagnost
10573
10537
  body: { event: "COMMENT", body: "React-Luau Doctor found new issues", comments }
10574
10538
  });
10575
10539
  } catch (error) {
10540
+ postedNewComments = false;
10576
10541
  failures.push(`could not create new review comments: ${errorMessage(error)}`);
10577
10542
  }
10578
10543
  }
10579
- if (resolvedThreadIds.length > 0) {
10580
- try {
10581
- await resolveReviewThreads(resolvedThreadIds);
10582
- } catch (error) {
10583
- failures.push(`could not resolve fixed review threads: ${errorMessage(error)}`);
10544
+ if (postedNewComments) {
10545
+ const postedReplacementCounts = new Map;
10546
+ for (const candidate of selected) {
10547
+ if (candidate.replacement)
10548
+ incrementCount(postedReplacementCounts, diagnosticReviewFingerprint(directory, candidate.diagnostic));
10549
+ }
10550
+ const remainingByReview = new Map;
10551
+ for (const { comment } of reviewComments) {
10552
+ remainingByReview.set(comment.pull_request_review_id, (remainingByReview.get(comment.pull_request_review_id) ?? 0) + 1);
10553
+ }
10554
+ for (const stale of staleComments) {
10555
+ if (stale.needsReplacement && !takeCount(postedReplacementCounts, stale.fingerprint))
10556
+ continue;
10557
+ try {
10558
+ await githubApi(repo, `/pulls/comments/${stale.comment.id}`, { method: "DELETE" });
10559
+ const reviewId = stale.comment.pull_request_review_id;
10560
+ const remaining = Math.max(0, (remainingByReview.get(reviewId) ?? 1) - 1);
10561
+ remainingByReview.set(reviewId, remaining);
10562
+ } catch (error) {
10563
+ failures.push(`could not delete stale review comment ${stale.comment.id}: ${errorMessage(error)}`);
10564
+ }
10565
+ }
10566
+ for (const review of reviews) {
10567
+ if (!isDoctorReview(review) || review.body === ARCHIVED_REVIEW_BODY)
10568
+ continue;
10569
+ if ((remainingByReview.get(review.id) ?? 0) > 0)
10570
+ continue;
10571
+ try {
10572
+ await githubApi(repo, `/pulls/${pullNumber}/reviews/${review.id}`, {
10573
+ method: "PUT",
10574
+ body: { body: ARCHIVED_REVIEW_BODY }
10575
+ });
10576
+ } catch (error) {
10577
+ failures.push(`could not archive empty review ${review.id}: ${errorMessage(error)}`);
10578
+ }
10584
10579
  }
10585
10580
  }
10586
10581
  if (failures.length > 0)
@@ -10672,7 +10667,7 @@ async function runCiJob(argv) {
10672
10667
  }
10673
10668
  if (settings.reviewComments && base) {
10674
10669
  try {
10675
- await manageReviewComments(repo, pullNumber, report, reviewChanges.diagnostics, path11.resolve(settings.directory), reviewChanges.base ?? base);
10670
+ await manageReviewComments(repo, pullNumber, report, reviewChanges.diagnostics, path11.resolve(settings.directory), reviewChanges.base ?? base, base);
10676
10671
  } catch (error) {
10677
10672
  process.stderr.write(`react-luau-doctor: could not update inline review comments: ${errorMessage(error)}
10678
10673
  `);
@@ -12479,5 +12474,5 @@ ${os2.release()}
12479
12474
  }
12480
12475
  main();
12481
12476
 
12482
- //# debugId=77F2455CC417160A64756E2164756E21
12477
+ //# debugId=93BE5E0B263571E264756E2164756E21
12483
12478
  //# sourceMappingURL=cli.js.map