@domstack/sync

A modern, dependency-minimized live-reload dev server. A love letter to browser-sync: ESM-only, Fastify, native WebSocket, and a server-rendered HTMX UI panel.
Install
npm install @domstack/sync
CLI
domstack-sync --server . --files '**/*.css' '**/*.html'
domstack-sync --files '**/*.css'
dss --server . --files '**/*'
domstack-sync --server . --no-ui
domstack-sync init
domstack-sync reload --port 3000
CLI flags
--server, -s | — | Directory to serve |
--files, -f | — | Glob patterns to watch (repeatable) |
--port | 3000 | Port to listen on |
--no-ui | — | Disable the UI panel |
--no-notify | — | Disable the notification overlay |
--no-ghost-mode | — | Disable scroll/click/form sync |
--cors | — | Enable CORS headers |
--log-level | info | silent | debug | info | warn | error |
--log-connections | — | Log browser connection events at info level |
--help, -h | — | Show help text |
--version, -v | — | Show version |
API
import { createServer, parseOptions } from '@domstack/sync'
const sync = await createServer({
server: './public',
files: ['public/**/*.css', 'public/**/*.html'],
port: 3000,
})
console.log(sync.url)
console.log(sync.uiUrl)
sync.reload()
sync.reload(['styles/main.css'])
sync.notify('Build complete')
someReadableStream.pipe(sync.stream())
sync.events.on('client:connect', (info) => console.log('connected', info.ua))
sync.events.on('client:disconnect', (id) => console.log('disconnected', id))
sync.events.on('file:change', (evt) => console.log('changed', evt.path))
sync.pause()
sync.resume()
await sync.exit()
Options
port | number | 3000 | Port to listen on (falls back to OS-assigned if taken) |
server | string | false | false | Directory to serve statically |
files | string[] | [] | Glob patterns to watch for changes |
ghostMode | { scroll, clicks, forms } | all true | Sync interactions across connected browsers |
logLevel | 'silent' | 'debug' | 'info' | 'warn' | 'error' | 'info' | Log verbosity |
logConnections | boolean | false | Log browser connection events at info level |
ui | boolean | { port: number } | true | UI panel — false disables, { port } pins the port |
notify | boolean | true | Show notification overlay in connected browsers |
cors | boolean | false | Add CORS headers to all responses |
injectChanges | boolean | true | CSS-inject .css changes instead of full reload |
reloadDebounce | number | 500 | Milliseconds to debounce file-change reloads |
reloadDelay | number | 0 | Milliseconds to delay reload after a change |
watchOptions | object | {} | Options passed through to chokidar |
cwd | string | process.cwd() | Working directory for resolving server and files |
TypeScript
All types are exported:
import type { BsInstance, BsOptions, BsOptionsInput } from '@domstack/sync'
import type { ServerToClientMessage, ClientToServerMessage } from '@domstack/sync'
How it works
- Script injection — an IIFE
<script> tag is injected before </body> (or appended if there is no </body>) in every HTML response. The injected script connects to the WebSocket server.
- CSS injection — when a watched
.css file changes and injectChanges: true, the matching <link rel="stylesheet"> has its href cache-busted in place without a full page reload. Falls back to a full reload if no matching stylesheet is found.
- Ghost mode — scroll position, clicks, and form input changes in one browser are relayed to all other connected browsers via the WebSocket server. Disable with
ghostMode: false or --no-ghost-mode.
- UI panel — a separate Fastify server on an auto-detected port shows connected browsers, file-change history, and server URLs. Disable with
ui: false or --no-ui.
- Port selection — the requested port is tried first. If it is already in use, the OS assigns a free ephemeral port automatically.
License
MIT