dev-web.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. /**
  2. * Watch-build for client-plugin HMR: runs every dshClient plugin package
  3. * through the tsdown JS API in watch mode. Reload signaling is not this
  4. * script's business — the host webserver stat-polls the bundles it serves and
  5. * broadcasts `rebuilt` frames itself (`dsh web --dev`), so any process that
  6. * rewrites `lib/client.js` files triggers reloads; this script is merely the
  7. * convenient way to keep them all rebuilt on source change.
  8. *
  9. * Usage: `pnpm exec tsx scripts/dev-web.ts [--poll[=ms]]`. Requires the
  10. * packages' node halves built once (`tsc -b tsconfig.build.json`): the lib
  11. * config's entries are tsc output. `--poll` switches the source-file watcher
  12. * to polling (default 500ms): network mounts (weka) deliver no inotify
  13. * events, so native watching sees the initial build only and never a source
  14. * change.
  15. *
  16. * Each package keeps its own tsdown.config.ts untouched: this script layers
  17. * `watch` through API-level inline config (tsdown workspace mode fills inline
  18. * keys under each package's file config, and no package config defines it).
  19. */
  20. import { readdirSync, readFileSync } from 'node:fs'
  21. import { join } from 'node:path'
  22. import { fileURLToPath } from 'node:url'
  23. import { build } from 'tsdown'
  24. const repoRoot = fileURLToPath(new URL('..', import.meta.url))
  25. /**
  26. * Discover the watch workspace by declaration: every packages/<group>/<name>
  27. * whose package.json carries `dshClient` with platform "web" is a client
  28. * plugin bundle emitter. Scanned once at startup — a package added while
  29. * watching means restarting this script.
  30. * @returns workspace-relative plugin package directories.
  31. */
  32. function discoverPluginDirs(): string[] {
  33. const dirs: string[] = []
  34. for (const group of readdirSync(join(repoRoot, 'packages'), { withFileTypes: true })) {
  35. if (!group.isDirectory()) continue
  36. for (const pkg of readdirSync(join(repoRoot, 'packages', group.name), { withFileTypes: true })) {
  37. if (!pkg.isDirectory()) continue
  38. let manifest: { dshClient?: { platform?: unknown } }
  39. try {
  40. manifest = JSON.parse(
  41. readFileSync(join(repoRoot, 'packages', group.name, pkg.name, 'package.json'), 'utf8'),
  42. ) as { dshClient?: { platform?: unknown } }
  43. } catch {
  44. continue // no package.json (support dirs, scratch): not a workspace package
  45. }
  46. if (manifest.dshClient?.platform === 'web') dirs.push(`packages/${group.name}/${pkg.name}`)
  47. }
  48. }
  49. return dirs
  50. }
  51. const PLUGIN_DIRS = discoverPluginDirs()
  52. if (PLUGIN_DIRS.length === 0) {
  53. console.error('dev-web: no dshClient (platform "web") packages found under packages/')
  54. process.exit(1)
  55. }
  56. const args = process.argv.slice(2)
  57. const pollArg = args.find(a => a === '--poll' || a.startsWith('--poll='))
  58. if (args.some(a => a !== pollArg)) {
  59. console.error('dev-web: usage: tsx scripts/dev-web.ts [--poll[=ms]]')
  60. process.exit(1)
  61. }
  62. const pollInterval = pollArg === undefined ? undefined : Number(pollArg.split('=')[1] ?? '500')
  63. if (pollInterval !== undefined && (!Number.isInteger(pollInterval) || pollInterval <= 0)) {
  64. console.error(`dev-web: invalid --poll interval "${pollArg ?? ''}"`)
  65. process.exit(1)
  66. }
  67. await build({
  68. cwd: repoRoot,
  69. workspace: PLUGIN_DIRS,
  70. watch: true,
  71. // Rolldown watch options ride through inputOptions (tsdown has no watcher
  72. // tuning of its own); polling is opt-in for network mounts without inotify.
  73. ...pollInterval !== undefined
  74. ? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
  75. : {},
  76. })
  77. console.log(
  78. `dev-web: watching ${String(PLUGIN_DIRS.length)} dshClient plugin packages`
  79. + `${pollInterval !== undefined ? ` (polling ${String(pollInterval)}ms)` : ''}:\n ${PLUGIN_DIRS.join('\n ')}`,
  80. )