sygnal 6.6.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +258 -0
- package/README.md +2 -0
- package/dist/astro/index.cjs.js +32 -9
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +32 -9
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +32 -43
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +32 -43
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +154 -6
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +153 -7
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +3 -1
- package/dist/guide/ai-chat.md +6 -6
- package/dist/guide/behaviors.md +1 -1
- package/dist/guide/element-commands.md +4 -2
- package/dist/guide/forms-reference.md +89 -3
- package/dist/guide/forms.md +8 -8
- package/dist/guide/http.md +77 -7
- package/dist/guide/migrating-to-7.md +314 -0
- package/dist/guide/recipes/ai-summarize.md +2 -1
- package/dist/guide/recipes/i18n.md +1 -1
- package/dist/guide/resources.md +2 -0
- package/dist/guide/timers.md +3 -3
- package/dist/guide/ui/tabs.md +5 -4
- package/dist/guide/ui/tooltip.md +24 -0
- package/dist/index.cjs.js +954 -388
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +300 -46
- package/dist/index.esm.js +953 -389
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui.cjs.js +96 -11
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +96 -12
- package/dist/ui.esm.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +19 -9
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +19 -9
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +23 -22
- package/package.json +1 -1
- package/src/astro/index.d.ts +8 -0
- package/src/astro/index.ts +18 -0
- package/src/core/actions.ts +28 -21
- package/src/core/define.ts +2 -0
- package/src/core/hooks.ts +2 -2
- package/src/core/hosts/collection.ts +4 -3
- package/src/core/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +66 -17
- package/src/core/runtime.ts +5 -1
- package/src/core/statics.ts +5 -5
- package/src/extra/ai/chat/behavior.ts +3 -3
- package/src/extra/ai/commandBar.ts +3 -3
- package/src/extra/ai/compose/behavior.ts +3 -3
- package/src/extra/ai/transports/shared.ts +10 -2
- package/src/extra/behaviors.ts +24 -3
- package/src/extra/diagnostics/checks/fetch.ts +21 -1
- package/src/extra/diagnostics/checks/forms.ts +4 -3
- package/src/extra/diagnostics/checks/index.ts +1 -0
- package/src/extra/diagnostics/checks/next.ts +49 -3
- package/src/extra/diagnostics/checks/props.ts +6 -1
- package/src/extra/diagnostics/checks/public.d.ts +12 -0
- package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
- package/src/extra/diagnostics/codes.ts +24 -0
- package/src/extra/fetchDriver.ts +115 -7
- package/src/extra/fetchEventSource.ts +117 -0
- package/src/extra/form.ts +68 -26
- package/src/extra/formHelpers.ts +3 -1
- package/src/extra/lens.ts +3 -2
- package/src/extra/queryCache.ts +4 -3
- package/src/extra/socketDriver.ts +60 -9
- package/src/extra/ssr.ts +30 -41
- package/src/extra/testing.ts +100 -18
- package/src/extra/timers.ts +5 -2
- package/src/index.d.ts +299 -45
- package/src/index.ts +3 -0
- package/src/shared.ts +3 -0
- package/src/ui/dialog.ts +3 -2
- package/src/ui/tooltips.ts +92 -0
- package/src/ui.d.ts +35 -0
- package/src/ui.ts +1 -0
- package/src/vite/graph.ts +11 -3
- package/src/vite/plugin.d.ts +7 -0
- package/src/vite/plugin.ts +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,264 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Sygnal are listed here. Versions follow [semantic versioning](https://semver.org). Releases before 5.4.0 are described in the [GitHub releases](https://github.com/tpresley/sygnal/releases) and tags.
|
|
4
4
|
|
|
5
|
+
## 7.1.0 — 2026-10-20
|
|
6
|
+
|
|
7
|
+
Fixes and additions from building an app (Temper's Tune UI) on 7.0 ([PLAN-13](dev-plans/PLAN-13.md)):
|
|
8
|
+
an invalidation / navigation race, declaration statics that see the context, per-page Switchable
|
|
9
|
+
state, SSE resume and dead-stream detection, reply helpers for hand-written drivers, and more
|
|
10
|
+
precise `sygnal-check` findings. All additive.
|
|
11
|
+
|
|
12
|
+
**Measured impact.** Size: the kanban gate is (a) 42,982 B with `nativeGlobalThis: false` (7.0.0:
|
|
13
|
+
42,844 B; budget 43,000 B, 18 B headroom) and (b) 38,987 B by default. Statics with context cost
|
|
14
|
+
19 B and the per-page Switchable map 126 B (it reuses the owner's binding watch for a page whose
|
|
15
|
+
record is deleted); everything else is in drivers, the dev diagnostics entry, the test helper or
|
|
16
|
+
`sygnal-check`, and adds nothing to an app that doesn't use it. The perf count gate is within its
|
|
17
|
+
limits.
|
|
18
|
+
|
|
19
|
+
**Companion packages.**
|
|
20
|
+
- **`sygnal-check` 0.12.0:** SYG117 (`next()` to an action with no entry here), SYG302 (`import xs
|
|
21
|
+
from 'xstream'`, with `--fix`), SYG429 (a Switchable state map key that names no page); reply names
|
|
22
|
+
through request helpers and consts; SYG256 sees past helper calls in other fields; `next` followed
|
|
23
|
+
into helpers, imported ones included; context reads in the statics count for SYG170; `replyKeys`
|
|
24
|
+
in the config file. Its caret range doesn't reach 0.12.0 from `^0.11.0`, so update the dev
|
|
25
|
+
dependency.
|
|
26
|
+
- **`create-sygnal-app` 2.10.0:** templates depend on `sygnal` ^7.1.0 and `sygnal-check` ^0.12.0, use
|
|
27
|
+
a system font stack (no Google Fonts) and scaffold a `.gitignore`.
|
|
28
|
+
- **VS Code extension 0.9.0:** bundles `sygnal-check` 0.12.0.
|
|
29
|
+
- **Claude Code plugin 0.2.1:** the `sygnal-dev` skill covers 7.1 (statics with context, per-page
|
|
30
|
+
Switchable state, SSE resume, reply helpers, `import { xs } from 'sygnal'`).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **SYG117: `next()` to an action the component has no entry for (PLAN-13 N-1).** `next('X')`
|
|
35
|
+
reaches only its own component's model, so a child's `next('ENTER')` for an action only its
|
|
36
|
+
parent handles silently did nothing. The dev checks (`sygnal/diagnostics`, 0 production bytes)
|
|
37
|
+
warn on the first such `next()` per component and name, naming the ancestor that handles it and
|
|
38
|
+
the fix (send it up with `PARENT` or `EVENTS`), or the closest entry for a typo. `sygnal-check`
|
|
39
|
+
reports a literal `next('X')` with no entry `X` in the same component (with a rename fix for a typo).
|
|
40
|
+
|
|
41
|
+
- **sygnal-check reads reply names through request helpers and consts (PLAN-13 C-1).** An object
|
|
42
|
+
argument with a reply key of a call a sink returns is a request (`req('/a', { ok: 'GOT_A' })`,
|
|
43
|
+
`api.get_run(args, { ok: 'RUN' })`, any callee), a returned const is followed (`HTTP: () => SNAP`),
|
|
44
|
+
and so are `connections` specs and SSE `events` maps held in consts. SYG102 no longer reports
|
|
45
|
+
their reply actions as unreachable, SYG112 checks the names, SYG152 and `--graph` see the requests.
|
|
46
|
+
|
|
47
|
+
- **SYG256 sees past helper calls in other fields (PLAN-13 C-2).** A call in the STATE reducer
|
|
48
|
+
stopped the proof, so a leave-guard like `EDIT: { STATE: (s, d) => ({ ...s, dirty: true, edit:
|
|
49
|
+
{ ...s.edit, [keyOf(s)]: d } }), ROUTER: (s) => s.dirty ? ABORT : … }` went unreported. A call
|
|
50
|
+
(or a function) inside a plain field's value now only makes that field unknown; calls in a test,
|
|
51
|
+
a spread or a computed key of the written object, and any assignment, still keep the check out.
|
|
52
|
+
|
|
53
|
+
- **create-sygnal-app: no Google Fonts, and a `.gitignore` (PLAN-13 T-1).** The vite, vite-pwa and
|
|
54
|
+
astro templates loaded Inter from Google Fonts; every template now uses a system font stack and
|
|
55
|
+
loads nothing remote. No template wrote a `.gitignore` (npm leaves it out of the package): each
|
|
56
|
+
ships `gitignore`, scaffolded as `.gitignore` (`node_modules`, `dist`, `.vite`, `.astro`,
|
|
57
|
+
`*.local`, `dev-dist` for the PWA). The release smoke checks both.
|
|
58
|
+
|
|
59
|
+
- **SYG302: `import xs from 'xstream'` (PLAN-13 X-1).** Under Node ESM and Vite 8 (a `"type":
|
|
60
|
+
"module"` package) xstream's CommonJS default import is the module object, so a custom driver's
|
|
61
|
+
`xs.create` is undefined. The docs, llms.txt and the skill use `import { xs } from 'sygnal'`
|
|
62
|
+
(the accessibility guide's sample was the last default import), the custom-drivers guide says
|
|
63
|
+
why, and `sygnal-check` reports the default import (info) with a `--fix`.
|
|
64
|
+
|
|
65
|
+
- **`makeReplies()` and `senderOf()` for drivers you write (PLAN-13 D-1).** The reply-action
|
|
66
|
+
protocol `makeFetchDriver`, `driverFromAsync` and the socket driver use is exported and typed
|
|
67
|
+
(`ReplySender`, `ReplyChannel`): spread `replies` into the source, `reply(senderOf(req), type,
|
|
68
|
+
data)` as often as needed (progress, then a result), and abort a disposed sender's work in
|
|
69
|
+
`onStop`. The custom-drivers guide has an upload driver that replies more than once.
|
|
70
|
+
|
|
71
|
+
- **SSE event ids, resuming and dead streams (PLAN-13 S-1).** On a `connections` spec: `meta:
|
|
72
|
+
true` gives SSE `message` / `events` actions `{ data, id, event }`; `resume: true | 'param'`
|
|
73
|
+
keeps the last event id and makes a reconnect the driver makes (after EventSource gave up, e.g. a
|
|
74
|
+
proxy 502, or a non-2xx on the `headers` path) resume from it, as `Last-Event-ID` over fetch or
|
|
75
|
+
`?lastEventId=` (or `?param=`) on the EventSource URL; `staleAfterMs` (SSE and WebSocket) closes
|
|
76
|
+
a connection that stays silent that long as a drop (`close` gets `stale: true`) so the reconnect
|
|
77
|
+
policy applies; over fetch, heartbeat comments count. `t.push(sink, data, { id })` gives a test
|
|
78
|
+
event its id. No bytes for apps without a socket driver.
|
|
79
|
+
|
|
80
|
+
- **Declaration statics see the context (PLAN-13 F-1).** `resources`, `timers`, `connections`,
|
|
81
|
+
`head` and `browser` get `(state, context)` and are computed again when the component's context
|
|
82
|
+
changes (by identity, D603; an unrelated write re-runs nothing). A routed page reads `route` from
|
|
83
|
+
context (`App.context = { route: (s) => s.route }`, `TaskPage.resources = { task: (s, { route })
|
|
84
|
+
=> … }`) instead of keeping a copy of the params in its own state. Server rendering passes the
|
|
85
|
+
context to `resources` and `head` too; the types take it as the second parameter; `sygnal-check`
|
|
86
|
+
counts context reads in the statics for SYG170. Core: +19 B (kanban gzip).
|
|
87
|
+
|
|
88
|
+
- **Per-page Switchable state (PLAN-13 F-3).** `state` takes a map from page names to bindings
|
|
89
|
+
(`state={{ projects: 'pages.projects', settings: byId('teams', state.teamId) }}`): each listed
|
|
90
|
+
page binds its own key, path, lens or byId selection with a tag child's lifetime rules (another
|
|
91
|
+
target makes only that page again; a deleted target disposes it on its last state), and the
|
|
92
|
+
pages not listed share the owner's state. A key that names no page is the new **SYG429** (dev
|
|
93
|
+
checks and `sygnal-check`, with a rename fix). Types infer the page names from `of`. Core:
|
|
94
|
+
+126 B (kanban gzip), leaving 18 B of the 43,000 B budget with F-1.
|
|
95
|
+
|
|
96
|
+
- **Deep-link tests start on their route (PLAN-13 F-2).** `renderComponent(App, { router, url })`
|
|
97
|
+
applies the root's `route` entry's STATE reducer to the initial state with the `url`'s route
|
|
98
|
+
before the first render: `initialState`'s `router.current()` ran at module load, so a test used
|
|
99
|
+
to render `/` first and send its requests. The entry's other sinks (a guard's redirect) still run
|
|
100
|
+
once, when the router answers. Tests only; no core change.
|
|
101
|
+
|
|
102
|
+
- **sygnal-check follows `next` into helpers (PLAN-13 C-3).** A model that passes `next` to a
|
|
103
|
+
function (`advance(state, next)`), imported or in the same file and with any parameter name, has
|
|
104
|
+
that function's `next('X')` calls counted as its own, 3 calls deep: SYG102 sees the targets, and
|
|
105
|
+
SYG117 reports a bad one in the helper's file. A helper it can't read (a package) makes the
|
|
106
|
+
targets unknown, so SYG102 drops to info. The language server re-checks the component when the
|
|
107
|
+
helper changes.
|
|
108
|
+
|
|
109
|
+
- **`replyKeys` in sygnal-check's config (PLAN-13 C-4).** A custom driver's own reply keys
|
|
110
|
+
(`cancelled: 'UP_CANCELLED'`) can be listed in `sygnal-check.config.json`, for every sink
|
|
111
|
+
(`["cancelled"]`) or per sink (`{ "UPLOAD": ["progress", "cancelled"] }`): their actions count for
|
|
112
|
+
SYG102 and are checked by SYG112. Without the setting, a request key the checker doesn't know
|
|
113
|
+
whose value names a model entry makes that entry's SYG102 info, pointing at `replyKeys`.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- **Follow-ups from trying the PLAN-13 build in Temper.** A Switchable page with a per-page
|
|
118
|
+
`state` map no longer reports SYG106 for the Switchable's own `state` prop (every page did, so
|
|
119
|
+
`expectNoDiagnostics()` failed). The `sygnal-check` command line reads `replyKeys` (check,
|
|
120
|
+
`--fix`, `--graph`, `--base`; the API and the language server already did). SSE `resume` replaces
|
|
121
|
+
an id parameter the URL already has instead of adding a second one. An `every` timer ticks once
|
|
122
|
+
per interval under a test clock that freezes `Date.now()` while timers run (Playwright's
|
|
123
|
+
`setFixedTime()`): its drift-free delays had grown by one interval per tick (1, 3, 6, 10 s).
|
|
124
|
+
- **An invalidation no longer resends a resource's old request after a navigation (PLAN-13 R-1).**
|
|
125
|
+
When a write's `ok` action navigated (`ROUTER`) and its `invalidates` (a URL prefix or a tag)
|
|
126
|
+
matched a resource of a lens- or key-bound page, the page's new request was sent and then the
|
|
127
|
+
invalidation sent the old one in the same lane, cancelling the new one: the new page showed the
|
|
128
|
+
old record. A load now stops when the resource's request changed (or it was paused) while it
|
|
129
|
+
wrote its loading state; refresh, polling and focus refetches go through the same path.
|
|
130
|
+
|
|
131
|
+
## 7.0.0 — 2026-10-19
|
|
132
|
+
|
|
133
|
+
**Read [Migrating to 7.0](https://sygnal.js.org/guide/migrating-to-7/) before upgrading.** 7.0
|
|
134
|
+
has one reducer timing contract and no option to switch back; apps that aren't ready stay on 6.x.
|
|
135
|
+
|
|
136
|
+
**Measured impact.** Size: the kanban gate is (a) 42,844 B with `nativeGlobalThis: false` (6.6.0:
|
|
137
|
+
42,495 B; budget raised from 42,700 B to 43,000 B for the binding lifetimes) and (b) 38,834 B by
|
|
138
|
+
default. Most of the growth is the binding-lifetime bookkeeping; the single timing contract is
|
|
139
|
+
smaller than the opt-in switch it replaced (−181 B against the 7.0 candidate). The timing audit is
|
|
140
|
+
in the dev diagnostics entry (0 production bytes), and the upload, SSE, form and `tooltips()`
|
|
141
|
+
features add nothing to an app that doesn't use them. The perf count gate is within its limits.
|
|
142
|
+
|
|
143
|
+
**Companion packages.**
|
|
144
|
+
- **`sygnal-check` 0.11.0:** checks reducer timing and binding changes against the Sygnal version
|
|
145
|
+
installed for each source package (7: every output gets the committed state, changed known
|
|
146
|
+
targets remount; 6: the 6.x contracts). New **SYG256** (an output guard that rejects the state
|
|
147
|
+
its own action writes, in a Sygnal 7 project); **SYG454** (an unkeyed dynamic `byId` selection)
|
|
148
|
+
only for an installed Sygnal 6. SYG731 accepts state announced through text, `aria-label`,
|
|
149
|
+
`aria-live` or `role="status"`; SYG609 also reports intent sources with no driver; `progress`
|
|
150
|
+
counts as a reply-action key (SYG102 / SYG112). Explanations for SYG257 and SYG636. Its caret
|
|
151
|
+
range doesn't reach 0.11.0 from `^0.10.0`, so update the dev dependency.
|
|
152
|
+
- **`create-sygnal-app` 2.9.0:** every template depends on `sygnal` ^7.0.0 and `sygnal-check`
|
|
153
|
+
^0.11.0 and has `npm run check` (TypeScript templates: sygnal-check and `tsc --noEmit`).
|
|
154
|
+
Scaffolding into a directory that holds only `.git` and/or `.DS_Store` keeps that metadata, and
|
|
155
|
+
`.` gets a valid package name from the directory.
|
|
156
|
+
- **VS Code extension 0.8.0:** bundles `sygnal-check` 0.11.0 for Sygnal 7 and existing Sygnal 6
|
|
157
|
+
apps; workspace checkers need 0.11.0 or later (earlier ones fall back to the bundled checker).
|
|
158
|
+
- **Claude Code plugin 0.2.0:** the `sygnal-dev` skill describes Sygnal 7; a project's
|
|
159
|
+
`sygnal-check` older than 0.11.0 is refused only when the project has Sygnal 7.
|
|
160
|
+
|
|
161
|
+
### Breaking: STATE runs first, and output sinks see its result
|
|
162
|
+
|
|
163
|
+
In every model entry STATE now runs first, whatever the key order, and every other sink (EFFECT,
|
|
164
|
+
EVENTS, PARENT, ELEMENT, HTTP and other drivers) gets the committed state, calculated fields
|
|
165
|
+
included, as `state` and `props.state`. The new `props.previousState` (fourth argument) is the
|
|
166
|
+
state from before the action; a STATE reducer gets the old state in both. 6.x gave every reducer
|
|
167
|
+
the state from before the action.
|
|
168
|
+
|
|
169
|
+
- An output that computed the update itself now applies it twice: in
|
|
170
|
+
`INC: { STATE: s => ({ ...s, n: s.n + 1 }), PARENT: s => ({ n: s.n + 1 }) }`, PARENT becomes
|
|
171
|
+
`s => ({ n: s.n })`.
|
|
172
|
+
- A value STATE clears or deletes (a submitted draft, a removed record) is read from
|
|
173
|
+
`previousState`.
|
|
174
|
+
- A guard that admits the action (a loading or status flag STATE sets) tests `previousState`.
|
|
175
|
+
Keep the output guard: STATE returning `ABORT` doesn't cancel the other sinks.
|
|
176
|
+
- When STATE returns `ABORT` or the same state, or throws, the other sinks still run, with
|
|
177
|
+
`state === previousState`.
|
|
178
|
+
- When STATE removes the component's own state (`STATE: () => undefined` in a Collection item, or
|
|
179
|
+
a child bound with `state="x"` / `byId()` deleting its record), the other sinks get its last
|
|
180
|
+
state, so `REMOVE: { STATE: () => undefined, EVENTS: event('REMOVED', (s) => s.id) }` works as
|
|
181
|
+
before.
|
|
182
|
+
- Behaviors: the first argument is the slice after STATE, `props.state` the host state after
|
|
183
|
+
STATE, `props.previousState` the host state before the action; the old slice is
|
|
184
|
+
`props.previousState[key]`. Every built-in behavior (form, chat, compose, commandBar, undo,
|
|
185
|
+
pager, selection, sortable, and the `sygnal/ui` dialog, popover, tooltip, tabs, accordion and
|
|
186
|
+
disclosure) is on this contract.
|
|
187
|
+
- Libraries that support 6 and 7 read old values as `props.previousState ?? state`.
|
|
188
|
+
|
|
189
|
+
### New: the timing audit (SYG257)
|
|
190
|
+
|
|
191
|
+
An opt-in dev check for upgrading 6.x code: with `renderComponent(C, { timingAudit: true })`,
|
|
192
|
+
`sygnal({ diagnostics: { timingAudit: true } })` in `vite.config`, `configureTimingAudit(true)`
|
|
193
|
+
from `'sygnal/diagnostics'` or `globalThis.__SYGNAL_TIMING_AUDIT__ = true`, every output sink that
|
|
194
|
+
reads a top-level state field its action's STATE changed reports **SYG257** (info, once per
|
|
195
|
+
component, action, sink and field). Keep `state` where the new value is meant; read
|
|
196
|
+
`previousState` where the old one was. It is off by default because in code written for 7.0
|
|
197
|
+
reading the new value is the intent, and it adds nothing to production builds.
|
|
198
|
+
|
|
199
|
+
sygnal-check's **SYG256** (warning) reports an output guard that contradicts the same action's
|
|
200
|
+
literal STATE write, the loading/admission pitfall above. sygnal-check reads the installed
|
|
201
|
+
Sygnal version per package; **SYG454** (a `byId()` selection without a key) applies only to
|
|
202
|
+
Sygnal 6.
|
|
203
|
+
|
|
204
|
+
### Breaking: a child follows the record its state binding points at
|
|
205
|
+
|
|
206
|
+
When a child's `state` path, or its `byId()` field, id or idKey, changes, Sygnal disposes the old
|
|
207
|
+
child and mounts a new one: `DISPOSE` runs with the last state, pending requests, delayed `next()`
|
|
208
|
+
actions and subscriptions are cancelled, initialization reruns and local state starts from its
|
|
209
|
+
defaults. The child's DOM is replaced, so focus, caret, scroll position and in-progress input
|
|
210
|
+
inside it are lost. The same applies to Collection/VirtualCollection `from` and Switchable
|
|
211
|
+
`state` (all items or pages, hidden ones included, even when ids overlap). Deleting the bound
|
|
212
|
+
record disposes the child too, after the deleting action's outputs; re-adding the same id gets a
|
|
213
|
+
new child, never the old pending work. A new `byId()` call with the same arguments, field edits
|
|
214
|
+
and reordering keep the child. `key={selected.id}` is no longer needed for a selection.
|
|
215
|
+
|
|
216
|
+
**A stable explicit key no longer keeps a child across a target change**: lift state that must
|
|
217
|
+
survive the change into the parent. A child with a custom lens keeps its binding until its key
|
|
218
|
+
changes.
|
|
219
|
+
|
|
220
|
+
### New: uploads, SSE auth, form files and types, `tooltips()`
|
|
221
|
+
|
|
222
|
+
- **Upload and download progress** on `makeFetchDriver` requests: `progress: 'UPLOADING'` (a reply
|
|
223
|
+
action) or `progress: true` (`HTTP.progress(category)`) sends `{ phase, loaded, total, fraction }`
|
|
224
|
+
steps, throttled to one every 50 ms per phase. Such a request goes over XMLHttpRequest, the only
|
|
225
|
+
way to see upload progress in every browser; fetch-only `init` options it can't honour are
|
|
226
|
+
SYG636. Tests: `t.progress('HTTP', { loaded, total }, 'UPLOADING')`.
|
|
227
|
+
- **`multipart: { file, title }`** on a request: the multipart/form-data body as data, the way
|
|
228
|
+
`json` is; a model never builds a `FormData`.
|
|
229
|
+
- **The `form` behavior** takes file inputs (a field's value is its `File`, `multiple` an array,
|
|
230
|
+
validated by the schema), keeps the submit's progress in `state.form.progress`
|
|
231
|
+
(`progress: 'form.PROGRESS'`) and counts start-overs in `state.form.resets` (key a file input
|
|
232
|
+
with it). `request: { url }` (or `(data, state) => request`) sends a valid submit itself, as
|
|
233
|
+
JSON or multipart, with `form.DONE` / `form.ERRORS` wired: the common save needs no model entry.
|
|
234
|
+
`form.RESET` from a click goes back to the start values (it used to take the event as values).
|
|
235
|
+
- **Form types from the schema**: a typed Standard Schema (zod, valibot, arktype) types
|
|
236
|
+
`state.form.values` and `fields.x.value`; `FormOutput<typeof uses.form>` is the submit data.
|
|
237
|
+
- **SSE connections take `headers`** (`{ authorization: 'Bearer ' + state.token }`): such a stream
|
|
238
|
+
is read with fetch (`Last-Event-ID`, `retry`), since EventSource can't send headers.
|
|
239
|
+
- **`tooltips()`** in `sygnal/ui`: one behavior and one state slice for every tooltip of a
|
|
240
|
+
component (glossary terms, icon toolbars), one tip at a time.
|
|
241
|
+
- `Component<State, any>` keeps `state` typed; SYG731 accepts state announced through text,
|
|
242
|
+
`aria-label`, `aria-live` or `role="status"`; sygnal-check's SYG609 also reports intent sources
|
|
243
|
+
with no driver; a test wait that times out on the fake clock names the pending timer; every
|
|
244
|
+
create-sygnal-app template has `npm run check` (TypeScript: sygnal-check and `tsc --noEmit`).
|
|
245
|
+
|
|
246
|
+
### Also included
|
|
247
|
+
|
|
248
|
+
- A child that fails to instantiate is no longer retried on every parent render.
|
|
249
|
+
- State-wait timeout messages report queued simulated inputs; the testing guide separates
|
|
250
|
+
serialized simulated input from synchronous native bursts.
|
|
251
|
+
- TypeScript tests can use `t.query<HTMLInputElement>(selector)` and `t.queryAll<…>(selector)`
|
|
252
|
+
(type assertions, not runtime checks); typed control tokens keep their inference.
|
|
253
|
+
- The combined Rollup build exits normally (compiler watcher lifecycle fix).
|
|
254
|
+
- The release workflow checks the CHANGELOG headings of the new versions before the gates and
|
|
255
|
+
any publish.
|
|
256
|
+
|
|
257
|
+
### Changed
|
|
258
|
+
|
|
259
|
+
- **The app graph's banner line says why it's missing.** When the dev server's graph route is on but the project has no `sygnal-check` 0.10.0 or later, `sygnal/vite` prints `➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)` instead of nothing (the URL already answered 404 with the same hint).
|
|
260
|
+
- **`astro dev` prints the graph page's address** under Astro's own banner (Astro never runs Vite's URL printing), and the Astro integration takes the `graph` option (`sygnal({ graph: false })`).
|
|
261
|
+
- **The repository's examples link `sygnal-check`** (`file:../../sygnal-check`, as they link `sygnal`), so `npm run dev` in any of them serves the app graph; `examples/mcp-app` moves off `sygnal-check` ^0.7.0.
|
|
262
|
+
|
|
5
263
|
## 6.6.0 — 2026-10-16
|
|
6
264
|
|
|
7
265
|
The app graph as an interactive page: `sygnal-check --graph=html`, live with `--watch` or at `/__sygnal/graph` in the Vite dev server, in a VS Code panel and in Sygnal DevTools. See [The App Graph](https://sygnal.js.org/guide/app-graph/).
|
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ A reactive component framework with pure functions, zero side effects, and autom
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
> **Upgrading from 6.x?** Sygnal 7.0 changes reducer timing: output sinks (EFFECT, EVENTS, PARENT, HTTP, ...) now see the state after the action's STATE update. See [Migrating to 7.0](https://sygnal.js.org/guide/migrating-to-7/).
|
|
13
|
+
|
|
12
14
|
## Why Sygnal?
|
|
13
15
|
|
|
14
16
|
- **Pure components** — Views are plain functions. All side effects are handled by drivers, outside your code.
|
package/dist/astro/index.cjs.js
CHANGED
|
@@ -867,17 +867,25 @@ function graphMiddleware(server, root, engine, ping = 25000) {
|
|
|
867
867
|
res.end(req.method === 'HEAD' ? undefined : body);
|
|
868
868
|
};
|
|
869
869
|
}
|
|
870
|
-
/**
|
|
871
|
-
|
|
870
|
+
/** The banner line when the project has no sygnal-check with the graph page (it used to print nothing). */
|
|
871
|
+
const GRAPH_MISSING_LINE = ' ➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)';
|
|
872
|
+
/**
|
|
873
|
+
* Adds the page's URL under Vite's own when the dev server prints them, or, when the project's
|
|
874
|
+
* sygnal-check is missing or too old for the page (`available` false), a line saying what to install.
|
|
875
|
+
*/
|
|
876
|
+
function printGraphUrl(server, available = true) {
|
|
872
877
|
const print = server?.printUrls;
|
|
873
878
|
if (typeof print !== 'function')
|
|
874
879
|
return;
|
|
875
880
|
server.printUrls = function (...args) {
|
|
876
881
|
const out = print.apply(this, args);
|
|
877
882
|
try {
|
|
883
|
+
const logger = server.config?.logger || console;
|
|
878
884
|
const local = server.resolvedUrls?.local?.[0];
|
|
879
|
-
if (
|
|
880
|
-
|
|
885
|
+
if (!available)
|
|
886
|
+
logger.info(GRAPH_MISSING_LINE);
|
|
887
|
+
else if (local)
|
|
888
|
+
logger.info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`);
|
|
881
889
|
}
|
|
882
890
|
catch (_) { }
|
|
883
891
|
return out;
|
|
@@ -1016,7 +1024,8 @@ function printGraphUrl(server) {
|
|
|
1016
1024
|
* skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
|
|
1017
1025
|
* builds it with createLiveProject on start and after each change: one build gives the page,
|
|
1018
1026
|
* the check overlay / console findings of 5, and the component stamps. Its URL is printed
|
|
1019
|
-
* under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"
|
|
1027
|
+
* under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"; without a sygnal-check
|
|
1028
|
+
* 0.10.0+ in the project, a line saying so instead). The modules of the
|
|
1020
1029
|
* scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
|
|
1021
1030
|
* key, D521) on each top-level component function, which the runtime inspect() reports as
|
|
1022
1031
|
* `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
|
|
@@ -1084,7 +1093,7 @@ function sygnal(options = {}) {
|
|
|
1084
1093
|
// Files whose `import ... from 'sygnal'` resolves to the run() wrapper
|
|
1085
1094
|
const runtimeImporters = new Set();
|
|
1086
1095
|
// Statements run before the app starts: dev flag (+ strict flag)
|
|
1087
|
-
const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '');
|
|
1096
|
+
const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '') + (diagnostics.timingAudit ? AUDIT_FLAG : '');
|
|
1088
1097
|
const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`;
|
|
1089
1098
|
// D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
|
|
1090
1099
|
// E-1: the MCP endpoint needs the bridge, so `mcp` turns it on
|
|
@@ -1282,8 +1291,7 @@ function sygnal(options = {}) {
|
|
|
1282
1291
|
// W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
|
|
1283
1292
|
if (graphOn && server?.middlewares) {
|
|
1284
1293
|
server.middlewares.use(graphMiddleware(server, root, engine));
|
|
1285
|
-
|
|
1286
|
-
printGraphUrl(server);
|
|
1294
|
+
printGraphUrl(server, graphAvailable(root));
|
|
1287
1295
|
}
|
|
1288
1296
|
},
|
|
1289
1297
|
transform(code, id, opts) {
|
|
@@ -1568,7 +1576,7 @@ function normalizeDiagnostics(value) {
|
|
|
1568
1576
|
if (!MODES.includes(mode)) {
|
|
1569
1577
|
throw new Error(`[vite-plugin-sygnal] diagnostics mode must be one of ${MODES.map(m => `'${m}'`).join(', ')}; got ${JSON.stringify(mode)}`);
|
|
1570
1578
|
}
|
|
1571
|
-
return { mode, strict: !!o.strict, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
|
|
1579
|
+
return { mode, strict: !!o.strict, timingAudit: !!o.timingAudit, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
|
|
1572
1580
|
}
|
|
1573
1581
|
/** Module id without a query or hash. */
|
|
1574
1582
|
function cleanId(id) {
|
|
@@ -1946,6 +1954,8 @@ function startChecker(server, root, mod, setup, opts, engine) {
|
|
|
1946
1954
|
}
|
|
1947
1955
|
const DEV_FLAG = 'if (globalThis.__SYGNAL_DEV__ === undefined) globalThis.__SYGNAL_DEV__ = true;';
|
|
1948
1956
|
const STRICT_FLAG = 'if (globalThis.__SYGNAL_STRICT__ === undefined) globalThis.__SYGNAL_STRICT__ = true;';
|
|
1957
|
+
// 7.0: the opt-in timing audit (SYG257, 'sygnal/diagnostics' checks/timingAudit.ts)
|
|
1958
|
+
const AUDIT_FLAG = 'if (globalThis.__SYGNAL_TIMING_AUDIT__ === undefined) globalThis.__SYGNAL_TIMING_AUDIT__ = true;';
|
|
1949
1959
|
// A top-level (column 0) run(Component, ...) statement: bare, or declaring a
|
|
1950
1960
|
// new binding. Assignments to existing variables, nested calls and indented
|
|
1951
1961
|
// calls (e.g. inside a function or test callback) do not match (B-007). It is
|
|
@@ -2084,10 +2094,12 @@ const rootDir = (root) =>
|
|
|
2084
2094
|
// @ts-ignore — Node's process
|
|
2085
2095
|
!root ? process.cwd() : typeof root === 'string' ? root : node_url.fileURLToPath(root);
|
|
2086
2096
|
function sygnalAstroIntegration(options = {}) {
|
|
2097
|
+
let projectRoot;
|
|
2087
2098
|
return {
|
|
2088
2099
|
name: SYGNAL_RENDERER_NAME,
|
|
2089
2100
|
hooks: {
|
|
2090
2101
|
'astro:config:setup': ({ addRenderer, updateConfig, command, config }) => {
|
|
2102
|
+
projectRoot = rootDir(config?.root);
|
|
2091
2103
|
addRenderer({
|
|
2092
2104
|
name: SYGNAL_RENDERER_NAME,
|
|
2093
2105
|
clientEntrypoint: 'sygnal/astro/client',
|
|
@@ -2117,6 +2129,7 @@ function sygnalAstroIntegration(options = {}) {
|
|
|
2117
2129
|
check: options.check,
|
|
2118
2130
|
nativeGlobalThis: options.nativeGlobalThis,
|
|
2119
2131
|
devtools: options.devtools,
|
|
2132
|
+
graph: options.graph,
|
|
2120
2133
|
})];
|
|
2121
2134
|
}
|
|
2122
2135
|
else if (options.nativeGlobalThis !== false) {
|
|
@@ -2129,6 +2142,16 @@ function sygnalAstroIntegration(options = {}) {
|
|
|
2129
2142
|
vite.plugins = [...(vite.plugins || []), onErrorPlugin(hook)];
|
|
2130
2143
|
updateConfig({ vite });
|
|
2131
2144
|
},
|
|
2145
|
+
// W-2: Astro prints its own banner (Vite's printUrls never runs), so the graph page's line goes here
|
|
2146
|
+
'astro:server:start': ({ address, logger }) => {
|
|
2147
|
+
if (options.graph === false || !projectRoot)
|
|
2148
|
+
return;
|
|
2149
|
+
const print = (msg) => (logger ? logger.info(msg.replace(/^\s*➜\s*/, '')) : console.log(msg));
|
|
2150
|
+
if (!graphAvailable(projectRoot))
|
|
2151
|
+
print(GRAPH_MISSING_LINE);
|
|
2152
|
+
else if (address?.port)
|
|
2153
|
+
print(` ➜ Sygnal graph: http://localhost:${address.port}${GRAPH_PATH}`);
|
|
2154
|
+
},
|
|
2132
2155
|
},
|
|
2133
2156
|
};
|
|
2134
2157
|
}
|