Files
deepseek-harness/scripts/dev-web.ts
T
imccyu 6512e57047 feat(gui): dsh-client-hmr reload driver and the dshClient-discovered watch-build script
client-hmr is a normal plugin package composed into dev graphs only. It
listens on /plugins/events and reloads one plugin per rebuilt frame,
serialized: invalidate, prefetch (fresh factory registers while the old
fiber still serves), registry.delete before touching the fiber, drain
disposers, drop owned style tags, entry.refresh(), fiber.await() loud.
Dependency cascade costs zero client code — fiber activation epochs
re-load dependents through cordis itself. Reload is coarse by design;
no rollback in v1; self-reload works with a frame gap the next rebuild
heals.

scripts/dev-web.ts (pnpm run dev:web) is the convenience watch-build:
it discovers its package list by scanning packages/*/*/package.json for
dshClient platform "web" at startup — no hardcoded roster — and talks
no protocol to the host. Gate bookkeeping rides along: knip entries for
the new packages, README model-experience allowlist rows.
2026-07-24 02:09:42 +08:00

86 lines
3.6 KiB
TypeScript

/**
* Watch-build for client-plugin HMR: runs every dshClient plugin package
* through the tsdown JS API in watch mode. Reload signaling is not this
* script's business — the host webserver stat-polls the bundles it serves and
* broadcasts `rebuilt` frames itself (`dsh web --dev`), so any process that
* rewrites `lib/client.js` files triggers reloads; this script is merely the
* convenient way to keep them all rebuilt on source change.
*
* Usage: `pnpm exec tsx scripts/dev-web.ts [--poll[=ms]]`. Requires the
* packages' node halves built once (`tsc -b tsconfig.build.json`): the lib
* config's entries are tsc output. `--poll` switches the source-file watcher
* to polling (default 500ms): network mounts (weka) deliver no inotify
* events, so native watching sees the initial build only and never a source
* change.
*
* Each package keeps its own tsdown.config.ts untouched: this script layers
* `watch` through API-level inline config (tsdown workspace mode fills inline
* keys under each package's file config, and no package config defines it).
*/
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { build } from 'tsdown'
const repoRoot = fileURLToPath(new URL('..', import.meta.url))
/**
* Discover the watch workspace by declaration: every packages/<group>/<name>
* whose package.json carries `dshClient` with platform "web" is a client
* plugin bundle emitter. Scanned once at startup — a package added while
* watching means restarting this script.
* @returns workspace-relative plugin package directories.
*/
function discoverPluginDirs(): string[] {
const dirs: string[] = []
for (const group of readdirSync(join(repoRoot, 'packages'), { withFileTypes: true })) {
if (!group.isDirectory()) continue
for (const pkg of readdirSync(join(repoRoot, 'packages', group.name), { withFileTypes: true })) {
if (!pkg.isDirectory()) continue
let manifest: { dshClient?: { platform?: unknown } }
try {
manifest = JSON.parse(
readFileSync(join(repoRoot, 'packages', group.name, pkg.name, 'package.json'), 'utf8'),
) as { dshClient?: { platform?: unknown } }
} catch {
continue // no package.json (support dirs, scratch): not a workspace package
}
if (manifest.dshClient?.platform === 'web') dirs.push(`packages/${group.name}/${pkg.name}`)
}
}
return dirs
}
const PLUGIN_DIRS = discoverPluginDirs()
if (PLUGIN_DIRS.length === 0) {
console.error('dev-web: no dshClient (platform "web") packages found under packages/')
process.exit(1)
}
const args = process.argv.slice(2)
const pollArg = args.find(a => a === '--poll' || a.startsWith('--poll='))
if (args.some(a => a !== pollArg)) {
console.error('dev-web: usage: tsx scripts/dev-web.ts [--poll[=ms]]')
process.exit(1)
}
const pollInterval = pollArg === undefined ? undefined : Number(pollArg.split('=')[1] ?? '500')
if (pollInterval !== undefined && (!Number.isInteger(pollInterval) || pollInterval <= 0)) {
console.error(`dev-web: invalid --poll interval "${pollArg ?? ''}"`)
process.exit(1)
}
await build({
cwd: repoRoot,
workspace: PLUGIN_DIRS,
watch: true,
// Rolldown watch options ride through inputOptions (tsdown has no watcher
// tuning of its own); polling is opt-in for network mounts without inotify.
...pollInterval !== undefined
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
: {},
})
console.log(
`dev-web: watching ${String(PLUGIN_DIRS.length)} dshClient plugin packages`
+ `${pollInterval !== undefined ? ` (polling ${String(pollInterval)}ms)` : ''}:\n ${PLUGIN_DIRS.join('\n ')}`,
)