Skip to content

refactor(init): use runner detection in format.ts#124

Merged
wyattjoh merged 2 commits intomainfrom
refactor/format-uses-runners
Apr 14, 2026
Merged

refactor(init): use runner detection in format.ts#124
wyattjoh merged 2 commits intomainfrom
refactor/format-uses-runners

Conversation

@wyattjoh
Copy link
Copy Markdown
Contributor

@wyattjoh wyattjoh commented Apr 7, 2026

Summary

  • Replaces hardcoded ["npx", "prettier", ...] arrays in init/format.ts with the runner detection from lib/runners.ts. A bun project with no npx installed (e.g. Bun via Homebrew but no Node) now falls back to bunx instead of silently failing to format.
  • Wraps the Bun.spawn call in try/catch, formatting is best-effort and shouldn't break init even if the runner subprocess crashes.
  • Signature change: runFormatters now takes the full ProjectContext instead of just cwd, so it can read packageManager. Updates the lone caller in init/index.ts.

Stacked on #118.

Test plan

  • bun run test passes (59 passed)
  • Manual: on a bun-only machine without npx, run clerk init in a Prettier-using project and confirm formatting still runs (via bunx)

@wyattjoh
Copy link
Copy Markdown
Contributor Author

wyattjoh commented Apr 7, 2026

@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch from 47dc327 to 21072f9 Compare April 7, 2026 19:40
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch from 21072f9 to d52abd4 Compare April 7, 2026 20:23
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch 2 times, most recently from 133bd4a to ecae3c7 Compare April 8, 2026 21:39
@wyattjoh wyattjoh force-pushed the feat/lib-runners branch 2 times, most recently from 006f9cc to 515fd36 Compare April 9, 2026 20:23
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch 2 times, most recently from 3f687c8 to 207adbd Compare April 9, 2026 22:54
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch from 207adbd to dcb4afa Compare April 11, 2026 06:48
Base automatically changed from feat/lib-runners to main April 11, 2026 06:50
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch from dcb4afa to d700db1 Compare April 11, 2026 06:54
@wyattjoh wyattjoh marked this pull request as ready for review April 11, 2026 06:54
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a82fa97-fb78-41aa-8890-4d28eb5bc676

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3375e and f7ab840.

📒 Files selected for processing (3)
  • packages/cli-core/src/commands/init/format.test.ts
  • packages/cli-core/src/commands/init/format.ts
  • packages/cli-core/src/commands/init/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli-core/src/commands/init/index.ts

📝 Walkthrough

Walkthrough

runFormatters signature changed to accept a ProjectContext (ctx) and uses ctx.cwd and ctx.deps (falling back to reading package deps). Formatters are pre-filtered by dependency presence. Formatter configs now expose runner/args separately (binArgs) and no longer hardcode npx; available runners are detected and a preferred runner is chosen via preferredRunner(ctx.packageManager, available). Runner command and formatter args are composed into spawn invocations; spawns run with the project cwd and are wrapped in try/catch to swallow spawn errors while ignoring stdout/stderr. The caller in init/index.ts now passes ctx. A new Bun test suite for runFormatters was added.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main refactoring: switching to runner detection in format.ts, which aligns with the core change of replacing hardcoded 'npx' with dynamic runner selection.
Description check ✅ Passed The description is directly related to the changeset, explaining the rationale for replacing hardcoded runner commands, wrapping spawn in try/catch, and the signature change to accept ProjectContext.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch 3 times, most recently from 523c58a to 3a3375e Compare April 13, 2026 23:20
Replaces hardcoded `["npx", "prettier", ...]` arrays in init/format.ts
with the runner detection from lib/runners.ts. A bun project with no
`npx` installed (e.g. Bun via Homebrew but no Node) now falls back to
bunx instead of silently failing to format.

Also wraps the spawn call in try/catch, formatting is best-effort and
shouldn't break init even if the runner subprocess crashes.

Signature change: runFormatters now takes the full ProjectContext
instead of just cwd, so it can read packageManager. Updates the lone
caller in init/index.ts.
@wyattjoh wyattjoh force-pushed the refactor/format-uses-runners branch from 3a3375e to f7ab840 Compare April 14, 2026 18:56
rafa-thayto
rafa-thayto previously approved these changes Apr 14, 2026
@rafa-thayto rafa-thayto dismissed their stale review April 14, 2026 19:13

missclick

@wyattjoh wyattjoh merged commit a55f098 into main Apr 14, 2026
6 checks passed
@wyattjoh wyattjoh deleted the refactor/format-uses-runners branch April 14, 2026 19:13
Copy link
Copy Markdown
Contributor

@rafa-thayto rafa-thayto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments

Comment on lines +22 to +34
} catch {
// Bun.spawn may not be writable on some runtimes
}
}
function restoreSpawn() {
try {
(Bun as unknown as { spawn: typeof Bun.spawn }).spawn = origSpawn;
} catch {
// ignore
}
}

function mockWhich(present: ReadonlySet<string>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent mock failures make tests untrustworthy

The try/catch blocks silently swallow mock-assignment failures. If Bun makes these properties non-writable in a future version, every test runs against real globals with confusing results rather than failing fast.

Same issue in mockWhich (line 42) and mockSpawnSync (line 55) and all three restore* functions.

Suggestion — drop all try/catch blocks, verify the assignment took effect:

function setSpawn(impl: SpawnImpl) {
  (Bun as unknown as { spawn: SpawnImpl }).spawn = impl;
  if (Bun.spawn !== (impl as unknown)) {
    throw new Error("Failed to mock Bun.spawn — property may be non-writable");
  }
}

function restoreSpawn() {
  (Bun as unknown as { spawn: typeof Bun.spawn }).spawn = origSpawn;
}

Bonus — extract the cast once to reduce boilerplate (repeated 6× as-is):

const bunOverrides = Bun as unknown as {
  spawn: SpawnImpl;
  which: (bin: string) => string | null;
  spawnSync: (cmd: string[]) => { exitCode: number };
};

function setSpawn(impl: SpawnImpl) {
  bunOverrides.spawn = impl;
}
function restoreSpawn() {
  bunOverrides.spawn = origSpawn as unknown as SpawnImpl;
}
// ... etc

Comment on lines +2 to +5
// Pulls in the same runner detection skills.ts uses, so a bun project with
// no `npx` on PATH (entirely possible if the user installed Bun via Homebrew
// but never installed Node) will fall back to bunx instead of silently failing.
import { detectAvailableRunners, preferredRunner, runnerCommand } from "../../lib/runners.js";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads like commit-message rationale rather than a code comment. The function-level docstring already covers the best-effort / silent-failure contract, and the import names (detectAvailableRunners, preferredRunner) are self-documenting.

Suggestion: delete this comment block entirely — the PR description is the right place for the "why we're switching" context.

if (files.length === 0) return;

const deps = await readDeps(cwd);
const deps = ctx.deps && Object.keys(ctx.deps).length > 0 ? ctx.deps : await readDeps(ctx.cwd);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things on this line:

1. ctx.deps && is dead codedeps is typed Record<string, string> (not optional), and gatherContext always sets it to deps ?? {}. An empty object is truthy, so the && never short-circuits. Simplify to:

const deps = Object.keys(ctx.deps).length > 0 ? ctx.deps : await readDeps(ctx.cwd);

2. The disk fallback itself is redundant in practicegatherContext() already calls readDeps(cwd) and stores the result. If it returned null, ctx.deps becomes {}. Re-calling readDeps here will return the same null. The only scenario this fallback helps is when someone constructs a ProjectContext manually with deps: {} while a real package.json exists — which only happens in the test for this very code path.

Consider simplifying to just trust ctx.deps:

export async function runFormatters(ctx: ProjectContext, files: string[]): Promise<void> {
  if (files.length === 0) return;
  if (Object.keys(ctx.deps).length === 0) return;

  const matchingFormatters = FORMATTERS.filter((f) => f.pkg in ctx.deps);
  ...

Then update the "reads deps from disk when ctx.deps is empty" test to pass deps: { prettier: "3.0.0" } directly, and drop the "no-op when ctx.deps is empty and package.json is missing" test (already covered by "no-op when no supported formatter is in deps").

await runFormatters(ctx, ["x.ts"]);
expect(seenCwd).toBe(tempDir);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test: non-zero formatter exit code

Tests cover spawn throwing (swallowed correctly) and spawn succeeding (exit 0), but there's no test for a formatter exiting non-zero. The current code silently ignores exit codes (await proc.exited with no check), which is the right best-effort behavior — worth locking in with a test:

test("ignores non-zero exit code from formatter (best-effort)", async () => {
  setSpawn((cmd) => {
    spawnCalls.push(cmd);
    return { exited: Promise.resolve(1) };
  });
  const ctx = makeCtx({
    cwd: tempDir,
    packageManager: "bun",
    deps: { prettier: "3.0.0", "@biomejs/biome": "1.9.0" },
  });
  await runFormatters(ctx, ["x.ts"]);
  // Both formatters attempted despite prettier exiting non-zero
  expect(spawnCalls).toHaveLength(2);
});

wyattjoh added a commit that referenced this pull request Apr 14, 2026
- Drop commit-message-style comment above runners import; the docstring
  and import names cover the contract.
- Trust ctx.deps in runFormatters; gatherContext already reads deps from
  disk, so the fallback readDeps call was dead code in practice.
- Drop try/catch blocks around Bun.spawn/which/spawnSync mock setters;
  verify the assignment took effect instead, so a future non-writable
  property fails fast rather than silently running against real globals.
- Extract the Bun override cast once to reduce boilerplate.
- Add test locking in best-effort behavior when a formatter exits non-zero.
wyattjoh added a commit that referenced this pull request Apr 14, 2026
- Drop commit-message-style comment above runners import; the docstring
  and import names cover the contract.
- Trust ctx.deps in runFormatters; gatherContext already reads deps from
  disk, so the fallback readDeps call was dead code in practice.
- Drop try/catch blocks around Bun.spawn/which/spawnSync mock setters;
  verify the assignment took effect instead, so a future non-writable
  property fails fast rather than silently running against real globals.
- Extract the Bun override cast once to reduce boilerplate.
- Add test locking in best-effort behavior when a formatter exits non-zero.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants