Files
orca/mobile/src/session/github-pr-value-readers.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

211 lines
6.3 KiB
TypeScript

import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import type {
CheckStatus,
GitHubAssignableUser,
GitHubPRMergeMethod,
GitHubPRMergeMethodSettings,
GitHubPRReviewSummary,
GitHubRepositoryIdentity,
PRMergeableState,
PRReviewDecision,
PRState,
ProviderCheckSummary
} from '../../../src/shared/github/pull-request-types'
import type { HostedReviewProvider } from '../../../src/shared/hosted-review'
// Primitive + enum value readers shared by the github.* PR parsers. Each narrows
// `unknown` defensively (never throws) so RPC payloads can be parsed safely.
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
export function readString(value: unknown): string | undefined {
return typeof value === 'string' ? value : undefined
}
export function readNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}
export function readBoolean(value: unknown): boolean | undefined {
return typeof value === 'boolean' ? value : undefined
}
export function readStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return []
}
return value.flatMap((entry): string[] => {
const str = readString(entry)
return str === undefined ? [] : [str]
})
}
export function readProvider(value: unknown): HostedReviewProvider | undefined {
return value === 'github' ||
value === 'gitlab' ||
value === 'bitbucket' ||
value === 'azure-devops' ||
value === 'gitea' ||
value === 'unsupported'
? value
: undefined
}
export function readPRState(value: unknown): PRState | null {
return value === 'open' || value === 'closed' || value === 'merged' || value === 'draft'
? value
: null
}
export function readCheckStatus(value: unknown): CheckStatus {
return value === 'pending' || value === 'success' || value === 'failure' || value === 'neutral'
? value
: 'pending'
}
export function readMergeableState(value: unknown): PRMergeableState | undefined {
return value === 'MERGEABLE' || value === 'CONFLICTING' || value === 'UNKNOWN' ? value : undefined
}
export function readReviewDecision(value: unknown): PRReviewDecision | null | undefined {
if (value === null) {
return null
}
return value === 'APPROVED' || value === 'CHANGES_REQUESTED' || value === 'REVIEW_REQUIRED'
? value
: undefined
}
export function readCheckRunStatus(value: unknown): PRCheckDetail['status'] | null {
return value === 'queued' || value === 'in_progress' || value === 'completed' ? value : null
}
// Why: dropping `action_required` here rendered a merge-blocking approval gate as a pending
// check; the shared classifier counts it as a failure, so it must survive parsing.
export function readCheckRunConclusion(value: unknown): PRCheckDetail['conclusion'] {
return value === 'success' ||
value === 'failure' ||
value === 'cancelled' ||
value === 'timed_out' ||
value === 'action_required' ||
value === 'neutral' ||
value === 'skipped' ||
value === 'pending'
? value
: null
}
export function readAssignableUser(value: unknown): GitHubAssignableUser | null {
if (!isRecord(value)) {
return null
}
const login = readString(value.login)
if (login === undefined) {
return null
}
return {
login,
name: readString(value.name) ?? null,
avatarUrl: readString(value.avatarUrl) ?? ''
}
}
export function readAssignableUserArray(value: unknown): GitHubAssignableUser[] {
if (!Array.isArray(value)) {
return []
}
return value.flatMap((entry): GitHubAssignableUser[] => {
const parsed = readAssignableUser(entry)
return parsed ? [parsed] : []
})
}
export function readReviewSummary(value: unknown): GitHubPRReviewSummary | null {
if (!isRecord(value)) {
return null
}
// Desktop maps latestReviews to top-level `login`. Raw `gh pr view --json`
// keeps nested `author.login` — accept both so mobile never drops reviewers.
const nestedAuthor = isRecord(value.author) ? value.author : null
const login =
readString(value.login) ?? (nestedAuthor ? readString(nestedAuthor.login) : undefined)
if (login === undefined) {
return null
}
const avatarUrl =
readString(value.avatarUrl) ??
(nestedAuthor
? (readString(nestedAuthor.avatarUrl) ?? readString(nestedAuthor.avatar_url) ?? null)
: null)
return {
login,
state: readString(value.state) ?? null,
avatarUrl
}
}
export function readRepoIdentity(value: unknown): GitHubRepositoryIdentity | undefined {
if (!isRecord(value)) {
return undefined
}
const owner = readString(value.owner)
const repo = readString(value.repo)
// Empty owner/repo is malformed, not a valid identity — drop it before it reaches prRepo parsing.
if (!owner || !repo) {
return undefined
}
// Why: dropping `host` here would strip the GHES identity before every
// subsequent PR RPC, forcing the host to re-derive it per call.
const host = readString(value.host)
return { owner, repo, ...(host ? { host } : {}) }
}
function readMergeMethod(value: unknown): GitHubPRMergeMethod | undefined {
return value === 'merge' || value === 'squash' || value === 'rebase' ? value : undefined
}
export function readMergeMethodSettings(value: unknown): GitHubPRMergeMethodSettings | undefined {
if (!isRecord(value)) {
return undefined
}
const defaultMethod = readMergeMethod(value.defaultMethod)
if (defaultMethod === undefined || !isRecord(value.allowedMethods)) {
return undefined
}
const allowed = value.allowedMethods
return {
defaultMethod,
allowedMethods: {
merge: readBoolean(allowed.merge) ?? false,
squash: readBoolean(allowed.squash) ?? false,
rebase: readBoolean(allowed.rebase) ?? false
}
}
}
export function readCheckSummary(value: unknown): ProviderCheckSummary | undefined {
if (!isRecord(value)) {
return undefined
}
const state = value.state
if (
state !== 'success' &&
state !== 'failure' &&
state !== 'pending' &&
state !== 'neutral' &&
state !== 'none'
) {
return undefined
}
return {
state,
total: readNumber(value.total) ?? 0,
passed: readNumber(value.passed) ?? 0,
failed: readNumber(value.failed) ?? 0,
pending: readNumber(value.pending) ?? 0,
neutral: readNumber(value.neutral) ?? 0
}
}