mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
#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.
181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
import { normalizeExecutionHostId } from './execution-host'
|
|
import type { ProjectGroup, ProjectGroupCreatedFrom } from './project-group-types'
|
|
import type { Repo } from './repo-types'
|
|
|
|
export const UNGROUPED_PROJECT_GROUP_KEY = 'project-group:ungrouped'
|
|
|
|
function createProjectGroupId(): string {
|
|
const randomUUID = globalThis.crypto?.randomUUID
|
|
if (randomUUID) {
|
|
return randomUUID.call(globalThis.crypto)
|
|
}
|
|
return `project-group-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
}
|
|
|
|
export function normalizeProjectGroupName(name: string, fallback = 'Untitled group'): string {
|
|
const trimmed = name.trim()
|
|
return trimmed.length > 0 ? trimmed : fallback
|
|
}
|
|
|
|
export function createProjectGroup(input: {
|
|
name: string
|
|
parentPath?: string | null
|
|
connectionId?: string | null
|
|
parentGroupId?: string | null
|
|
createdFrom: ProjectGroupCreatedFrom
|
|
tabOrder: number
|
|
now?: number
|
|
}): ProjectGroup {
|
|
const now = input.now ?? Date.now()
|
|
return {
|
|
id: createProjectGroupId(),
|
|
name: normalizeProjectGroupName(input.name),
|
|
parentPath: input.parentPath ?? null,
|
|
connectionId: input.connectionId ?? null,
|
|
parentGroupId: input.parentGroupId ?? null,
|
|
createdFrom: input.createdFrom,
|
|
tabOrder: input.tabOrder,
|
|
isCollapsed: false,
|
|
color: null,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
}
|
|
}
|
|
|
|
export function normalizeProjectGroups(value: unknown): ProjectGroup[] {
|
|
if (!Array.isArray(value)) {
|
|
return []
|
|
}
|
|
const groups: ProjectGroup[] = []
|
|
const seen = new Set<string>()
|
|
for (const candidate of value) {
|
|
if (!candidate || typeof candidate !== 'object') {
|
|
continue
|
|
}
|
|
const raw = candidate as Partial<ProjectGroup>
|
|
if (typeof raw.id !== 'string' || seen.has(raw.id)) {
|
|
continue
|
|
}
|
|
seen.add(raw.id)
|
|
const now = Date.now()
|
|
const executionHostId = normalizeExecutionHostId(raw.executionHostId)
|
|
groups.push({
|
|
id: raw.id,
|
|
name: normalizeProjectGroupName(typeof raw.name === 'string' ? raw.name : ''),
|
|
parentPath: typeof raw.parentPath === 'string' ? raw.parentPath : null,
|
|
connectionId:
|
|
typeof raw.connectionId === 'string'
|
|
? raw.connectionId
|
|
: raw.connectionId === null
|
|
? null
|
|
: null,
|
|
parentGroupId: typeof raw.parentGroupId === 'string' ? raw.parentGroupId : null,
|
|
createdFrom:
|
|
raw.createdFrom === 'manual' ||
|
|
raw.createdFrom === 'folder-scan' ||
|
|
raw.createdFrom === 'migration'
|
|
? raw.createdFrom
|
|
: 'manual',
|
|
tabOrder:
|
|
typeof raw.tabOrder === 'number' && Number.isFinite(raw.tabOrder) ? raw.tabOrder : 0,
|
|
isCollapsed: raw.isCollapsed === true,
|
|
color: typeof raw.color === 'string' ? raw.color : null,
|
|
createdAt:
|
|
typeof raw.createdAt === 'number' && Number.isFinite(raw.createdAt) ? raw.createdAt : now,
|
|
updatedAt:
|
|
typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : now,
|
|
// Why: runtime-owned groups otherwise look local after persistence reload.
|
|
...(executionHostId ? { executionHostId } : {})
|
|
})
|
|
}
|
|
groups.sort(
|
|
(left, right) => left.tabOrder - right.tabOrder || left.name.localeCompare(right.name)
|
|
)
|
|
const groupIds = new Set(groups.map((group) => group.id))
|
|
for (const group of groups) {
|
|
if (group.parentGroupId === group.id || !groupIds.has(group.parentGroupId ?? '')) {
|
|
group.parentGroupId = null
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
export function clearMissingProjectGroupMemberships(repos: Repo[], groups: ProjectGroup[]): Repo[] {
|
|
const groupIds = new Set(groups.map((group) => group.id))
|
|
return repos.map((repo) =>
|
|
repo.projectGroupId && !groupIds.has(repo.projectGroupId)
|
|
? { ...repo, projectGroupId: null }
|
|
: repo
|
|
)
|
|
}
|
|
|
|
export function getProjectGroupSubtreeIds(
|
|
groups: readonly Pick<ProjectGroup, 'id' | 'parentGroupId'>[],
|
|
rootGroupId: string
|
|
): Set<string> {
|
|
const childGroupsByParentId = new Map<string, string[]>()
|
|
for (const group of groups) {
|
|
if (!group.parentGroupId) {
|
|
continue
|
|
}
|
|
const children = childGroupsByParentId.get(group.parentGroupId) ?? []
|
|
children.push(group.id)
|
|
childGroupsByParentId.set(group.parentGroupId, children)
|
|
}
|
|
|
|
const subtreeIds = new Set<string>()
|
|
const pending = [rootGroupId]
|
|
while (pending.length > 0) {
|
|
const groupId = pending.pop()!
|
|
if (subtreeIds.has(groupId)) {
|
|
continue
|
|
}
|
|
subtreeIds.add(groupId)
|
|
// Why: imported project-group trees can be very wide; `push(...children)`
|
|
// can exceed V8's argument limit while collecting descendants.
|
|
for (const childGroupId of childGroupsByParentId.get(groupId) ?? []) {
|
|
pending.push(childGroupId)
|
|
}
|
|
}
|
|
return subtreeIds
|
|
}
|
|
|
|
/** Manual rank for a project inside a group bucket. Explicit
|
|
* `projectGroupOrder` wins; otherwise fall back to global repo order so drag
|
|
* midpoint math and sidebar sorting stay aligned. */
|
|
export function getEffectiveProjectGroupManualRank(
|
|
repo: Pick<Repo, 'id' | 'projectGroupOrder'> | undefined,
|
|
repoOrderRankById?: ReadonlyMap<string, number>,
|
|
siblingFallbackIndex?: number
|
|
): number {
|
|
if (!repo) {
|
|
return Number.POSITIVE_INFINITY
|
|
}
|
|
const order = repo.projectGroupOrder
|
|
if (typeof order === 'number' && Number.isFinite(order)) {
|
|
return order
|
|
}
|
|
const repoRank = repoOrderRankById?.get(repo.id)
|
|
if (repoRank !== undefined) {
|
|
return repoRank * 1000
|
|
}
|
|
if (siblingFallbackIndex !== undefined) {
|
|
return siblingFallbackIndex * 1000
|
|
}
|
|
return Number.POSITIVE_INFINITY
|
|
}
|
|
|
|
export function getNextProjectGroupOrder(repos: readonly Repo[], groupId: string | null): number {
|
|
let max = -1
|
|
for (const repo of repos) {
|
|
if ((repo.projectGroupId ?? null) !== groupId) {
|
|
continue
|
|
}
|
|
const order = repo.projectGroupOrder
|
|
if (typeof order === 'number' && Number.isFinite(order)) {
|
|
max = Math.max(max, order)
|
|
}
|
|
}
|
|
return max + 1
|
|
}
|