From cc7a487aca03dd2836bf8bc91d5f338fecdfc3bc Mon Sep 17 00:00:00 2001 From: nokeo08 Date: Wed, 17 Jun 2026 21:57:42 -0500 Subject: [PATCH] Lazy-import keeper.ts so --help and --version skip nut.js load keeper.ts statically imports @nut-tree-fork/nut-js, which dlopens a sizeable native .node binary. That load dominated cold-start: ~1.2s for 'move --version' immediately after install, vs ~125ms warm. For a flag that just prints a string, that's all overhead. Change: dynamic 'await import("./keeper.ts")' placed after the --help and --version short-circuits. The dynamic import is wrapped in try/catch so import-time failures (e.g., missing native binary, unsupported architecture) route through the same failRuntime() path as anything thrown by the loop. Expected cold start for --help and --version drops to ~50-150ms (Bun + reading a handful of small files, no native module load). move with no flags pays the same load cost as today. tsc --noEmit and the test suite remain clean. --- src/move.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/move.ts b/src/move.ts index 39ef3fd..d9800a0 100755 --- a/src/move.ts +++ b/src/move.ts @@ -12,13 +12,17 @@ * * Order of operations: * 1. Parse CLI args. Bad input -> stderr + usage hint, exit 2. - * 2. `--help` / `--version` short-circuit before any I/O or mouse work. + * 2. `--help` / `--version` short-circuit before any I/O, config load, or + * mouse work. `keeper.ts` is also lazy-imported (see below) so these + * flags don't pay the cost of loading the nut.js native binary. * 3. Load + validate the config file (default XDG path, or `--config * ` if supplied). Validation failures share the exit-2 path. * 4. Resolve the full `Config` (CLI > file > DEFAULT_CONFIG) — verbose * lives inside `Config` and is layered with the same precedence as * the numeric fields. - * 5. Run keeper. Any unhandled rejection exits 1. + * 5. Lazy-import `keeper.ts` (dynamic import keeps nut.js out of the + * `--help` / `--version` startup path) and run it. Any unhandled + * rejection — from the import itself or from the loop — exits 1. * * Runtime: Bun (uses `@nut-tree-fork/nut-js` via `keeper.ts`). The shebang * above lets this file run as a real CLI once linked via `bun link`. @@ -29,7 +33,13 @@ import type { ParsedCliArgs } from "./cli.ts"; import { resolveConfig } from "./config.ts"; import type { ConfigOverrides } from "./config.ts"; import { loadConfigFile } from "./configFile.ts"; -import { runKeeper } from "./keeper.ts"; + +// `keeper.ts` is intentionally NOT statically imported here. It transitively +// pulls in `@nut-tree-fork/nut-js`, which in turn dlopens a sizeable native +// `.node` binary. On a cold first run that load dominates startup (~1 s on +// macOS). For `--help` and `--version` we never actually need nut.js, so we +// defer the import to the only branch that actually runs the keeper loop. +// See the dynamic `await import("./keeper.ts")` near the bottom of the file. /** * Print a user-error message and exit 2. Used for anything that comes from @@ -98,4 +108,14 @@ const cliOverrides: ConfigOverrides = { const config = resolveConfig(fileOverrides, cliOverrides); -runKeeper(config).catch(failRuntime); +// Dynamic import so nut.js and the rest of the keeper machinery aren't +// loaded for invocations that exit early (--help, --version, validation +// failures). The `try` covers import-time failures too (e.g., a missing +// nut.js native binary), routing them through the same runtime-failure +// path as anything raised by the loop itself. +try { + const { runKeeper } = await import("./keeper.ts"); + runKeeper(config).catch(failRuntime); +} catch (err: unknown) { + failRuntime(err); +}