feat: first-class Bun compatibility (core, timer, logger-console, loader, include) - #1
Open
ebowwa wants to merge 33 commits into
Open
feat: first-class Bun compatibility (core, timer, logger-console, loader, include)#1ebowwa wants to merge 33 commits into
ebowwa wants to merge 33 commits into
Conversation
added 19 commits
August 16, 2026 05:08
- tests/bun/: 57 bun:test specs covering core plugins/effects/events, provide/inject, isolation, timer, logger-console, loader+include (real config files, dynamic TS import, repeated cycles, patches) and SIGINT shutdown with full root-fiber disposal - packages/loader: declare @cordisjs/plugin-include devDependency so the fallback import resolves under Bun's isolated workspace node_modules (mirrors the existing plugin-logger-console entry; no code change) - docs/: phased plan and session status log Node suite: 19 files / 163 tests passed. Bun suite: 57/57 passed on Bun 1.3.14 (1.3.14+0d9b296af).
…docs - packages/core/bin.bun.js: Bun entrypoint mirroring bin.js with graceful SIGINT/SIGTERM root-fiber disposal - packages/core/bin.bun.watch.js: development supervisor. Measured on Bun 1.3.14: --watch re-evaluates in-process with fresh globalThis/process (old root unreachable, disposers never run) and --hot duplicates live state across reloads. The supervisor restarts a child process instead: SIGTERM -> complete root disposal -> respawn, using public APIs only - tests/bun/watch.spec.ts: asserts dispose-before-activate restart ordering across three generations plus clean SIGINT - tests/bun/repros/: standalone scripts documenting the --watch/--hot findings (manual, excluded from test discovery) - .github/workflows/bun.yml: pinned Bun 1.3.14 job alongside untouched upstream build.yml - package.json scripts: test:bun / start:bun / dev:bun (additive) - docs/BUN_COMPATIBILITY.md: package matrix, verified commands, reload decision, Cordis-vs-Bun change list Phase boundary: bun test tests/bun 58/58; Node suite 19 files / 163 tests.
- bench/: runtime-neutral workloads (core ops, effects, events, inject, isolate, timers, loader tree updates, include boot, fresh TS module eval), cold-start variants (JS vs TS plugins), 600-cycle leak check, fixed-duration profile workload, cpuprofile aggregator, orchestrator - docs/BUN_BENCH.md: full results. Control (fork vs upstream@8cc9e33 in a worktree): statistically indistinguishable under Node, as expected with zero src changes. Node vs Bun: Bun faster on every Cordis op (~5x plugin lifecycle, ~3x timers, ~4x include boot, ~80x TS module eval; timer- settle-bound workloads runtime-neutral). Cold start: core boot parity (~23ms), Node TS penalty is the tsx toolchain. Memory: no leak on either runtime. Profiles: top hotspot is reflect.ts proxy machinery on both. - package.json: devDependencies for cordis/plugin-loader/plugin-timer so root-level bench files resolve workspace packages under Bun's isolated linker (no-op under Yarn hoisting) Phase boundary: bun test tests/bun 58/58; Node suite 19 files/163 tests.
…ild cycle The plugin-include devDependency added to packages/loader created a plugin-loader -> plugin-include -> plugin-loader cycle in yakumo-tsc's build graph (include peer-depends on loader), failing both the yarn build job and the Bun job with 'circular dependency detected'. packages/loader/package.json is now upstream-identical again; the dependency lives in root devDependencies, which resolves via parent walk-up under Bun's isolated linker, is a no-op under Yarn hoisting, and is outside yakumo's package graph. Verified: yakumo esbuild exit 0, yakumo tsc exit 0, bun test tests/bun 58/58, Node suite 163/163.
Focused upstream integration (no Bun fork, no runtime-source changes): - docs: correct the --watch/--hot semantics to match Bun's official documentation and re-measurement on 1.3.14 — --watch is a hard restart (globalThis fresh, no handler accumulation), --hot is the in-process soft reload preserving globalThis (handlers/timers accumulate). The old Phase 5 notes had these attributes swapped; bin.bun.js's globalThis root guard was always the correct --hot strategy. - bin.bun.js: register import.meta.hot.dispose(() => disposePrevious()) when the runtime provides it (PR build) — runtime-awaited disposal before re-evaluation; no-op on stock Bun/Node. globalThis guard kept as defense-in-depth. - tests/bun/hot.spec.ts: 2 PR-build-gated integration tests (skip cleanly when bun-32856 is absent, verified): awaited async disposal completes before reactivation, no resource duplication across 3 generations; broken generation still disposes the old root and recovers on next edit. - CI: install the PR build best-effort (bunx bun-pr 32856) and run the hot suite; download failures leave the tests skipped, real failures fail. - repros: hot-pr-dispose-order.ts, signal-handler-accumulation.ts; README corrected and extended. bun test tests/bun: 60/60 (216 expect) · Node suite: 163/163 · yakumo esbuild+tsc: exit 0. No Bun-source changes needed; nothing to report on PR #32856.
…epro - bin.bun.watch.js header: --watch is a hard restart (no signal-handler accumulation — each generation starts clean); --hot is the in-process soft reload; 'only way' claim scoped to stock Bun now that bun#32856 gives bin.bun.js an in-process disposal path. - watch-timer-disposers.ts: SIGINT comment corrected — exactly one handler (the current generation's) fires under --watch; cross-references signal-handler-accumulation.ts for the --hot contrast. Verified: watch.spec.ts 1/1; hot.spec.ts 2 more runs 0 fail (3 stable runs total); bun.yml parses as valid YAML.
…dit gap Third PR-gated hot test: when generation 2 of a plugin drops its import of a helper module, the helper's import.meta.hot.dispose callback AND the Cordis root-fiber disposal complete strictly before generation 2 activates, and the helper's timer never fires again — verifying the PR's 'disposal for modules no longer imported' claim downstream. Also documents a verified --hot limitation: editing cordis.yml triggers no reload (config files are outside the module graph); the supervisor's fs.watch does catch config edits. This completes Cordis's full case list against the PR build: root-fiber disposal, async cleanup, dynamic plugins, removed plugins, repeated reloads, failed evaluation — all pass. bun test tests/bun: 61/61 (223 expect); hot.spec skips 3/3 without the PR binary.
Owner-approved. Upstream activity remains a comment only — no Bun code, clone, or defect report (nothing failed).
Upgrades the last 🟡 runtime cell of the compatibility matrix: - loads lib/browser.js by path — the file the export map's default condition serves to non-node consumers (Bun's bare-specifier import correctly picks the node condition) - wires via ctx.plugin (exercises the schemastery Config static) - verifies error→console.error, warn→console.warn, else→console.log with the [T] name prefix; argument pass-through BY IDENTITY (the browser contract: the exporter never serializes); log levels; operation with document/window hard-absent - declarative checks: export map routes non-node consumers to browser.js; shipped artifact contains zero node: specifiers Also documents a verified nuance (NOT a Bun defect): bundling bare @cordisjs/* imports from inside this repo resolves via root tsconfig paths → packages/*/src, bypassing export maps, in both Bun.build and esbuild regardless of target. Cross-checked against esbuild before ruling out a bundler bug. bun test tests/bun: 68/68 (248 expect calls)
Our upstream PR (a file:// URL's query is part of the module key) is the missing public-API primitive for per-module reload on Bun — the capability @cordisjs/plugin-hmr gets on Node from --expose-internals internals. - tests/bun/fixtures/selective-reload-driver.ts + selective-reload.spec.ts: capability-gated spec proving the full swap contract — fresh module instance per ?gen=N, old fiber disposed only after the new applies, root effects survive unduplicated, single disposal at shutdown. Probes the runner binary (BUN_QUERY_BUSTING_BIN or a local .upstream/bun debug build); stock Bun skips cleanly. - repros/selective-reload.ts: manual narrative version. - docs: Phase C section rewritten from 'deferred' to 'primitive done'; suite now 69 tests (68 + the gated one); Phase 10 status recorded. Verified: 69/69 with the bun#39426 debug build present; the full Cordis suite also runs green on that build; skip path exercised on stock Bun.
6 tasks
added 10 commits
August 17, 2026 09:59
… + disclosure Our #39426 partially duplicates robobun's earlier #35601 (same diagnosis, broader coverage, also fixes #13391, but stalled/dirty since Jul 26). Disclosed on both PRs; Cordis PR #2 unaffected (capability gating). Lesson: run the is:pr <issue> prior-art search before opening a fix.
…lter, history, adjacent issues) Map: #35601 (our dup, stalled), #35703 (fragments, claimed), #37702 (plugin prefilter vs query dots, adjacent non-conflicting), #16456 (merged Jan 2025 — introduced relative+query), #13391 (astro, covered), #35345 (lcov re-import bug — becomes reachable via file:// queries once either fix lands; cross-reference kept local), #7823 (mock.restore). Verdict: neighborhood fully occupied by the Bun team's in-flight work; no uncontested target remains. Our plays: fresh-on-main #39426, downstream validation, Cordis PR #2 capability gating.
build/release/bun (the ~60-90MB usable binary) checked before the debug build; env override unchanged.
…dling - build:release from the PR branch → 1.4.0-canary.1+c16333e9e; preserved at .upstream/bin/bun-39426 (59 MB, git-excluded); build dir reclaimed - verified: selective-reload spec + full 69/69 suite against it; real-app smoke (bin.bun.js boots, loads plugin, SIGINT root disposal, exit 0) - spec: resolve() the BUN_QUERY_BUSTING_BIN override — the driver child runs from a temp cwd, so a relative path silently broke spawn (caught during release verification) - docs: 'Running Cordis on the contributed Bun today' usage section; Phase 10 binary record incl. prior-art note re #35601
Same-day, two-pass re-run of the workload suite across fork-node, stock Bun 1.3.14, and bun-39426 (release build of our oven-sh/bun#39426) + cold/leak on the contributed binary: - bench/compare.mjs: regenerates the matrix from results/<label>.json - bench/workloads.mjs: new 'selective-reload' workload (one full generation swap: query-busted fresh TS import + activate + graceful previous-fiber dispose) — the Phase C primitive as a metric - docs/BUN_BENCH.md §6: the matrix, the degenerate-row markers, and the retraction — module-eval-ts on stock Bun measured 1 eval + 149 cache HITS (#21346), so the old ~80x was benchmarking the bug; honest ratio ~5x (verified a===b on distinct queries: stock true, fixed/node false) - §6.2: real selective-reload cost on bun-39426 = 390-415 us/swap, ~4.1x faster than Node's honest equivalent - §6.3: canary >= stock on all 15 honest workloads; leak parity - BUN_COMPATIBILITY.md header updated to match (no more ~80x claim)
The clone is a standalone repo, not part of Cordis — it now sits at ~/Developer/bun alongside this repo. References updated: spec auto-detect (sibling checkout first), repro commands, both docs. Capability chain now also falls back to the preserved .upstream/bin/bun-39426 release binary, so the selective-reload test activates with no env var on this machine. Verified: spec passes via the preserved-binary fallback.
added 4 commits
August 17, 2026 12:24
…reserved binary → skip)
…01 with co-author credit
…d for new work - detection chain flipped: preserved .upstream/bin/bun-39426 now wins over sibling-checkout builds, so Cordis work stays on the verified release build even when ~/Developer/bun contains a build of another in-flight PR (BUN_QUERY_BUSTING_BIN overrides for explicit testing) - ~/Developer/bun: on fresh origin/main (8bc4d2a8), closed-PR branch claude/file-url-query-module-keys preserved at c16333e9, worktree clean — the launchpad for the next claude/<topic> branch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes this fork a first-class Bun-compatible Cordis implementation while preserving the Node implementation, tests, and a clean merge path against
cordiverse/cordis.Verified under Bun 1.3.14 (
1.3.14+0d9b296af):cordiscore,@cordisjs/plugin-timer,@cordisjs/plugin-logger-console(node and browser exports),@cordisjs/plugin-loader,@cordisjs/plugin-include.@cordisjs/plugin-hmrremains Node-only by design (requires Node ESM internals).Full details in
docs/BUN_COMPATIBILITY.md,docs/BUN_PORT_PLAN.md,docs/BUN_PORT_STATUS.md, anddocs/BUN_BENCH.md.What's included
1. Bun behavioral suite —
tests/bun/(68 tests, 13 spec files)58 core + 7 browser-export tests run on stock Bun; 3 are gated on the oven-sh/bun#32856 PR build and skip cleanly when it isn't installed (verified). Covers every required scenario: function/object/class plugins; sync & async effects; reverse disposal order; nested plugin cleanup; provide/inject (activation, removal+reactivation, replacement); isolated contexts; all five dispatch modes; timer cleanup; JSON & YAML config loading; dynamic TypeScript plugin loading; repeated load/dispose cycles with no duplicated listeners/timers; process shutdown with complete root-fiber disposal; supervisor restart ordering; the browser export of logger-console (method routing, identity pass-through, no DOM needed, zero
node:specifiers); and (on the PR build) awaited disposal before--hotreactivation with no resource duplication.2. Bun CLI & development supervisor
packages/core/bin.bun.js— Bun entrypoint mirroringbin.js, with graceful SIGINT/SIGTERM root-fiber disposal. Stores the root context onglobalThisand disposes the previous root before booting a new one (the correct strategy for--hot's in-process re-evaluation; a no-op under--watch, whereglobalThisis reset per reload). When the runtime providesimport.meta.hot, disposal is additionally driven by the runtime's awaited dispose phase — see item 3.packages/core/bin.bun.watch.js— dev reload supervisor. Measured on Bun 1.3.14, per Bun's watch-mode docs:--watchis a hard restart (globalThisfresh per reload — old root unreachable, Cordis disposers never run; no public before-reload hook) and--hotis the in-process soft reload preservingglobalThis, where live state duplicates across reloads on stock Bun. The supervisor restarts a child process instead: SIGTERM → complete root disposal → respawn, using public APIs only. Restart ordering (dispose before activate) is asserted by tests.3. Upstream integration — oven-sh/bun#32856 (
import.meta.hotforbun --hot)This fork consumes the PR as a downstream integration fixture — no Bun fork or source changes:
bunx bun-pr 32856(Aug 13, 2026 CI artifact)bin.bun.jsregistersimport.meta.hot.dispose(() => disposePrevious())when available (no-op on stock Bun/Node)tests/bun/hot.spec.tsproves: an async disposer (150 ms) completes before the next generation activates, timers never duplicate across 3 generations, a module the next generation no longer imports is disposed and its resources stop (the PR's removed-modules claim), a broken generation still disposes the old root and recovers on the next valid edit, and SIGINT exits 0--hotas the supported in-process dev reload--hotlimitation (verified, documented): editingcordis.ymltriggers no reload — config files are outside the module graph; the supervisor'sfs.watchdoes catch config edits4. CI matrix —
.github/workflows/bun.ymlPinned Bun 1.3.14 job (bun install → build →
bun test tests/bun→ Node suite), plus a best-effortbunx bun-pr 32856install step and the bun#32856 integration suite (download failures leave those tests skipped; real failures fail CI) — alongside the untouched upstreambuild.yml.5. Benchmarks & profiles —
bench/+docs/BUN_BENCH.mdcordiverse/cordis@8cc9e33in a worktree, run identically): statistically indistinguishable under Node — consistent with zero runtime-source changesreflect.tsproxy machinery on both runtimesChanges vs upstream
Additive only — 39 files, 3711 insertions, 0 deletions, zero changes to any
packages/*/src:package.json: +cordis/@cordisjs/plugin-loader/@cordisjs/plugin-timer/@cordisjs/plugin-includedevDeps (resolution under Bun's isolated workspacenode_modulesfor bench and the loader's fallback import; no-op under Yarn hoisting) and +3 additive scripts (test:bun,start:bun,dev:bun)packages/loader/package.jsonstays upstream-identical — declaringplugin-includethere created aplugin-loader → plugin-include → plugin-loadercycle in yakumo-tsc's build graph, so the devDep lives at the root insteadtests/bun/**,bench/,docs/BUN_*.md,packages/core/bin.bun*.js,.github/workflows/bun.ymlNo public Cordis semantics were changed. No Bun internals were accessed. No Bun-side defects were found (the two candidate findings reproduced identically on Node — documented as parity facts — and the stock-Bun
--hotcleanup gap is being fixed upstream by bun#32856, which passed all Cordis cases here).Test plan
bun test tests/bun— 68/68 pass (248 expect calls)HOME=/tmp/... bun test tests/bun/hot.spec.ts— 0 pass / 3 skip / 0 fail (clean skip without the PR build)tests/bun/hot.spec.tsstable across 3 consecutive runsbun installat root — 864 packages