Skip to content

refactor: import Node built-ins with the node: prefix - #657

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
eric-engberg:refactor/node-protocol-imports
Oct 6, 2026
Merged

sirmalloc merged 2 commits into
sirmalloc:mainfrom
eric-engberg:refactor/node-protocol-imports

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Every import of a Node.js built-in module now uses the node: prefix: 'fs' → 'node:fs', 'child_process' → 'node:child_process', and likewise for path, os, crypto, https, module, url, util, events, tty and string_decoder. That covers 84 files (30 runtime/script files, 54 test files) and 203 lines. Nothing visible changes and there's no behavior change.

Why

main mixes both styles. Seven runtime files (custom-command.ts, git.ts, git-review-cache.ts, jsonl-blocks.ts, jsonl-cache.ts, jsonl-metrics.ts, CurrentWorkingDir.tsx) and 19 test files already import with node:, and two more tests use it in require(). Meanwhile 30 runtime/script files and 54 test files use bare ids, and 13 files mix both styles in the same import block (for example, git.ts imports 'child_process' next to 'node:fs').

The prefix says plainly that a module is Node's own and can't resolve to an npm package of the same name. Using one style also ends the per-file choice.

The published bundle already contains node: imports, because Bun keeps them as written in dist/. So the minimum Node version the package needs doesn't change (details under How).

How

Every form a module id takes was converted:

  • static import / export ... from, including import type
  • dynamic import() (ccstatusline.ts, powerline.ts)
  • require(): the script the Custom Command widget passes to a child runtime with -e, and the helper scripts tests write to disk and run
  • vi.mock() ids in tests

ESLint's import-x/order keeps builtins and packages in one alphabetized group, so its auto-fix moved five imports (https-proxy-agent now sorts before node:*, and node:crypto before node:fs). No other edits.

Mocks. The only built-in mocked by id is child_process, in 30 test files, and all 30 now use vi.mock('node:child_process', ...), the same id the code under test imports. I checked under both Bun's test runner and Vitest on Node that a mock registered under either id applies to static and dynamic imports of both 'child_process' and 'node:child_process'. Neither runner applies mocks to createRequire() calls, so the three tests that use require('node:child_process') to reach the real execFileSync past the mock still get the real one. vi.spyOn on a module namespace sees the same object for 'os' and 'node:os' in both runners. As a control, I pointed each of the 30 mocks at an unused built-in (node:dgram), and 29 files then failed when run alone, so their mocks are in effect. The other one, CompactionCounter.test.ts, passes either way on main too; its mock is a guard those tests never reach.

Minimum Node version. The Custom Command child script now does require('node:child_process'), and require() accepts node: only from Node 16.0 (and 14.18). main's bundle already needs Node 16.6, because its entry chunk calls Array.prototype.at at module load (in get-east-asian-width). Official Node 16.5.0 fails to load both main and this branch with the same ambiguousRanges.at is not a function. Node 16.6.0 renders a Custom Command line identically on both.

Demo

No demo: nothing visible changes.

Testing

  • bun run lint: passes.
  • bun test (Bun 1.4.2): 2358 pass, 0 fail, 5210 expect() calls across 148 files, the same totals as main. The JUnit reports match main test by test, assertion counts included.
  • Suite under Node (Node 26.10.0, node node_modules/vitest/vitest.mjs run --reporter=json): this branch has 2336 tests, 2185 passed, 151 failed in 17 files. A clean main gives the same 151 failures in 17 files, matching test by test; the failures come from spying on Node built-ins, which Node forbids. The only difference: two files that fail at load on both quote the mock id in their error message (No "spawn" export is defined on the "node:child_process" mock). A first main run also failed 5 timing-sensitive TUI/process tests that passed on rerun; this branch had none of them.
  • Built CLI under both runtimes (runtime-check.sh refactor/node-protocol-imports, which builds this branch and main):
    Piped render (2 runtimes x plain/powerline):
      plain: all runtimes agree, same as main
      powerline: all runtimes agree, same as main
    TUI smoke (main menu opens, ctrl+c exits cleanly):
      ok    bun-1.4.2
      ok    node-v26.10.0
    
    A rerun that added Node 16.6.0 to the piped renders also reports "all runtimes agree, same as main" for both modes. A Custom Command line (echo hello-from-cmd) renders byte-for-byte the same on main and this branch under Bun 1.4.2, Node 26.10.0 and Node 16.6.0.
  • No bare built-in id remains in tracked TS/JS files. Searching every built-in name from require('module').builtinModules, in any from / import / import() / require() / vi.mock() position, matches nothing. Searching for any quoted built-in name at all finds only the 'url' editor-mode literal in src/widgets/Link.tsx, which isn't a module id.

eric-engberg and others added 2 commits October 4, 2026 22:39
The codebase mixed two styles for Node's own modules: a handful of files
imported 'node:fs', 'node:os' and friends while the rest used bare ids
such as 'fs' and 'child_process'. Use the node: prefix everywhere, so
each import says plainly that it is Node's module and cannot resolve to
an npm package of the same name.

This covers static imports and re-exports, type-only imports, dynamic
import(), require() (including the script the Custom Command widget
hands to a child runtime and the scripts tests write to disk), and
vi.mock() ids in tests. Vitest and Bun resolve 'child_process' and
'node:child_process' to the same module, for both mocks and spies, so
every mock still applies to the code under test. ESLint's import order
moved five imports to keep builtins sorted.

No behavior change. The bundle already loads node: imports, and every
Node version that can load the bundle also supports require('node:...').
@sirmalloc

Copy link
Copy Markdown
Owner

Restored the two unprefixed require() calls in the custom-command subprocess. That script runs verbatim through node -e, bypassing the bundler, and Node 14 before 14.18 doesn’t support require('node:…'). This avoids adding another compatibility limitation while we still declare Node 14+ support.

We plan to raise the minimum to at least Node 20, alongside a custom launcher that shows an update message in the status line for users running older versions.

All other imports retain the node: prefix. Lint, build, and all 2,469 tests pass on the installed runtimes.

@sirmalloc
sirmalloc merged commit 9ab118e into sirmalloc:main Oct 6, 2026
3 checks passed
@eric-engberg
eric-engberg deleted the refactor/node-protocol-imports branch October 9, 2026 21:47
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 11, 2026
Upstream: label prefixes editable with e (sirmalloc#601, sirmalloc#666), Session Cost Rate (sirmalloc#671) and Extra Usage Daily Budget (sirmalloc#672) widgets, own-lock-only usage lock clearing (sirmalloc#624), NO_PROXY and request deadlines, terminal control-sequence sanitizing, git hardening and linear-time parsing, shared searchable option editor (sirmalloc#649) and widget base classes (sirmalloc#646-sirmalloc#654), node: imports (sirmalloc#657), Number.* globals lint (sirmalloc#645), test waits on redraw instead of fixed delays (sirmalloc#652).

Hand edits outside conflicts:
- src/widgets/shared/searchable-option-editor.tsx: compat fix - ports the fork's ref-based input handling (Enter picks from what was just typed) into upstream's new shared editor
- src/utils/usage-fetch.ts: compat fix - fork's fetchFreshUsageData uses upstream's clearOwnUsageLock (sirmalloc#624) instead of the removed clearUsageLock
- src/widgets/ApiUsage.tsx, src/widgets/Battery.ts: compat fix - Number.isNaN/Number.parseInt for upstream's no-restricted-globals rule
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - mock renamed to upstream's mockFetchUsage
- src/tui/components/__tests__/PowerlineSetup.test.ts, src/tui/components/__tests__/StatusLinePreview.test.ts, src/tui/components/__tests__/TerminalWidthMenu.test.ts: compat fix - drop the fork's flushInk import, unused after upstream's waitFor switch
- README.md: upstream notes linked docs/USAGE.md (deleted in the fork); inlined the proxy and workspace-trust text instead; upstream's own release-note entries not taken
- .fork-keep-deleted: src/widgets/FableWeeklyUsage.ts (upstream widget, unregistered in the fork, which ships WeeklyFableUsage.ts)
- src/widgets/WeeklyFableUsage.ts: compat fix - adopts upstream's shared getUsagePercentWidgetEditorDisplay, which carries the time-cursor modifier
- src/tui/__tests__/App.test.ts: fork deviation - one extra down-arrow for the fork's Off Hours main-menu row
- src/widgets/__tests__/CacheTimer.test.ts, src/widgets/CacheTimer.ts: compat fix - emoji toggle moved from e to o because upstream reserves e for edit-label
- src/widgets/AgentAddress.ts: split the session-title guard into its own if (same behaviour) so the .fork-invariants nameSource guard matches again
- docs/test-retirements.md: ledger for tests upstream renamed or replaced
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