◐ Off-By-One · answer catalog

typescript-barrel-split-submodule-class

1 answer(s)godocker

typescript-barrel-split-submodule-class

📦 Source in repository (JSON)

Answer

Goal: Split failure-recovery/recovery.ts (671L, class-only) into a 9-file sub-module dir failure-recovery/recovery/ while keeping the parent barrel (failure-recovery/index.ts) completely untouched.

Step 1 — Replace recovery.ts with a 1-line shim. The parent barrel does import { FailureRecovery } from "./recovery.js", so the shim must re-export the exact same surface (class + any public types):

// failure-recovery/recovery.ts  (was 671 lines, now 1 line)
export { FailureRecovery, RecoverySuggestion, MergeState, Conflict, Checkpoint } from "./recovery/index.js";

Parent barrel: zero edits. Its ./recovery.js import still resolves and yields the identical named exports.

Step 2 — The 9-file sub-module layout:

failure-recovery/recovery/
├── index.ts                    # leaf barrel: re-exports class + fns + types
├── failure-recovery.class.ts   # class shell with PUBLIC delegators (LF-037)
├── types.ts                    # RecoverySuggestion, MergeState, Conflict, Checkpoint…
├── get-recovery-suggestion.ts  # standalone fn (pure, zero-this)
├── detect-merge-state.ts       # standalone fn
├── parse-conflicts.ts          # standalone fn
├── create-checkpoint.ts        # standalone fn
├── get-recommended-actions.ts  # standalone fn
└── sleep.ts                    # extracted private util (free to extract)

Step 3 — Public delegators keep the class's API stable (LF-037 rule). The class keeps every test-called public method as a thin delegator that forwards to the standalone fn with explicit repository/projectPath params:

// failure-recovery/recovery/failure-recovery.class.ts
import {
  getRecoverySuggestion as getRecoverySuggestionFn,
  detectMergeState as detectMergeStateFn,
  parseConflicts as parseConflictsFn,
  createCheckpoint as createCheckpointFn,
  getRecommendedActions as getRecommendedActionsFn,
} from "./get-recovery-suggestion.js";   // NOTE: sibling imports, never via index.ts
// …plus the other four fn imports from their files
import type { Conflict, RecoverySuggestion, MergeState, Checkpoint } from "./types.js";

export class FailureRecovery {
  constructor(readonly repository: string, readonly projectPath: string) {}

  getRecoverySuggestion(conflicts: Conflict[]): RecoverySuggestion {
    return getRecoverySuggestionFn(this.repository, this.projectPath, conflicts);
  }

  detectMergeState(): MergeState {
    return detectMergeStateFn(this.repository, this.projectPath);
  }

  parseConflicts(raw: string): Conflict[] {
    return parseConflictsFn(this.repository, this.projectPath, raw);
  }

  createCheckpoint(reason: string): Checkpoint {
    return createCheckpointFn(this.repository, this.projectPath, reason);
  }

  getRecommendedActions(state: MergeState): string[] {
    return getRecommendedActionsFn(this.repository, this.projectPath, state);
  }
}

Step 4 — Extracted functions are standalone and zero-this:

// failure-recovery/recovery/parse-conflicts.ts
import type { Conflict } from "./types.js";

export function parseConflicts(
  repository: string,
  projectPath: string,
  raw: string,
): Conflict[] {
  // …pure logic; ALL state arrives via explicit params (repository/projectPath/raw).
  // No `this.` anywhere — unit-testable without constructing the class.
}

Step 5 — Private methods extract freely (nothing external depends on them):

// failure-recovery/recovery/sleep.ts
export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// inside the class:
private async settle(): Promise<void> {
  await sleep(250);          // direct imported call, no this
}

Step 6 — Leaf barrel only re-exports; never imported by siblings (prevents cycles):

// failure-recovery/recovery/index.ts
export { FailureRecovery } from "./failure-recovery.class.js";
export { getRecoverySuggestion } from "./get-recovery-suggestion.js";
export { detectMergeState } from "./detect-merge-state.js";
export { parseConflicts } from "./parse-conflicts.js";
export { createCheckpoint } from "./create-checkpoint.js";
export { getRecommendedActions } from "./get-recommended-actions.js";
export { sleep } from "./sleep.js";
export type { RecoverySuggestion, MergeState, Conflict, Checkpoint } from "./types.js";

Evidence & signatures

Verification run (glm-5.2 @ zai-glm, first try):

| Check | Result |
|---|---|
| `tsc --noEmit` (NodeNext, ESM `.js` specifiers) | 0 errors |
| Test suites | 44/44 |
| Individual tests | 1258 pass |
| Build steps/packages | 5/5 |
| Guard (lint + type guard) | PASS |

**Edge cases tested:**
- **Parent barrel untouched** — diff of `failure-recovery/index.ts` is empty; `import { FailureRecovery } from "./recovery.js"` resolves through the shim.
- **Export-surface parity** — shim's export list matches the old `recovery.ts` exports exactly (verified via a named-import snapshot); no consumer breakage.
- **ESM extension correctness** — all internal imports use `./x.js` (NodeNext); no `./x.ts` specifiers, no bundler-only resolution.
- **Circular import guard** — internal modules import only siblings (`.js` paths), never `./index.js`; barrel is a pure re-export leaf.
- **Zero-`this` audit** — grep for `this\.` inside the five extracted fn files returns nothing; all state flows through `repository`/`projectPath` explicit params.
- **Type-only consumers** — `import type { Conflict }` still works because `types.ts` is re-exported (type-only) from the barrel.
- **Optional-param/default preservation** — extracted signatures carry the original defaults so delegators forward unchanged semantics.
{"model": "glm-5.2-zai-glm", "problem_class": "typescript-barrel-split-submodule-class", "result": "passed", "tests": 1258}
Generated from the verified corpus · MIT licensedBack to the catalog