Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
06ae971
feat(bun): add Bun behavioral test suite and port plan
Aug 16, 2026
a8499bd
feat(bun): CLI entrypoints, dev supervisor, CI matrix, compatibility …
Aug 16, 2026
e3cc3f9
docs(bun): record phase 4-5 commit SHAs and final status
Aug 16, 2026
f98c663
bench(bun): performance, memory and CPU profile comparison vs upstream
Aug 16, 2026
7602bab
docs(bun): record phase 6 commit SHA
Aug 16, 2026
10b2df7
fix(ci): move plugin-include devDependency to root to break yakumo bu…
Aug 16, 2026
fdfe2e9
feat(bun): integrate oven-sh/bun#32856 import.meta.hot for --hot reload
Aug 16, 2026
a46f378
docs(bun): record phase 7 commit SHA
Aug 16, 2026
c678d87
docs(bun): finish --watch/--hot correction in supervisor header and r…
Aug 17, 2026
76f4668
docs(bun): record c678d87 SHA
Aug 17, 2026
770134d
test(bun): cover bun#32856 removed-module disposal; document config-e…
Aug 17, 2026
39bac40
docs(bun): record 770134d SHA
Aug 17, 2026
2c07253
docs(bun): record downstream-validation comment on oven-sh/bun#32856
Aug 17, 2026
565fa27
test(bun): verify logger-console browser export under Bun (7 tests)
Aug 17, 2026
af33753
docs(bun): record 565fa27 SHA
Aug 17, 2026
1a67771
docs(bun): record phase 9 — upstream code contribution oven-sh/bun#39426
Aug 17, 2026
45bb35e
docs(bun): record #39426 review follow-up (resolveSync coverage)
Aug 17, 2026
5c43ad9
docs(bun): record #39426 CI state — head build blocked on fork approv…
Aug 17, 2026
c28a360
feat(bun): unlock Phase C selective plugin reload via bun#39426
Aug 17, 2026
5d2268b
docs(bun): record downstream-usage comment on oven-sh/bun#39426 and P…
Aug 17, 2026
199f1f2
docs(bun): record CodeRabbit resolution on oven-sh/bun#39426
Aug 17, 2026
71aecfc
docs(bun): correct CodeRabbit record — auto-update runs, merge risk n…
Aug 17, 2026
b6a8bdb
docs(bun): record prior-art sweep finding oven-sh/bun#35601 duplicate…
Aug 17, 2026
ffdd54f
docs(bun): record extended neighborhood sweep (fragment, plugin-prefi…
Aug 17, 2026
6dfa978
test(bun): prefer release bun build in selective-reload capability probe
Aug 17, 2026
9335557
docs(bun): record neighborhood-map comments posted on #39426 and #35601
Aug 17, 2026
660fe6f
feat(bun): ship contributed-Bun release binary + harden spec path han…
Aug 17, 2026
6d1d9cb
bench(bun): four-configuration matrix + retract tainted ~80x eval claim
Aug 17, 2026
da8bb25
chore(bun): relocate Bun clone to ~/Developer/bun (sibling of cordis)
Aug 17, 2026
5f3a4b2
docs(bun): complete detection-chain description (sibling checkout → p…
Aug 17, 2026
9dd2cc8
docs(bun): record resolution — #39426 closed, tests carried into #356…
Aug 17, 2026
0ef23df
docs(bun): record #35601 validation delivery — 69/69 on rebased head,…
Aug 17, 2026
62f904a
chore(bun): pin Cordis to the preserved bun-39426 build; clone preppe…
Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/bun.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Bun

# Bun-native verification alongside the existing Node toolchain (build.yml is
# intentionally untouched to preserve the upstream merge path).
#
# Pinned Bun: 1.3.14 (revision 1.3.14+0d9b296af) — the release this port is
# verified against. Update the pin only together with docs/BUN_COMPATIBILITY.md.

on:
push:
branches: [main, feat/bun-compat]
pull_request:

jobs:
test:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
bun-version: ['1.3.14']

steps:
- name: Check out
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.bun-version }}

- name: Install (bun)
run: bun install

- name: Build (node + yakumo)
run: |-
node --expose-internals --import tsx --import @cordisjs/unyaml \
node_modules/yakumo/lib/cli.js esbuild
node --expose-internals --import tsx --import @cordisjs/unyaml \
node_modules/yakumo/lib/cli.js tsc

- name: Bun behavioral suite
run: bun test tests/bun

# Downstream integration fixture for oven-sh/bun#32856
# (import.meta.hot for bun --hot). Install is best-effort: PR artifact
# availability varies, and tests/bun/hot.spec.ts skips cleanly when the
# binary is absent — but a real failure in it fails CI.
- name: Install bun#32856 PR build (best effort)
run: bunx bun-pr 32856
continue-on-error: true

- name: bun#32856 integration suite
run: bun test tests/bun/hot.spec.ts

- name: Node suite stays green alongside
run: |-
node --expose-internals --import tsx --import @cordisjs/unyaml \
node_modules/yakumo/lib/cli.js vitest --import tsx
22 changes: 22 additions & 0 deletions bench/cold-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// cold-start app: Loader + Include + 3 TypeScript plugins from config dir
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'

const dir = process.argv[2]
const ctx = new Context()
ctx.baseUrl = new URL('.', pathFromDir(dir)).href

const fiber = await ctx.plugin(Loader)
await ctx.loader.create({
name: '@cordisjs/plugin-include',
config: { path: './cordis.yml', enableLogs: false },
})
await ctx.loader.await()
console.log('READY')
await new Promise(r => setTimeout(r, 5))
await fiber.dispose()
process.exit(0)

function pathFromDir(d) {
return 'file://' + (d.endsWith('/') ? d : d + '/')
}
8 changes: 8 additions & 0 deletions bench/cold-min.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// cold-start app: minimal (Context + 3 plugins)
import { Context } from 'cordis'
const ctx = new Context()
const p = c => { c.on('x', () => {}) }
await ctx.plugin(p); await ctx.plugin(p); await ctx.plugin(p)
console.log('READY')
await new Promise(r => setTimeout(r, 5))
process.exit(0)
54 changes: 54 additions & 0 deletions bench/compare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/**
* Compare bench passes across runtimes into a multi-config table.
*
* Reads bench/results/<label>.json (same-day r1/r2 passes per config) and
* emits a markdown table of best/worst us/op per workload per config with
* node/bun ratios. Feeds docs/BUN_BENCH.md (multi-config matrix section).
*
* node bench/compare.mjs
*/
import { readFileSync, existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const RESULTS = join(dirname(fileURLToPath(import.meta.url)), 'results')

const CONFIGS = [
['fork node', ['fork-node-r1', 'fork-node-r2']],
['bun 1.3.14', ['bun-r1', 'bun-r2']],
['bun-39426', ['bun39426-r1', 'bun39426-r2']],
]

function load(label) {
const p = join(RESULTS, `${label}.json`)
if (!existsSync(p)) {
throw new Error(`missing ${p}`)
}
const out = {}
for (const w of JSON.parse(readFileSync(p, 'utf8'))) out[w.name] = w.usPerOp
return out
}

const data = {}
for (const [name, labels] of CONFIGS) data[name] = labels.map(load)

const names = [...new Set(CONFIGS.flatMap(([name]) => Object.keys(data[name][0])))]

console.log('| workload | ' + CONFIGS.map(([c]) => `${c} (best/worst)`).join(' | ') + ' | node/bun | node/bun-39426 |')
console.log('| --- | ' + CONFIGS.map(() => '---').join(' | ') + ' | --- | --- |')
for (const n of names) {
const cells = []
const vals = {}
for (const [cfg] of CONFIGS) {
const passes = data[cfg].map(d => d[n]).filter(v => v !== undefined)
if (!passes.length) {
cells.push('-')
} else {
cells.push(`${Math.round(Math.min(...passes))} / ${Math.round(Math.max(...passes))}`)
vals[cfg] = Math.min(...passes)
}
}
const r = (a, b) => a && b ? `${(a / b).toFixed(1)}x` : '-'
console.log(`| ${n} | ${cells.join(' | ')} | ${r(vals['fork node'], vals['bun 1.3.14'])} | ${r(vals['fork node'], vals['bun-39426'])} |`)
}
61 changes: 61 additions & 0 deletions bench/leak.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Memory-leak check: many plugin load/dispose cycles, then verify the event
* hook registry and plugin registry return to empty and measure rss growth.
*/
import { Context } from 'cordis'

const CYCLES = 600

function gc() {
if (typeof Bun !== 'undefined') Bun.gc(false)
else if (globalThis.gc) globalThis.gc()
}

function snapshot(label) {
gc(); gc()
const m = process.memoryUsage()
return { label, rss: m.rss, heapUsed: m.heapUsed ?? 0 }
}

const root = new Context()
const plugin = ctx => {
ctx.on('bench-event', () => {})
ctx.on('bench-event', () => {})
ctx.effect(() => {
const t = setInterval(() => {}, 1 << 28)
return () => clearInterval(t)
})
ctx.effect(() => () => {})
}

// warmup
for (let i = 0; i < 50; i++) {
const f = await root.plugin(plugin)
await f.dispose()
}
await sleep(5)

const before = snapshot('before')
for (let i = 0; i < CYCLES; i++) {
const f = await root.plugin(plugin)
await f.dispose()
}
await sleep(5)
const after = snapshot('after')

const hooks = Object.entries(root.events._hooks)
.filter(([, v]) => v.length)
const report = {
runtime: typeof Bun !== 'undefined' ? 'bun ' + Bun.version : 'node ' + process.version,
cycles: CYCLES,
rssBefore: before.rss,
rssAfter: after.rss,
rssGrowthMB: +((after.rss - before.rss) / 1048576).toFixed(2),
heapGrowthMB: +((after.heapUsed - before.heapUsed) / 1048576).toFixed(2),
registryEmpty: root.registry.size === 0,
leftoverHooks: hooks.map(([k, v]) => `${k}:${v.length}`),
}

console.log('@@LEAK ' + JSON.stringify(report))

function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
10 changes: 10 additions & 0 deletions bench/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { runWorkloads } from './workloads.mjs'

const filter = process.argv.includes('--filter')
? process.argv[process.argv.indexOf('--filter') + 1]
: ''

process.stderr.write(`[bench] runtime=${typeof Bun !== 'undefined' ? 'bun ' + Bun.version : 'node ' + process.version}\n`)
const results = await runWorkloads(filter)
// machine-readable line for the orchestrator
console.log('@@RESULTS ' + JSON.stringify(results))
35 changes: 35 additions & 0 deletions bench/parse-profile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Aggregate a Chrome .cpuprofile by self time (hitCount) and print the top N.
* Works for both node --cpu-prof and bun --cpu-prof output.
*/
import { readFileSync } from 'node:fs'
import { basename } from 'node:path'

const file = process.argv[2]
const top = Number(process.argv[3] ?? 15)

const profile = JSON.parse(readFileSync(file, 'utf8'))
const nodes = profile.nodes ?? []

let totalHits = 0
const byKey = new Map()
for (const node of nodes) {
const hits = node.hitCount ?? 0
if (!hits) continue
totalHits += hits
const cf = node.callFrame ?? {}
const fn = cf.functionName || '(anonymous)'
const url = cf.url ? basename(new URL(cf.url, 'file://').pathname) : '(native)'
const key = `${fn} @ ${url}`
byKey.set(key, (byKey.get(key) ?? 0) + hits)
}

const entries = [...byKey.entries()].sort((a, b) => b[1] - a[1]).slice(0, top)
const duration = (profile.timeDeltas ?? []).reduce((a, b) => a + b, 0) / 1000

console.log(`profile: ${basename(file)}`)
console.log(`samples: ${totalHits} hits, ~${duration.toFixed(0)}ms sampled`)
for (const [key, hits] of entries) {
const pct = ((hits / totalHits) * 100).toFixed(1).padStart(5)
console.log(` ${pct}% ${hits.toString().padStart(7)} ${key}`)
}
46 changes: 46 additions & 0 deletions bench/profile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Fixed-duration mixed workload for CPU profiling
* (`node --cpu-prof` / `bun --cpu-prof`). Runs ~2s of realistic Cordis work.
*/
import { Context } from 'cordis'

const DURATION_MS = 2000
const start = performance.now()

const emitRoot = new Context()
for (let i = 0; i < 10; i++) emitRoot.on('bench-event', () => {})

const pluginRoot = new Context()
const plugin = ctx => {
ctx.on('bench-event', () => {})
ctx.effect(() => () => {})
}

const injectRoot = new Context()
injectRoot.inject(['bench-svc'], () => {})

let rounds = 0
while (performance.now() - start < DURATION_MS) {
for (let i = 0; i < 2000; i++) emitRoot.emit('bench-event')
for (let i = 0; i < 30; i++) {
const d = pluginRoot.effect(function* () {
yield () => {}
yield () => {}
yield () => {}
})
d()
}
for (let i = 0; i < 10; i++) {
const f = await pluginRoot.plugin(plugin)
await f.dispose()
}
for (let i = 0; i < 10; i++) {
const d = injectRoot.provide('bench-svc', i)
await 0
d()
await 0
}
await Promise.resolve()
rounds++
}
console.log('@@PROFILE rounds=' + rounds)
Loading