Files
Ant-Browser/frontend/scripts/dev-watcher.mjs
T
Ant Browser Release Bot fc5a6fadbe publish: 1.1.0 snapshot (ff46de5)
channel: master
version: 1.1.0
source-ref: master
published-at-utc: 2026-03-20T14:14:07Z
2026-03-20 22:14:08 +08:00

237 lines
6.1 KiB
JavaScript

import { spawn, spawnSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
const frontendDir = resolve(scriptDir, '..')
const defaultVitePort = 5218
const defaultMaxOldSpaceSizeMb = 256
const defaultMaxSemiSpaceSizeMb = 16
const defaultRssWarnMb = 256
const defaultRssHardLimitMb = 360
const defaultMemoryPollMs = 3000
const nodeExecutable = process.execPath
const ensureNativeScript = resolve(frontendDir, 'scripts', 'ensure-rollup-native.mjs')
const viteEntry = resolve(frontendDir, 'node_modules', 'vite', 'bin', 'vite.js')
function ensureNativeRuntime(env) {
const result = spawnSync(nodeExecutable, [ensureNativeScript], {
cwd: frontendDir,
stdio: 'inherit',
env,
})
if (result.error) {
throw result.error
}
if ((result.status ?? 0) !== 0) {
throw new Error(`ensure native failed with exit code ${result.status ?? 1}`)
}
}
function resolveRequestedPort(rawPort, fallbackPort = defaultVitePort) {
const parsed = Number.parseInt(String(rawPort || '').trim(), 10)
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
return parsed
}
return fallbackPort
}
function resolvePositiveInteger(rawValue, fallbackValue) {
const parsed = Number.parseInt(String(rawValue || '').trim(), 10)
if (Number.isInteger(parsed) && parsed > 0) {
return parsed
}
return fallbackValue
}
function resolveNodeArgs(env) {
const maxOldSpaceSizeMb = resolvePositiveInteger(
env.FRONTEND_NODE_MAX_OLD_SPACE_SIZE_MB,
defaultMaxOldSpaceSizeMb,
)
const maxSemiSpaceSizeMb = resolvePositiveInteger(
env.FRONTEND_NODE_MAX_SEMI_SPACE_SIZE_MB,
defaultMaxSemiSpaceSizeMb,
)
const args = [`--max-old-space-size=${maxOldSpaceSizeMb}`]
if (maxSemiSpaceSizeMb > 0) {
args.push(`--max-semi-space-size=${maxSemiSpaceSizeMb}`)
}
if (String(env.FRONTEND_NODE_HEAP_SNAPSHOT || '').trim() === '1') {
args.push('--heapsnapshot-near-heap-limit=2')
}
args.push(viteEntry)
return {
args,
maxOldSpaceSizeMb,
maxSemiSpaceSizeMb,
}
}
function killProcessTree(pid) {
if (!pid || pid <= 0) {
return
}
if (process.platform === 'win32') {
spawnSync('taskkill.exe', ['/F', '/T', '/PID', String(pid)], {
stdio: 'ignore',
})
return
}
try {
process.kill(pid, 'SIGTERM')
} catch {}
}
function readProcessRssMb(pid) {
if (!pid || pid <= 0) {
return 0
}
if (process.platform === 'win32') {
const result = spawnSync(
'powershell.exe',
[
'-NoProfile',
'-Command',
`$proc = Get-Process -Id ${pid} -ErrorAction SilentlyContinue; if ($proc) { [math]::Round($proc.WorkingSet64 / 1MB, 0) }`,
],
{
cwd: frontendDir,
encoding: 'utf8',
},
)
if (result.status !== 0) {
return 0
}
return resolvePositiveInteger(result.stdout, 0)
}
const result = spawnSync('ps', ['-o', 'rss=', '-p', String(pid)], {
cwd: frontendDir,
encoding: 'utf8',
})
if (result.status !== 0) {
return 0
}
const rssKb = resolvePositiveInteger(result.stdout, 0)
return Math.round(rssKb / 1024)
}
function startMemoryWatcher(child, env) {
const rssWarnMb = resolvePositiveInteger(env.FRONTEND_NODE_RSS_WARN_MB, defaultRssWarnMb)
const rssHardLimitMb = resolvePositiveInteger(env.FRONTEND_NODE_RSS_HARD_LIMIT_MB, defaultRssHardLimitMb)
const pollMs = resolvePositiveInteger(env.FRONTEND_NODE_MEMORY_POLL_MS, defaultMemoryPollMs)
let warnedAtMb = 0
const timer = setInterval(() => {
if (!child.pid || child.exitCode !== null) {
return
}
const rssMb = readProcessRssMb(child.pid)
if (rssMb <= 0) {
return
}
if (rssMb >= rssWarnMb && (warnedAtMb === 0 || Math.abs(rssMb - warnedAtMb) >= 128)) {
warnedAtMb = rssMb
console.warn(`[dev] vite RSS is ${rssMb} MB (warning threshold: ${rssWarnMb} MB)`)
}
if (rssHardLimitMb > 0 && rssMb >= rssHardLimitMb) {
console.error(`[dev] vite RSS reached ${rssMb} MB, exceeding hard limit ${rssHardLimitMb} MB. stopping dev server.`)
killProcessTree(child.pid)
}
}, pollMs)
timer.unref?.()
return timer
}
function main() {
const requestedPort = resolveRequestedPort(process.env.FRONTEND_PORT, defaultVitePort)
const childEnv = {
...process.env,
FRONTEND_PORT: String(requestedPort),
}
ensureNativeRuntime(childEnv)
const nodeArgs = resolveNodeArgs(childEnv)
console.log(
`[dev] starting Vite on http://127.0.0.1:${requestedPort} with --max-old-space-size=${nodeArgs.maxOldSpaceSizeMb} MB --max-semi-space-size=${nodeArgs.maxSemiSpaceSizeMb} MB --rss-hard-limit=${resolvePositiveInteger(childEnv.FRONTEND_NODE_RSS_HARD_LIMIT_MB, defaultRssHardLimitMb)} MB`,
)
const child = spawn(nodeExecutable, nodeArgs.args, {
cwd: frontendDir,
stdio: 'inherit',
env: childEnv,
})
const memoryWatcher = startMemoryWatcher(child, childEnv)
let shuttingDown = false
const shutdown = (exitCode = 0) => {
if (shuttingDown) {
return
}
shuttingDown = true
if (memoryWatcher) {
clearInterval(memoryWatcher)
}
if (child.pid && child.exitCode === null) {
killProcessTree(child.pid)
}
process.exit(exitCode)
}
const handleSignal = (signal) => {
console.log(`[dev] received ${signal}, stopping Vite...`)
shutdown(0)
}
process.on('SIGINT', handleSignal)
process.on('SIGTERM', handleSignal)
process.on('exit', () => {
if (memoryWatcher) {
clearInterval(memoryWatcher)
}
if (child.pid && child.exitCode === null) {
killProcessTree(child.pid)
}
})
child.on('error', (error) => {
console.error(`[dev] failed to start Vite: ${error instanceof Error ? error.message : String(error)}`)
shutdown(1)
})
child.on('exit', (code, signal) => {
if (memoryWatcher) {
clearInterval(memoryWatcher)
}
if (signal) {
console.error(`[dev] vite exited with signal ${signal}`)
process.exit(1)
return
}
process.exit(code ?? 0)
})
}
try {
main()
} catch (error) {
console.error(`[dev] ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}