Bundler Configuration
Vite Plugin (recommended)
Section titled “Vite Plugin (recommended)”The Sygnal Vite plugin configures JSX, wires up HMR with state preservation, and turns on diagnostics in the dev server:
import { defineConfig } from 'vite'import sygnal from 'sygnal/vite'
export default defineConfig({ plugins: [sygnal()],})That’s it. Your entry file just needs run():
import { run } from 'sygnal'import App from './App.jsx'
run(App)The plugin detects the run() call, finds the imported root component, and automatically injects HMR wiring during development. No manual import.meta.hot boilerplate needed.
Plugin Options
Section titled “Plugin Options”| Option | Type | Default | Description |
|---|---|---|---|
disableJsx | boolean | false | Don’t configure JSX (set it up yourself) |
disableHmr | boolean | false | Don’t inject HMR wiring (handle HMR yourself, or let a framework such as Vike do it) |
diagnostics | 'off' | 'collect' | 'warn' | 'error' or { mode, strict, ignore } | 'warn' | Runtime diagnostics in the dev server. 'off' injects no checks |
diagnostics.mode | same as above | 'warn' | The runtime diagnostics mode |
diagnostics.strict | boolean | false | Turn on the strict-mode runtime checks; also the default for check.strict |
diagnostics.ignore | string[] | [] | Codes to drop, at runtime and in sygnal-check |
check | boolean or { strict, a11y, include, ignore, overlay } | true | Run sygnal-check in the dev server (skipped silently when it isn’t installed) |
check.strict | boolean | the config file’s strict, else diagnostics.strict | Also run the strict rules (SYG501-508) |
check.a11y | 'warn' | 'error' | the config file’s a11y, else 'warn' | Severity of the accessibility findings (SYG701-708). They stay warnings with strict; 'error' makes them errors, which open the overlay |
check.include | string[] | the config file’s paths, else the existing src/, pages/, renderer/ directories, else the project root | Files, directories or globs to check, relative to the Vite root |
check.ignore | string[] | the config file’s ignore, else diagnostics.ignore | Codes to drop |
check.overlay | 'error' | false | 'error' | Whether error-severity findings open Vite’s error overlay |
vitestSetup | boolean | true | Under Vitest, add sygnal/diagnostics to test.setupFiles |
nativeGlobalThis | boolean | true | Resolve xstream’s globalthis polyfill to the native globalThis (see below) |
devtools | boolean | true | Install the DevTools bridge in the dev server. false leaves it out |
mcp | boolean or { timeout, confirm } | false | Serve the dev MCP endpoint at /__sygnal/mcp, so a coding agent can read and drive the open page. Also installs the DevTools bridge |
mcp.timeout | number | 10000 | Milliseconds to wait for the page’s answer |
mcp.confirm | 'page' | boolean | 'page' | A consequential agent tool (agent.actions.X.consequential) asks the person in the page first ('page'); true runs it, false declines it |
sygnal({ diagnostics: { mode: 'error', strict: true, ignore: ['SYG105'] }, check: { include: ['src', 'lib'] },})The sygnal-check config file
Section titled “The sygnal-check config file”The check option’s defaults come from the project’s sygnal-check config file when there is one: sygnal-check.config.json at the project root, or the "sygnal-check" key in package.json. That is the file the sygnal-check CLI, its MCP server and the editor read too, so the dev overlay and the editor report the same findings. A check option you set still wins; a setting neither sets falls back to diagnostics.strict / diagnostics.ignore and the defaults above. The plugin re-checks when the config file changes; a change to its paths applies after a dev server restart. sygnal-check before 0.4.0 has no config file, and the check option works as before.
{ "strict": true, "ignore": ["SYG105"] }What the plugin does in dev
Section titled “What the plugin does in dev”Everything below happens only in vite / vite dev. A production build (vite build) gets the JSX configuration and the globalthis alias, and nothing else: no flags, checks, wrappers, dev client or DevTools.
-
DevTools. The same files also import
sygnal/devtoolsfirst, which installs the bridge the DevTools extension connects to (window.__SYGNAL_DEVTOOLS__). It is independent ofdiagnostics('off'still installs it);sygnal({ devtools: false })leaves it out. -
Diagnostics. Every file that imports
runfromsygnalgets a dev flag (runtime diagnostics in'warn'mode) and imports ofsygnal/diagnostics(the dev checks) andvirtual:sygnal/dev(which logssygnal-checkresults in the browser console). They’re added on an existing line, so line numbers and source maps don’t change. A mode other than'warn', or an ignore list, is passed torun()as itsdiagnosticsoption, unless therun()call sets that option itself. Opt out for one app withrun(App, drivers, { diagnostics: 'off' }), or for the whole server withsygnal({ diagnostics: 'off' }). -
MCP endpoint. With
mcp: truethe dev server answers MCP requests at/__sygnal/mcp, and the same files importvirtual:sygnal/mcp, which connects the page to it over Vite’s HMR connection. See Dev server MCP endpoint. -
Vike and Astro. Their apps are started by Sygnal’s own client entries, which get the same dev setup (see Vike and Astro).
-
sygnal-check. When the
sygnal-checkpackage is installed, the plugin checkscheck.includewhen the dev server starts and after every source change. Findings are printed in the terminal and logged in the browser console. Whencheck.includeisn’t set, it checks thesrc/,pages/andrenderer/directories that exist, or the project root (skippingnode_modulesand build output) with a one-time notice; a notice is also logged when none of the given paths exists. -
The error overlay. Only error-severity findings open Vite’s error overlay (
overlay: falseturns that off). Warnings never do, because Vite reloads the page on the next HMR update while an overlay is open;overlay: 'warn'is accepted but treated as'error', with a notice. Mostsygnal-checkfindings are warnings or info; the errors are wiring that fails for certain (SYG112, SYG124, SYG125, SYG128, SYG405 and a few more). The accessibility findings stay warnings, also withstrict, unlesscheck.a11yis'error'. The overlay is sent only to the page that loads, closed before each HMR update and sent again after the re-check, so it never causes a reload or goes stale.
Vitest
Section titled “Vitest”Under Vitest, the plugin configures JSX and adds sygnal/diagnostics to test.setupFiles (merged with your own setup files, never added twice), so renderComponent() tests get the dev checks. It also allows that file’s directory in server.fs.allow, so the setup works in jsdom and happy-dom environments with a linked Sygnal. Set vitestSetup: false to manage the setup yourself. Nothing else is injected under Vitest: no dev flags, HMR wiring or checker, because renderComponent() manages the diagnostics mode per test.
Native globalThis
Section titled “Native globalThis”xstream loads the globalthis npm polyfill, which brings a chain of small packages (about 4 KB gzipped) into every app. Every browser and Node version Sygnal supports has a native globalThis, so the plugin adds a resolve.alias from globalthis to a stub in the Sygnal package (sygnal/shims/globalthis) that returns it. The alias applies in the dev server (including dependency pre-bundling), vite build and Vitest; the Astro integration adds it in astro build too. The alias applies to every import of globalthis in the app, so any other dependency that loads the polyfill gets the stub too. If your own resolve.alias (object or array form) already maps globalthis, the plugin leaves it alone and adds nothing. Set nativeGlobalThis: false to keep the polyfill package. Without the plugin, add the same alias yourself: resolve: { alias: [{ find: /^globalthis$/, replacement: 'sygnal/shims/globalthis' }] }.
JSX and Vite versions
Section titled “JSX and Vite versions”The plugin sets Vite 8’s oxc JSX options. Under Vite 7 and older (for example Astro 6, or Vitest running on Vite 7), which compile JSX with esbuild, it also sets the esbuild options. In the Vite 8 dev server it also configures JSX for the dependency scanner, so the first page load doesn’t fail to resolve react/jsx-dev-runtime.
How the HMR transform works
Section titled “How the HMR transform works”In dev mode, the plugin transforms your entry file from:
import { run } from 'sygnal'import App from './App.jsx'run(App)Into:
import { run } from 'sygnal'import App from './App.jsx'const __sygnal = run(App)if (import.meta.hot) { import.meta.hot.accept('./App.jsx', __sygnal.hmr) import.meta.hot.dispose(__sygnal.dispose)}HMR wiring is only added for a top-level run() call in a recognized shape. If you already have import.meta.hot in your file, the plugin leaves it alone. Test files (*.test.*, *.spec.*) are never transformed.
Manual Vite Configuration
Section titled “Manual Vite Configuration”If you prefer not to use the plugin, configure JSX manually:
import { defineConfig } from 'vite'
export default defineConfig({ oxc: { jsx: { runtime: 'automatic', importSource: 'sygnal' }, },})Under Vite 7 and older, use the esbuild options instead:
import { defineConfig } from 'vite'
export default defineConfig({ esbuild: { jsx: 'automatic', jsxImportSource: 'sygnal', },})For TypeScript projects, also add to tsconfig.json:
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "sygnal" }}And wire HMR yourself — see Hot Module Replacement. For diagnostics without the plugin, see Diagnostics; for the DevTools extension, import sygnal/devtools in dev (see DevTools without Vite).
Other Bundlers
Section titled “Other Bundlers”For Webpack, Rollup, or other bundlers that support the automatic JSX transform, configure them with sygnal as the JSX import source. The general pattern is:
// General pattern (option names vary by bundler)const jsxOptions = { jsx: 'automatic', // or equivalent setting jsxImportSource: 'sygnal', // or equivalent setting}Classic JSX transform (legacy, still supported)
If your bundler does not support the automatic JSX transform, you can use the classic transform:
export default defineConfig({ esbuild: { jsxInject: `import { jsx, Fragment } from 'sygnal/jsx'`, jsxFactory: 'jsx', jsxFragment: 'Fragment' }})Note: With the classic transform, some minifiers may rename the Fragment function, causing JSX fragments to break. To fix this with Vite, install terser and add:
export default defineConfig({ build: { minify: 'terser', terserOptions: { mangle: { reserved: ['Fragment'] } } }})This is not an issue with the automatic transform.
Using Without JSX
Section titled “Using Without JSX”If you prefer not to use JSX, use the h() function from Sygnal (re-exported from @cycle/dom):
import { h } from 'sygnal'
function MyComponent({ state }) { return h('div', [ h('h1', `Hello ${state.name}`), h('button.increment', 'Click me') ])}