mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
React, loaded by Ink, picks its development build unless NODE_ENV is exactly "production". Since React 19.2 that build records a performance.measure() entry on every render (its DevTools "Scheduler" and "Components" tracks), and Node keeps user-timing marks and measures in a process-wide buffer that nothing ever trims. The interactive dashboard re-renders on every progress tick and refresh, so the buffer grows by roughly 700 entries a minute; after a few days the heap hits V8's 4 GB default and the process aborts with "Ineffective mark-compacts near heap limit ... JavaScript heap out of memory". The launcher (dist/cli.js, the entry every client spawns) now defaults NODE_ENV to "production" before importing main.js, which selects the build that records nothing; an explicit NODE_ENV still wins. For that explicit case, a small guard drops the user-timing buffer every 30 s for the lifetime of each Ink UI (dashboard, sessions, context, compare). codeburn records no marks or measures of its own, so nothing is lost. Tests spawn the real launcher against a stub main.js to pin the default, and drive a ticking Ink app in a child Node process to show the development build's backlog, the production build's absence of one, and the guard keeping it bounded.
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
// The launcher (src/cli.ts, copied verbatim to dist/cli.js) is the only place
|
|
// that runs before React and Ink are imported. React picks its development
|
|
// build unless NODE_ENV is exactly "production", and that build records a
|
|
// performance.measure() entry on every render into Node's user-timing buffer,
|
|
// which nothing ever trims: an interactive dashboard left open for days grows
|
|
// until V8 aborts with "JavaScript heap out of memory". The launcher must
|
|
// default NODE_ENV to "production" while leaving an explicit value untouched.
|
|
|
|
let dir: string
|
|
|
|
function runLauncher(env: Record<string, string | undefined>): string {
|
|
const base = { ...process.env }
|
|
delete base['NODE_ENV']
|
|
const result = spawnSync(process.execPath, [join(dir, 'cli.js')], {
|
|
cwd: dir,
|
|
encoding: 'utf8',
|
|
env: { ...base, ...env },
|
|
})
|
|
expect(result.status, result.stderr).toBe(0)
|
|
return result.stdout.trim()
|
|
}
|
|
|
|
describe('CLI launcher NODE_ENV default', () => {
|
|
beforeAll(() => {
|
|
dir = mkdtempSync(join(tmpdir(), 'codeburn-cli-node-env-'))
|
|
copyFileSync(join(process.cwd(), 'src', 'cli.ts'), join(dir, 'cli.js'))
|
|
// Stand-in for dist/main.js: report the environment the launcher handed us.
|
|
writeFileSync(join(dir, 'main.js'), 'process.stdout.write(JSON.stringify(process.env.NODE_ENV ?? null))\n')
|
|
writeFileSync(join(dir, 'package.json'), '{"type":"module"}\n')
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('defaults NODE_ENV to production when the shell leaves it unset', () => {
|
|
expect(runLauncher({})).toBe('"production"')
|
|
})
|
|
|
|
it('treats an empty NODE_ENV as unset', () => {
|
|
expect(runLauncher({ NODE_ENV: '' })).toBe('"production"')
|
|
})
|
|
|
|
it('keeps an explicit NODE_ENV', () => {
|
|
expect(runLauncher({ NODE_ENV: 'development' })).toBe('"development"')
|
|
})
|
|
})
|