Skip to content

perf: enable Node.js on-disk compile cache for faster warm starts#633

Merged
toiroakr merged 19 commits into
mainfrom
perf/cli-start
Jul 18, 2026
Merged

perf: enable Node.js on-disk compile cache for faster warm starts#633
toiroakr merged 19 commits into
mainfrom
perf/cli-start

Conversation

@toiroakr

@toiroakr toiroakr commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • runMain now enables the Node.js on-disk compile cache (V8 code cache) automatically — Node >= 22.8.0, silent no-op otherwise — so dynamically imported modules such as lazy() subcommands skip recompilation on warm starts. The cache directory follows the same XDG convention as the shell-completion workers (${XDG_CACHE_HOME:-$HOME/.cache}/<command name>/node-compile-cache), so direct CLI runs and TAB-time completion invocations share one warm cache; NODE_COMPILE_CACHE always takes precedence. Opt out or override via the new MainOptions.compileCache option (false | custom directory).
  • New dependency-free politty/compile-cache subpath exporting enableCompileCache. ESM static imports are compiled during the link phase — before any code runs — so covering the whole CLI graph (politty, zod, command definitions) requires the bin-shim pattern documented in docs/recipes.md: a minimal entry that enables the cache first and loads the real CLI with a dynamic import.
  • New politty generate-shim CLI command (and generateCompileCacheShim export) that generates the bin shim at build time, so it can be wired into a postbuild/prepack script instead of living in source. The program name defaults to the first bin name in package.json; .cjs outputs and .js outputs in non-"type": "module" packages are rejected with a pointer to .mjs.
  • politty's own bin (src/cli.ts) is converted into such a shim, with the actual CLI moved to src/cli-main.ts.
  • vitest now points XDG_CACHE_HOME at the OS tmpdir so test runs never litter the real user cache with fixture command names.

Notes

  • Measured with hyperfine on dist/cli.js --help: ~122 ms cold (cache wiped per run) → ~99 ms warm (~19% faster).
  • Verified end-to-end that cache files are written under <XDG>/politty/node-compile-cache/, that a fresh-process integration test covers directory derivation and NODE_COMPILE_CACHE precedence, and that a generate-shim-generated shim runs a demo CLI and populates its cache directory.

Code Metrics Report

main (c609cf1) #633 (137a21c) +/-
Coverage 92.2% 92.1% -0.1%
Test Execution Time 17s 17s 0s
Details
  |                     | main (c609cf1) | #633 (137a21c) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          92.2% |          92.1% | -0.1% |
  |   Files             |             72 |             75 |    +3 |
  |   Lines             |           7798 |           7923 |  +125 |
+ |   Covered           |           7191 |           7304 |  +113 |
  | Test Execution Time |            17s |            17s |    0s |

Code coverage of files in pull request scope (91.5% → 91.2%)

Files Coverage +/- Status
src/cli-main.ts 0.0% 0.0% added
src/cli.ts 0.0% 0.0% modified
src/compile-cache-shim.ts 97.8% +97.8% added
src/compile-cache.ts 91.3% +91.3% added
src/core/runner.ts 93.8% +0.0% modified
src/index.ts 0.0% 0.0% modified
src/types.ts 0.0% 0.0% modified

Reported by octocov

Summary by CodeRabbit

  • New Features
    • Improved CLI warm starts by enabling Node.js V8 on-disk compile caching automatically (Node >= 22.8.0), with NODE_COMPILE_CACHE precedence and opt-out/custom directory via runMain’s compileCache.
    • Added politty generate-shim plus politty/compile-cache utilities to generate executable shims per declared bin entry for faster startup (with safety rules around overwriting/self-import).
  • Documentation
    • Added compile-cache and generate-shim setup guidance to the README and API/recipes docs.
  • Tests
    • Added Vitest coverage for compile-cache behavior, shim generation/overwrite/validation rules, and runMain integration.

- runMain now enables the V8 compile cache automatically (Node >= 22.8.0;
  silent no-op otherwise) so dynamically imported modules such as lazy()
  subcommands skip recompilation on warm starts. The cache directory
  follows the same XDG convention as the shell-completion workers, so
  direct runs and completion invocations share one warm cache. Opt out or
  override via the new MainOptions.compileCache option.
- Add a dependency-free politty/compile-cache subpath exporting
  enableCompileCache for the bin-shim pattern: ESM static imports are
  compiled during the link phase, so caching the whole CLI graph requires
  enabling the cache in a minimal shim that loads the real entry with a
  dynamic import.
- Convert politty's own bin (src/cli.ts) into such a shim, with the
  actual CLI moved to src/cli-main.ts. Measured on dist/cli.js --help:
  ~122ms cold to ~99ms warm.
- Point vitest's XDG_CACHE_HOME at the OS tmpdir so test runs never
  litter the real user cache with fixture command names.
Copilot AI review requested due to automatic review settings July 16, 2026 14:13
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

This PR adds Node.js V8 compile-cache support, integrates it into runMain and CLI startup, provides executable shim generation, exposes new APIs and package exports, and documents and tests the behavior.

Changes

Compile-cache startup and shim generation

Layer / File(s) Summary
Compile-cache runtime and runMain integration
src/compile-cache.ts, src/core/runner.ts, src/types.ts, src/cli.ts, package.json, src/*compile-cache*.test.ts, vitest.config.ts
Compile-cache directory resolution, runtime activation, environment precedence, runMain configuration, CLI startup loading, exports, and runtime tests are added.
Compile-cache shim generation
src/compile-cache-shim.ts, src/compile-cache-shim.test.ts, src/index.ts
Shim generation derives package metadata, validates entries and outputs, writes executable dynamic-import shims, and supports multi-bin packages.
CLI commands and build entrypoints
src/cli-main.ts, src/cli.ts, tsdown.config.ts
The CLI is split into a startup shim and main module; generate-shim is added alongside generate-worker.
Documentation and release metadata
README.md, docs/*, .changeset/fast-cli-startup.md
Compile-cache APIs, configuration, shim generation, recipes, and patch-release metadata are documented.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: copilot

Poem

A rabbit hops where cache files gleam,
Warm little starts now chase the dream.
Shims spring up with ears held high,
Imports whisk quickly through the sky.
“Compile away!” the bunny sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling Node.js on-disk compile cache to improve warm startup performance.
✨ 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/cli-start

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/politty@326ea63

commit: 326ea63

This comment was marked as resolved.

- Assert enabled === false instead of failing when
  module.enableCompileCache is unavailable (Node < 22.8) or disabled via
  NODE_DISABLE_COMPILE_CACHE.
- Skip the fresh-process tests on runtimes that cannot import .ts files
  directly (type stripping is default from Node 22.18 / 23.6); they spawn
  plain node against the TypeScript source.
Copilot AI review requested due to automatic review settings July 16, 2026 14:25

This comment was marked as outdated.

Hand-writing the bin shim is the only setup step the compile cache
feature requires; make it generatable instead. politty generate-shim
(backed by the exported generateCompileCacheShim) writes the executable
ESM shim at build time, so it can be wired into a postbuild or prepack
script and never has to live in source:

  "postbuild": "politty generate-shim --entry ./cli.js --out dist/bin.js"

The program name for the cache directory defaults to the first bin name
in package.json (falling back to the unscoped package name). Guards:
.cjs output and .js output in non-"type": "module" packages are
rejected with a pointer to .mjs, since the shim is an ES module.
Copilot AI review requested due to automatic review settings July 16, 2026 14:31

This comment was marked as resolved.

readNearestPackageJson now searches parent directories up to the
filesystem root, matching the documented behavior, so the default
program-name derivation and the type-module check keep working when the
command runs from a nested directory. A malformed package.json now
propagates as an error instead of silently falling through.
Copilot AI review requested due to automatic review settings July 16, 2026 14:40
compileCacheDir treated programName as a raw path segment, so a name
containing separators or dot segments (e.g. a scoped package name passed
as command.name) could place the cache outside the intended root.
Collapse separators and the scope marker into one segment
(@scope/cli -> scope-cli) and fall back to Node's default cache location
when nothing safe remains. Realistic command names are unchanged.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 16, 2026 14:44

This comment was marked as off-topic.

toiroakr added 2 commits July 17, 2026 00:12
TypeScript CLIs conventionally build into dist, so a bare
politty generate-shim in postbuild is now enough:

- --out defaults to the first bin path in the nearest package.json (the
  place the executable must live), resolved against the package.json
  directory.
- --entry defaults to the first of ./cli.js, ./cli.mjs, ./index.js,
  ./index.mjs that exists next to the shim (never the shim itself).
- Guardrails: refuse to overwrite an existing file the generator did not
  write (a bin still pointing at the real CLI entry fails loudly instead
  of clobbering the build output), and reject a shim that would import
  itself.
The compile cache feature was only documented in recipes.md and
api-reference.md; the package front door never mentioned it. Add a
Features bullet and a Faster Startup section (with the generate-shim
postbuild setup) to the README, and point the getting-started Next Steps
and doc-index recipe blurbs at it.
Copilot AI review requested due to automatic review settings July 16, 2026 15:12

This comment was marked as resolved.

toiroakr added 2 commits July 17, 2026 00:22
generate-shim now generates one shim per bin entry: --entry is
repeatable and pairs in declaration order with the package's bin entries
(or with --out paths of the same count; counts must match). Each shim's
program name defaults to its own bin name, so multi-bin packages get
correctly keyed cache directories. All shims are validated before any is
written, so a failure cannot leave a partially generated set.

Also tighten the overwrite-guard marker to a shim-specific string:
completion scripts also say "Generated by politty" and must never be
treated as overwritable.
A fixed tmpdir path could collide across concurrent vitest runs (watch
mode + CI on the same machine) and couple cache state across runs; key
the directory by process.pid.
Copilot AI review requested due to automatic review settings July 16, 2026 15:23
Spawn a fresh node process that never calls enableCompileCache with
NODE_COMPILE_CACHE set, and assert the cache directory gets populated —
verifying the docs claim that Node honors the variable at process
startup, so it applies even with compileCache: false.
Copilot AI review requested due to automatic review settings July 17, 2026 04:38
@toiroakr
toiroakr enabled auto-merge July 17, 2026 04:40

This comment was marked as off-topic.

@toiroakr

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Deriving a shim's program name from something other than its own bin
entry no longer happens silently: with multiple explicit --out paths,
every unmatched shim would share one name (and one cache directory).
Generation still proceeds at the specified paths; a warning names the
fallback and points at --program. Also align the remaining cache-path
JSDoc templates (CompileCacheOptions.programName, MainOptions.compileCache)
with the sanitized-name behavior.
Copilot AI review requested due to automatic review settings July 17, 2026 09:58

This comment was marked as resolved.

The dispatcher scripts use the raw program name while compileCacheDir
sanitizes it first; state in the JSDoc that a completion program name is
a single shell word resolved on PATH and can never contain separators,
so the two paths coincide for every name that can reach both sides.
Copilot AI review requested due to automatic review settings July 17, 2026 10:34

This comment was marked as resolved.

The self-import guard compared resolve(dirname(out), entry) with the
output path, which never matches a file: URL entry — an allowed form —
so a shim could be generated that imports itself. Normalize file:
entries with fileURLToPath before comparing (unparsable URLs skip the
best-effort check).
Copilot AI review requested due to automatic review settings July 17, 2026 10:42

This comment was marked as resolved.

Functionally identical to the previous default import (Node builtins
expose module.exports either way), but the namespace form still links on
Node < 22.8 where enableCompileCache is missing — a named import would
fail before the runtime feature check — and stops automated reviewers
from repeatedly flagging the default-import form. Comment the intent so
it is not "simplified" into a named import later.
Copilot AI review requested due to automatic review settings July 17, 2026 10:49

This comment was marked as off-topic.

The generated shim statically imported politty/compile-cache, so a CLI
that bundles politty into its build (tsdown noExternal, single-file
CLIs) crashed with ERR_MODULE_NOT_FOUND at startup — the optimization
became a single point of failure while the real entry ran fine on its
own. Load the helper with a dynamic import inside try/catch instead:
when politty is not resolvable next to the shim, the CLI starts without
the cache. Also document the mismatch warning and this fallback in
recipes.md.
Copilot AI review requested due to automatic review settings July 18, 2026 00:03
@toiroakr
toiroakr merged commit c057e6a into main Jul 18, 2026
14 checks passed
@toiroakr
toiroakr deleted the perf/cli-start branch July 18, 2026 00:05

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

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