Skip to content

perf(packages/core): replace system_profiler with pure JS font scanning#86

Merged
zrosenbauer merged 9 commits intomainfrom
perf/icons-middleware-startup
Mar 18, 2026
Merged

perf(packages/core): replace system_profiler with pure JS font scanning#86
zrosenbauer merged 9 commits intomainfrom
perf/icons-middleware-startup

Conversation

@zrosenbauer
Copy link
Member

Summary

  • Problem: Icons middleware took ~9.5s to start on macOS — system_profiler SPFontsDataType enumerates every installed font with full metadata
  • Fix: Pure JS directory scanner (readdir/lstat) that checks well-known font directories per OS, bringing startup to ~140ms (~97% improvement)
  • Benchmarking: Added CodSpeed + vitest bench for automated performance regression tracking in CI

Changes

  • list-system-fonts.ts — replaced shell command with recursive font directory scanning
    • macOS: ~/Library/Fonts, /Library/Fonts, /System/Library/Fonts, /System/Library/Fonts/Supplemental, /Network/Library/Fonts
    • Linux: /usr/share/fonts, /usr/local/share/fonts, ~/.fonts, ~/.local/share/fonts
    • Windows: %WINDIR%\Fonts, %LOCALAPPDATA%\Microsoft\Windows\Fonts
  • Uses lstat (not stat) to prevent infinite loops from cyclic symlinks
  • Zero new runtime dependencies — pure node:fs/promises
  • Added CodSpeed vitest plugin + GitHub Actions workflow for benchmark tracking

Performance

Before After
Startup time 9.5s ~140ms
Approach system_profiler SPFontsDataType readdir + lstat

Test plan

  • pnpm check passes (typecheck + lint + format)
  • pnpm test passes (735 unit + 62 integration)
  • pnpm test:bench runs successfully
  • Manual verification: time ./examples/icons/dist/index.mjs show 0 completes in <300ms

…ory scanning

The icons middleware was taking ~9.5 seconds to start on macOS because
`system_profiler SPFontsDataType` enumerates every installed font with
full metadata. Replace with a pure JS implementation that scans
well-known font directories using `readdir`/`lstat`, bringing startup
down to ~140ms (~97% improvement).

- Scan platform-specific font dirs (macOS, Linux, Windows)
- Use `lstat` instead of `stat` to prevent symlink loop hangs
- Skip symlinks entirely during recursive traversal
- Add CodSpeed benchmarking with vitest bench for CI regression tracking
- Add dedicated GitHub Actions workflow for benchmarks

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 18, 2026

🦋 Changeset detected

Latest commit: 6a84825

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@kidd-cli/core Minor
@kidd-cli/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 18, 2026

Congrats! CodSpeed is installed 🎉

🆕 2 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

zrosenbauer and others added 2 commits March 18, 2026 15:51
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 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
📝 Walkthrough

Walkthrough

Adds benchmark infrastructure (GitHub Actions workflow .github/workflows/codspeed.yml, vitest.bench.config.ts, tests/bench/icons.bench.ts, and package.json script/devDependency for CodSpeed) and updates CI workflows to Node.js 24. Refactors system font enumeration: replaces shell/command-based discovery with recursive fs-based directory traversal (list-system-fonts.ts) and updates detectNerdFonts accordingly. Tests updated to mock fs traversal (list-system-fonts.test.ts). Example runner error handling now throws on non-zero exit results (tests/helpers.ts).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: replacing system_profiler with pure JavaScript font scanning in the packages/core directory.
Description check ✅ Passed The description comprehensively covers the problem, solution, implementation details, performance improvements, and testing approach—all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/icons-middleware-startup
📝 Coding Plan
  • Generate coding plan for human review comments

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

coderabbitai[bot]
coderabbitai bot previously requested changes Mar 18, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/middleware/icons/list-system-fonts.test.ts (1)

37-37: ⚠️ Potential issue | 🟠 Major

Add configurable: true to every process.platform override.

These Object.defineProperty() calls create non-configurable descriptors, causing the restore in afterEach() to throw when subsequent tests attempt to override process.platform. All 5 instances (lines 37, 43, 141, 163, 174) must include configurable: true in the descriptor.

🐛 Proposed fix
-    Object.defineProperty(process, 'platform', { value: originalPlatform })
+    Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true })

Apply the same fix to lines 43, 141, 163, and 174.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/middleware/icons/list-system-fonts.test.ts` at line 37,
Every override of process.platform in list-system-fonts.test.ts is defined as a
non-configurable property which makes restoring it in afterEach() throw; update
each Object.defineProperty call that sets process.platform (e.g., the calls
currently written as Object.defineProperty(process, 'platform', { value: ... }))
to include configurable: true in the descriptor so the property can be
redefined/restored; apply this change to all five occurrences referenced in the
review (the overrides around the platform-specific test cases and the restore in
afterEach()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/middleware/icons/list-system-fonts.ts`:
- Around line 95-99: The current fallback for localAppData erroneously uses
process.env['APPDATA'] which points to a different profile tree; change the
localAppData assignment in list-system-fonts.ts so it only uses
process.env['LOCALAPPDATA'] and if unset falls back to join(home, 'AppData',
'Local') (remove the APPDATA fallback), and ensure the returned paths still use
this corrected localAppData variable when building the per-user Fonts path.
- Around line 177-183: safeReaddir and safeLstat are currently swallowing all fs
errors and returning empty/nullable values, causing listSystemFonts to appear
successful on unexpected failures; change both functions to only suppress
expected "missing path" errors (ENOENT, ENOTDIR) and otherwise surface the error
through the Result-style tuple used in this codebase (do not throw). Update the
functions' signatures to return a Result-like tuple (e.g., [value, error]) and
update callers — notably listSystemFonts and detectNerdFonts — to check the
returned error and propagate it instead of treating an empty array/null as
success; ensure tests and any downstream handling follow the tuple convention.

In `@tests/bench/icons.bench.ts`:
- Around line 8-13: The benchmark runner currently ignores the child process
exit status so failures are treated as successful runs; update
createExampleRunner (the helper used by these benches) to capture and return
spawnSync's exit status (or throw on non-zero status) along with combined
output, and then adjust the bench usage (the run call in
tests/bench/icons.bench.ts) to assert/throw when the returned status is
non-zero; ensure you reference the spawnSync result fields (status, error) in
createExampleRunner and propagate/raise errors so the bench fails when the CLI
crashes or build is missing.

In `@vitest.bench.config.ts`:
- Around line 3-9: The current config mutates a local array (const plugins = [];
plugins.push(...)) inside defineConfig; replace this with an immutable
expression that constructs the plugins array without push, e.g. build and return
plugins via a conditional expression that uses await
import('@codspeed/vitest-plugin') only when process.env['CI'] is truthy, invoke
the imported default (codspeedPlugin) to include the plugin, and otherwise
return an empty array—keeping the async defineConfig function and referencing
defineConfig, process.env['CI'], and the imported codspeedPlugin by name so no
mutable plugins array or push() calls remain.

---

Outside diff comments:
In `@packages/core/src/middleware/icons/list-system-fonts.test.ts`:
- Line 37: Every override of process.platform in list-system-fonts.test.ts is
defined as a non-configurable property which makes restoring it in afterEach()
throw; update each Object.defineProperty call that sets process.platform (e.g.,
the calls currently written as Object.defineProperty(process, 'platform', {
value: ... })) to include configurable: true in the descriptor so the property
can be redefined/restored; apply this change to all five occurrences referenced
in the review (the overrides around the platform-specific test cases and the
restore in afterEach()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 107daf64-76b1-443f-b512-909a16fdad36

📥 Commits

Reviewing files that changed from the base of the PR and between b47dee5 and de5d237.

⛔ Files ignored due to path filters (2)
  • .changeset/perf-icons-startup.md is excluded by !.changeset/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (7)
  • .github/workflows/codspeed.yml
  • package.json
  • packages/core/src/middleware/icons/detect.ts
  • packages/core/src/middleware/icons/list-system-fonts.test.ts
  • packages/core/src/middleware/icons/list-system-fonts.ts
  • tests/bench/icons.bench.ts
  • vitest.bench.config.ts

Fix Node.js 20 deprecation warnings by upgrading to v5 which
supports Node.js 24.

Co-Authored-By: Claude <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously requested changes Mar 18, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/codspeed.yml:
- Around line 36-46: The workflow runs "Run Benchmarks" after "Build"; move the
"Run Benchmarks" step (uses: CodSpeedHQ/action@v4 with run: pnpm test:bench) to
occur before the "Build" step (the step named "Build" that runs pnpm build) or
remove the "Build" step entirely if benchmarks don't require a build; ensure
"Rebuild bin links" (pnpm install --frozen-lockfile) remains in the correct
place relative to the benchmark invocation so pnpm test:bench has required
dependencies.
- Line 19: The workflow uses a non-GitHub-hosted runner label "runs-on:
codspeed-macro" which requires organization-specific self-hosted runners; either
configure self-hosted runners with the codspeed-macro label or change the job's
runner to a GitHub-hosted label (for example replace the value of runs-on in the
workflow with "ubuntu-latest") so the job will actually execute; locate the
"runs-on: codspeed-macro" entry in the codspeed.yml workflow and update it
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7dbde23-4080-45c5-b44a-8ca4eff9182c

📥 Commits

Reviewing files that changed from the base of the PR and between de5d237 and 97a3fde.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • .github/workflows/codspeed.yml

- Add `configurable: true` to all Object.defineProperty process.platform
  overrides in tests to prevent restore failures
- Remove incorrect APPDATA fallback for Windows LOCALAPPDATA (APPDATA
  points to Roaming, not Local)
- Extract CodSpeed plugin loading into named function to avoid ternary
  and await expression member access
- Check spawnSync exit status in createExampleRunner so benchmarks
  fail on CLI crashes

Co-Authored-By: Claude <noreply@anthropic.com>
@zrosenbauer
Copy link
Member Author

@coderabbitai resolve

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Comments resolved and changes approved.

@zrosenbauer zrosenbauer dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] March 18, 2026 20:32

All actionable items fixed: configurable:true on Object.defineProperty, APPDATA fallback removed, vitest.bench.config immutable, createExampleRunner exit status check. Remaining comments (codspeed-macro runner, build order, safeReaddir error swallowing) are intentional design decisions.

zrosenbauer and others added 2 commits March 18, 2026 16:37
The bench tests require examples/icons/dist/index.mjs which is not
built by `pnpm build` (only builds packages). Add explicit step to
build the icons example before running benchmarks.

Co-Authored-By: Claude <noreply@anthropic.com>
Bare `turbo` isn't on PATH in the CodSpeed macro runner;
use `pnpm turbo` to resolve it from node_modules.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/core/src/middleware/icons/list-system-fonts.ts (1)

176-182: ⚠️ Potential issue | 🟠 Major

Do not swallow all filesystem errors in safeReaddir/safeLstat.

Current behavior treats every fs failure as “missing path,” which can silently mask real scan failures and return false negatives as successful results.

🐛 Proposed fix (only suppress missing-path errors)
+function isMissingPathError(error: unknown): boolean {
+  if (!(error instanceof Error)) {
+    return false
+  }
+  const code = (error as NodeJS.ErrnoException).code
+  return code === 'ENOENT' || code === 'ENOTDIR'
+}
+
 async function safeReaddir(dir: string): Promise<readonly string[]> {
   try {
     return await readdir(dir)
-  } catch {
-    return []
+  } catch (error) {
+    if (isMissingPathError(error)) {
+      return []
+    }
+    return Promise.reject(error)
   }
 }
 
 async function safeLstat(
   filePath: string
 ): Promise<ReturnType<typeof lstat> extends Promise<infer T> ? T | null : never> {
   try {
     return await lstat(filePath)
-  } catch {
-    return null
+  } catch (error) {
+    if (isMissingPathError(error)) {
+      return null
+    }
+    return Promise.reject(error)
   }
 }
#!/bin/bash
set -euo pipefail

echo "Current fs helper error handling:"
rg -n "async function safe(Readdir|Lstat)|catch" packages/core/src/middleware/icons/list-system-fonts.ts -A6 -B3

echo
echo "Current test coverage of fs error codes:"
rg -n "ENOENT|ENOTDIR|EACCES|EPERM" packages/core/src/middleware/icons/list-system-fonts.test.ts -A2 -B2

Expected verification result: only missing-path errors should be suppressed; unexpected fs errors should propagate to err(...) through listSystemFonts.
As per coding guidelines, contributing/standards/typescript/errors.md requires explicit Result-style error propagation in multi-step flows instead of collapsing failures into success-like values.

Also applies to: 195-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/middleware/icons/list-system-fonts.ts` around lines 176 -
182, The helpers safeReaddir and safeLstat currently swallow all fs errors;
change their catch clauses to only suppress missing-path errors by checking the
error.code (e.g., ENOENT and ENOTDIR) and return the existing "missing" sentinel
(e.g., [] from safeReaddir) in those cases, but rethrow any other errors so they
propagate up to listSystemFonts and its err(...) handling; locate safeReaddir
and safeLstat in the diff and replace the bare catch with: catch (e) { if ((e as
NodeJS.ErrnoException)?.code === 'ENOENT' || (e as NodeJS.ErrnoException)?.code
=== 'ENOTDIR') return /* original missing value */; throw e }.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/helpers.ts`:
- Around line 30-36: The helper currently throws on failure paths (when
result.error or nonzero result.status); change the runner to return a
Result-style tuple instead of throwing: when result.error exists return
[result.error, null] (or equivalent Error-first tuple), and when result.status
!== 0 return [new Error(`CLI exited with status ${String(result.status)}:
${result.stderr}`), null]; on success return [null, result] (or [null, <success
value>]). Update the function signature/return type (e.g., in the runner
function that references the local variable result) to reflect the Result tuple
and remove the throw statements so callers receive the tuple rather than
exceptions.

---

Duplicate comments:
In `@packages/core/src/middleware/icons/list-system-fonts.ts`:
- Around line 176-182: The helpers safeReaddir and safeLstat currently swallow
all fs errors; change their catch clauses to only suppress missing-path errors
by checking the error.code (e.g., ENOENT and ENOTDIR) and return the existing
"missing" sentinel (e.g., [] from safeReaddir) in those cases, but rethrow any
other errors so they propagate up to listSystemFonts and its err(...) handling;
locate safeReaddir and safeLstat in the diff and replace the bare catch with:
catch (e) { if ((e as NodeJS.ErrnoException)?.code === 'ENOENT' || (e as
NodeJS.ErrnoException)?.code === 'ENOTDIR') return /* original missing value */;
throw e }.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84de7672-c297-4239-b864-984ba04c488d

📥 Commits

Reviewing files that changed from the base of the PR and between 97a3fde and f7a59e3.

📒 Files selected for processing (5)
  • .github/workflows/codspeed.yml
  • packages/core/src/middleware/icons/list-system-fonts.test.ts
  • packages/core/src/middleware/icons/list-system-fonts.ts
  • tests/helpers.ts
  • vitest.bench.config.ts

- Bump all workflows to actions/checkout@v5 and actions/setup-node@v5
- Set node-version: 24 across all workflows
- Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 env to eliminate deprecation
  warnings from third-party actions still bundled with Node 20
- Add engines.node >= 24 to root package.json

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
.github/workflows/codspeed.yml (1)

39-52: ⚠️ Potential issue | 🟠 Major

Run benchmark tests before the full build step.

The workflow currently executes benchmarks after pnpm build. Reorder so benchmark tests run first (or drop the full build if not required), while keeping only the minimum prerequisite build needed for the benchmark target.

Proposed step order adjustment
-      - name: Build
-        run: pnpm build
-
       - name: Rebuild bin links
         run: pnpm install --frozen-lockfile

       - name: Build benchmark examples
         run: pnpm turbo run build --filter=@examples/icons

       - name: Run Benchmarks
         uses: CodSpeedHQ/action@v4
         with:
           mode: walltime
           run: pnpm test:bench
+
+      - name: Build
+        run: pnpm build

As per coding guidelines, .github/workflows/**/*.yml must "Ensure tests run before build".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/codspeed.yml around lines 39 - 52, Reorder the CI steps so
the "Run Benchmarks" step executes before the full "Build" step: move the "Run
Benchmarks" (uses: CodSpeedHQ/action@v4 with run: pnpm test:bench) above the
"Build" step and ensure its minimal prerequisites remain directly before it—keep
"Rebuild bin links" (run: pnpm install --frozen-lockfile) and the "Build
benchmark examples" (run: pnpm turbo run build --filter=@examples/icons)
immediately before the "Run Benchmarks" step (or drop the full "Build" step
entirely if not required for benchmarks) so benchmarks run prior to the larger
build.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/codspeed.yml:
- Around line 39-52: Reorder the CI steps so the "Run Benchmarks" step executes
before the full "Build" step: move the "Run Benchmarks" (uses:
CodSpeedHQ/action@v4 with run: pnpm test:bench) above the "Build" step and
ensure its minimal prerequisites remain directly before it—keep "Rebuild bin
links" (run: pnpm install --frozen-lockfile) and the "Build benchmark examples"
(run: pnpm turbo run build --filter=@examples/icons) immediately before the "Run
Benchmarks" step (or drop the full "Build" step entirely if not required for
benchmarks) so benchmarks run prior to the larger build.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32b94722-077f-480d-a179-a25fd681e3b5

📥 Commits

Reviewing files that changed from the base of the PR and between f7a59e3 and 3f06e70.

📒 Files selected for processing (5)
  • .github/workflows/changeset-check.yml
  • .github/workflows/ci.yml
  • .github/workflows/codspeed.yml
  • .github/workflows/release.yml
  • package.json

Copy link
Member Author

@zrosenbauer zrosenbauer left a comment

Choose a reason for hiding this comment

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

❌ Request Changes

Please address comments in review.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/core/src/middleware/icons/list-system-fonts.ts (1)

174-180: ⚠️ Potential issue | 🟠 Major

Propagate unexpected filesystem failures instead of silently returning empty results.

Line 177 and Line 198 currently collapse all readdir/lstat errors into []/null. That can make font detection report a false negative when scanning actually failed (e.g., EACCES, EIO). Only suppress missing-path cases (like ENOENT/ENOTDIR) and let other failures propagate to listSystemFonts() as an error Result.

Proposed fix
+const isMissingPathError = (error: unknown): boolean =>
+  match(error)
+    .when(
+      (value): value is NodeJS.ErrnoException =>
+        value instanceof Error && 'code' in value,
+      ({ code }) => code === 'ENOENT' || code === 'ENOTDIR'
+    )
+    .otherwise(() => false)
+
 async function safeReaddir(dir: string): Promise<readonly string[]> {
   try {
     return await readdir(dir)
-  } catch {
-    return []
+  } catch (error) {
+    if (isMissingPathError(error)) {
+      return []
+    }
+    return Promise.reject(error)
   }
 }
@@
 async function safeLstat(
   filePath: string
 ): Promise<ReturnType<typeof lstat> extends Promise<infer T> ? T | null : never> {
   try {
     return await lstat(filePath)
-  } catch {
-    return null
+  } catch (error) {
+    if (isMissingPathError(error)) {
+      return null
+    }
+    return Promise.reject(error)
   }
 }

As per coding guidelines, contributing/standards/typescript/errors.md requires explicit Result-style error propagation for multi-step flows instead of masking unexpected failures.

Also applies to: 193-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/middleware/icons/list-system-fonts.ts` around lines 174 -
180, The helper functions (safeReaddir and the analogous safeLstat) currently
swallow all FS errors and return []/null; change them to only suppress
missing-path errors (errno/code ENOENT and ENOTDIR) and for any other error
rethrow (or return a propagated error Result) so listSystemFonts() receives
failures instead of false-negative empty results; update safeReaddir to catch
the error, if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return [] else
throw err (and apply the same pattern to safeLstat), ensuring listSystemFonts's
multi-step flow can surface unexpected filesystem errors as Results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/middleware/icons/list-system-fonts.ts`:
- Around line 51-62: The exported function listSystemFonts must not use
try/catch; remove the internal try/catch and instead wrap the promise chain
using the async Result wrapper pattern: call
Promise.all(dirs.map(scanDirectory)) and convert that promise into a Result via
the project's async-to-Result helper (the "async wrapper" from the standards),
then on success flatten the nested arrays to produce fonts and return ok(fonts),
and on failure return err(error); keep references to listSystemFonts, dirs,
scanDirectory, ok and err so reviewers can locate the changes.
- Around line 132-148: Replace the multi-branch if chain in processEntry with a
ts-pattern match on the awaited info value: after acquiring info via await
safeLstat(fullPath), use match(info) from 'ts-pattern' and add cases for null
(return []), a case where info.isSymbolicLink() (return []), info.isDirectory()
(return scanDirectory(fullPath)), and a case for info.isFile() &&
isFontFile(fullPath) (return [fullPath]), with an otherwise() returning [];
ensure you import match from 'ts-pattern' and preserve the existing async/await
behavior and returns from processEntry, referencing the function processEntry
and helpers safeLstat, scanDirectory, and isFontFile.

---

Duplicate comments:
In `@packages/core/src/middleware/icons/list-system-fonts.ts`:
- Around line 174-180: The helper functions (safeReaddir and the analogous
safeLstat) currently swallow all FS errors and return []/null; change them to
only suppress missing-path errors (errno/code ENOENT and ENOTDIR) and for any
other error rethrow (or return a propagated error Result) so listSystemFonts()
receives failures instead of false-negative empty results; update safeReaddir to
catch the error, if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return []
else throw err (and apply the same pattern to safeLstat), ensuring
listSystemFonts's multi-step flow can surface unexpected filesystem errors as
Results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fdb38b0c-7f4b-490d-9667-ced9c565008a

📥 Commits

Reviewing files that changed from the base of the PR and between 3f06e70 and 6a84825.

📒 Files selected for processing (1)
  • packages/core/src/middleware/icons/list-system-fonts.ts

@zrosenbauer zrosenbauer merged commit d6e831c into main Mar 18, 2026
5 checks passed
@zrosenbauer zrosenbauer deleted the perf/icons-middleware-startup branch March 18, 2026 21:06
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.

1 participant