sygnal 6.4.0 → 6.6.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 +64 -0
- package/dist/astro/index.cjs.js +398 -25
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +398 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3 -2
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -2
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +141 -3
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +141 -4
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +140 -21
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +140 -21
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/index.cjs.js +54 -24
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +139 -16
- package/dist/index.esm.js +54 -24
- 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/vike/config/package.json +1 -1
- package/dist/vike/onRenderClient.cjs.js +63 -43
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +63 -43
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +21 -25
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +21 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +398 -25
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +398 -25
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/core/cell.ts +25 -7
- package/src/core/hooks.ts +6 -0
- package/src/core/hosts/collection.ts +6 -11
- package/src/core/hosts/switchable.ts +2 -2
- package/src/core/instance.ts +2 -2
- package/src/core/view.ts +2 -0
- package/src/cycle/dom/utils.ts +4 -2
- package/src/devtools.d.ts +13 -0
- package/src/devtools.ts +3 -0
- package/src/extra/devGraph.ts +111 -0
- package/src/extra/devMcp.ts +1 -1
- package/src/extra/devtools.ts +18 -0
- package/src/extra/diagnostics/checks/collections.ts +31 -25
- package/src/extra/diagnostics/checks/index.ts +1 -1
- package/src/extra/diagnostics/checks/inspect.ts +68 -6
- package/src/extra/diagnostics/checks/next.ts +22 -0
- package/src/extra/diagnostics/checks/public.d.ts +121 -12
- package/src/extra/diagnostics/codes.ts +10 -0
- package/src/index.d.ts +19 -3
- package/src/vike/onRenderClient.ts +44 -46
- package/src/vike/onRenderHtml.ts +5 -26
- package/src/vike/shell.ts +33 -0
- package/src/vite/graph.ts +247 -0
- package/src/vite/mcp.ts +3 -3
- package/src/vite/plugin.d.ts +12 -0
- package/src/vite/plugin.ts +136 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,70 @@
|
|
|
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
|
+
## 6.6.0 — 2026-10-16
|
|
6
|
+
|
|
7
|
+
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/).
|
|
8
|
+
|
|
9
|
+
**Measured impact.** Size: the kanban gate stays flat, (a) 42,495 B with `nativeGlobalThis: false` (6.5.0: 42,496 B; budget 42,700 B): the runtime `contextConsumes` getter costs 9 B in the core, and nothing else in this release reaches an app bundle (the graph page, the Vite route, the dev stamps and the DevTools bridge are dev-only; a `vite build` contains none of them). The perf count gate is within its limits.
|
|
10
|
+
|
|
11
|
+
**Companion packages.**
|
|
12
|
+
- **`sygnal-check` 0.10.0:** `--graph=html` writes the graph as one self-contained page (no network, runs no app code) and opens it: cards for every component, render / Collection / Switchable / Vike layout edges, EVENTS, context and `PARENT` overlays, a health list, a detail panel with line links into the editor (`--editor`), traces of events, context fields, actions and state keys, search, filters, an EVENTS table, SVG / JSON / Mermaid export, and directory groups folded above 150 components. `--graph=html --watch` serves it on 127.0.0.1 behind a token and updates it on every save (`--poll`, `--port`). `--graph=mermaid` prints a flowchart (`--events=false`). `--out`, `--open` / `--no-open`. `--graph[=<format>] --base <ref>` compares the graph with a git ref without a checkout (`--graph --json --base` prints the diff for CI). The graph gains `key`, `entry`, `viewOnly`, Vike `route`s and layout edges, child `id` / `external` / `widget` / `state`, `childSelects`, `emitterIds` / `selectorIds`, `writes` / `removes` on actions, `stateReads`, and source lines on actions, selectors, children, context entries and EVENTS sites, in `--graph --json`, the MCP `graph` tool and the language server's new `sygnal/graph` request. New rules **SYG170** (a context field no provider above gives) and **SYG171** (`PARENT` that no parent selects). Its caret range doesn't reach 0.10.0 from `^0.9.0`, so update the dev dependency; `sygnal/vite`'s graph route needs 0.10.0 or later.
|
|
13
|
+
- **`create-sygnal-app` 2.8.0:** every template depends on `sygnal` ^6.6.0 and `sygnal-check` ^0.10.0.
|
|
14
|
+
- **VS Code extension 0.7.0:** **Sygnal: Show App Graph**, the page in a panel that updates as you type; bundles `sygnal-check` 0.10.0.
|
|
15
|
+
- **Claude Code plugin 0.1.5:** the `sygnal-dev` skill mentions the graph page and `--graph --json`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **The app graph in the Vite dev server.** `sygnal/vite` serves the live graph page at `/__sygnal/graph` (local requests only) when `sygnal-check` 0.10.0 or later is installed, and prints its address. In dev it also stamps each component function with its static key (a non-enumerable `__sygnalKey`, reported by `inspect()` as `key`), so the page's **Live data** shows which components are mounted, their instance counts, a flash when an action runs and each instance's state, from the app tabs of the same origin. New option `graph` (default `true`); `graph: false` turns off the route, the stamps and the bridge. `vite build`, `vite preview` and Vitest get none of it.
|
|
20
|
+
- **`installGraphBridge()`** in `sygnal/devtools`: connects the page's apps to the graph page's live data over a same-origin `BroadcastChannel` (`sygnal:graph`). The Vite plugin calls it in dev.
|
|
21
|
+
- **App graph in the DevTools panel.** An **App graph** button draws the running app's `inspect()` graph with the same page: a card per component under each parent (`×N` for several instances), each app as its own group, action flashes and instance state.
|
|
22
|
+
- **More in the runtime `inspect()`** (the `sygnal/diagnostics` dev entry): `key`, `entry`, `viewOnly`, `app`, `shown`, `children[].ids`, `events[type].emitterIds` / `selectorIds`, the time of each diagnostic (`timestamp`), and `contextConsumes`: the context fields the component's latest render read (a read-only getter in the core, 9 B gzip). The `InspectGraph` type gains these and the static graph's new fields, all optional.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A Transition's leave, or a Portal mounted late, under jsdom.** The DOM "poke" that re-emits the app's DOM source after such a change created its event with the global `Event`. jsdom refuses an event from another realm, so a jsdom test whose environment was torn down while a leave transition's timer was pending failed with "parameter 1 is not of type 'Event'" (the 6.5.0 release's CI run did once). It now creates the event from the element's own window. Browsers were not affected.
|
|
27
|
+
- **Nested Vike Layouts and Wrappers (`sygnal/vike`).** Vike lists a cumulative config's values closest to the page first, so a page under `pages/admin/` gets `[adminLayout, rootLayout]`. `sygnal/vike` treated the first as the outermost, so the admin Layout rendered outside the root Layout, on the server and on hydration. Client-side navigation also kept the first page's Layouts and Wrappers, so going from `/` to `/admin` never added the admin Layout (nor did going back remove it), and navigating from a page with no Layout to one with a Layout swapped the Page into the old app. Now the root's Layout is outermost and every Wrapper is outside every Layout, as in `vike-react`. Navigation keeps mounted, with their state, the Layouts and Wrappers that two pages share (from the outside in), adds the new page's others and removes the ones it lacks. A page with one Layout and one Wrapper renders and hydrates as before. The Vike example has an `/admin` page with a nested Layout.
|
|
28
|
+
|
|
29
|
+
## 6.5.0 — 2026-10-14
|
|
30
|
+
|
|
31
|
+
State paths: a child's `state`, a Switchable's `state` and a Collection's `from` reach nested state (`state="settings.rules"`, `from="board.cards"`) without a hand-written lens. Additive, except that a `state` / `from` string with a `.` or `[` now means a path (SYG428 reports a state that still has a key with a dot in its name). The first full release published by the keyless release workflow.
|
|
32
|
+
|
|
33
|
+
**Measured impact.** Size: the kanban gate goes down, (a) 42,496 B with `nativeGlobalThis: false` (was 42,527 B; budget 42,700 B): the path cell adds about 107 B, and moving the string-`from` SYG401 checks out of the core saves more than that.
|
|
34
|
+
|
|
35
|
+
**Companion packages.**
|
|
36
|
+
- **`sygnal-check` 0.9.0:** SYG401 follows a `from` path through the literal `initialState`; rename, definition and references treat a path's first segment as a use of that state key. Its caret range doesn't reach 0.9.0 from `^0.8.0`, so update the dev dependency.
|
|
37
|
+
- **`create-sygnal-app` 2.7.0:** every template depends on `sygnal` ^6.5.0 and `sygnal-check` ^0.9.0.
|
|
38
|
+
- **VS Code extension 0.6.0:** bundles `sygnal-check` 0.9.0. The extension itself doesn't change.
|
|
39
|
+
- **Claude Code plugin 0.1.4:** the `sygnal-dev` skill describes state paths.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **State paths.** A child's `state`, a Switchable's `state` and a Collection's (and VirtualCollection's) `from` take a path into nested state: `<RulesPanel state="settings.rules" />`, `<BoardTitle state="boards[0].title" />`, `<Collection of={Card} from="board.cards" />`. Reads follow the path; a write copies only the objects and arrays on it, so untouched branches keep their identity, as a hand-written lens would (a missing level is created as an object when an `isolatedState` child first writes). A calculated first segment is read-only (SYG409), as a calculated key is. For list items, `byId('boards', id)` stays the way to follow an item rather than a position. Typed Collections accept dotted paths to nested arrays (`ArrayPathsOf<STATE>`, three levels). There is no array form (`state={['a', 'b']}`): an array `state` / `from` is still the data passed by mistake (SYG410 / SYG412).
|
|
44
|
+
- **New SYG428** (warn, dev diagnostics): a `state="a.b"` or `from="a.b"` whose owner's state has a key named `"a.b"` (see Changed).
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **A `state` / `from` string with a `.` or `[` is now a path.** Before, `state="a.b"` meant the key named `"a.b"`; now it means `state.a.b`. A state that still has a key with a dot in its name is reported as SYG428 in development; keep it with a lens (`state={{ get: (s) => s['a.b'], set: (s, v) => ({ ...s, 'a.b': v }) }}`) or rename the key.
|
|
49
|
+
- **SYG401 for a string `from` is reported by the development diagnostics only** (`sygnal/diagnostics`, which `sygnal/vite` loads in dev and `renderComponent` in tests): a `from` key or path that isn't in state, or isn't an array. Production builds no longer carry or print it (D22 had the core report it in every mode); the core still reports a lens getter that returns a non-array (SYG401) and an invalid `from` (SYG412). The kanban gate goes down 31 B (42,527 → 42,496 B) with state paths included.
|
|
50
|
+
- **`sygnal-check`: SYG401 follows a `from` path** through the literal `initialState` (a missing key along the way names the keys of that level; it stops where a value isn't a literal), and the editor features (rename, go to definition, references) treat a path's first segment as a use of that state key, so renaming `board` edits `from="board.cards"` to `from="deck.cards"`.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **State guide:** a child bound to a property that is `undefined` renders nothing until the property exists (only an `isolatedState` child renders and creates it); the guide said the property "gets added when the child first updates".
|
|
55
|
+
|
|
56
|
+
## VS Code extension 0.5.1 — 2026-10-14
|
|
57
|
+
|
|
58
|
+
Bundles `sygnal-check` 0.8.1 (the SYG427 fixes above); the extension's own code doesn't change. The first extension release published by the keyless release workflow: the VS Code Marketplace through Microsoft Entra ID, Open VSX through trusted publishing.
|
|
59
|
+
|
|
60
|
+
## sygnal-check 0.8.1 — 2026-10-14
|
|
61
|
+
|
|
62
|
+
A patch for SYG427 (new in 0.8.0), reported from a real project; `sygnal` itself is unchanged (6.4.0). Projects on `sygnal-check` ^0.8.0 get it on their next install. The first release published by the keyless release workflow (npm trusted publishing, with provenance).
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **SYG427 is filed under the component that has the mistake.** It was labelled with the imported component (`RuleForm: RuleForm is imported from …`), which may not exist at all (the import is `undefined`), while every other finding in the file, and the runtime SYG427 for the same mistake, names the component whose view or intent uses it. It now names that component (`RulesPanel: …`), or the file outside any component; the imported name stays in the message and `data.name`.
|
|
67
|
+
- **SYG427's fix says how the module does export the component.** A named import of a module that only has a default export (`import { RuleForm } from './RuleForm.jsx'` for `export default RuleForm`) now gets `'./RuleForm.jsx' exports a default: import RuleForm from './RuleForm.jsx'`, and a default import of a named export the reverse, instead of "export it from …". The remaining case reads `export it from '…' with export function X, or fix the name`, without the parenthetical that nested inside the formatter's parentheses.
|
|
68
|
+
|
|
5
69
|
## 6.4.0 — 2026-10-14
|
|
6
70
|
|
|
7
71
|
Fixes for issues an agent hit while building a real app on 6.3.0, and a step-by-step chat tutorial in the AI showcase. Additive: an `event()` payload function may now return `ABORT`, `makeServiceWorkerDriver` takes `enabled`, and a new diagnostic code (SYG427) reports a `CHILD.select()` of something that isn't a component. Dev-time checks get fewer false positives (SYG103, SYG223).
|
package/dist/astro/index.cjs.js
CHANGED
|
@@ -357,11 +357,11 @@ function inAllowed(h, allowed) {
|
|
|
357
357
|
return e.startsWith('.') ? h === e.slice(1) || h.endsWith(e) : h === e;
|
|
358
358
|
});
|
|
359
359
|
}
|
|
360
|
-
/** Why the request is refused (403), or null */
|
|
361
|
-
function refusal(req, allowedHosts) {
|
|
360
|
+
/** Why the request is refused (403), or null. PLAN-10 W-2: the graph route (./graph.ts) uses the same guard */
|
|
361
|
+
function refusal(req, allowedHosts, what = 'the Sygnal MCP endpoint') {
|
|
362
362
|
const remote = String(req?.socket?.remoteAddress || '');
|
|
363
363
|
if (!LOOPBACK_IP.test(remote))
|
|
364
|
-
return
|
|
364
|
+
return `${what} only answers requests from this machine (not ${remote || 'an unknown address'})`;
|
|
365
365
|
const host = req?.headers?.host;
|
|
366
366
|
if (typeof host !== 'string' || !host)
|
|
367
367
|
return 'missing Host header';
|
|
@@ -623,6 +623,267 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
|
|
|
623
623
|
`;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
/**
|
|
627
|
+
* PLAN-10 W-2 / P-2: the live app graph page of sygnal/vite's dev server (D543, D544).
|
|
628
|
+
*
|
|
629
|
+
* `/__sygnal/graph` serves sygnal-check's graph page (`graphHtml`, the same file `sygnal-check
|
|
630
|
+
* --graph=html --watch` serves) in live mode, and `/__sygnal/graph/events` its SSE stream: the
|
|
631
|
+
* whole graph (`event: graph`, `id: <version>`) on connect (unless `Last-Event-ID` already has it)
|
|
632
|
+
* and after each build whose graph changed, a `: ping` comment every 25 s. Dev server only.
|
|
633
|
+
*
|
|
634
|
+
* One live project (sygnal-check's createLiveProject, W-1) feeds the page, the check overlay and
|
|
635
|
+
* the console (the plugin's `check`): each build gives the graph and the diagnostics, so they agree.
|
|
636
|
+
* It builds on start and 100 ms after the last source or config file change Vite's watcher reports.
|
|
637
|
+
*
|
|
638
|
+
* P-2: each build also gives the top-level binding and static key (D521: 'file#name', '#2' for a
|
|
639
|
+
* second of the same name in a file) of every component in a scanned file (sygnal-check's
|
|
640
|
+
* componentStamps); the dev transform appends to those modules a tail that sets a non-enumerable
|
|
641
|
+
* `__sygnalKey` on each component function, and the runtime inspect() reports it as `key`. A
|
|
642
|
+
* module whose text changed since the last build is built again first, so a stamp is never stale.
|
|
643
|
+
*
|
|
644
|
+
* The page is served on the app's origin: its "Live data" (graphView/liveData.js) joins the open
|
|
645
|
+
* app tabs over a BroadcastChannel (src/extra/devGraph.ts), with no server in between.
|
|
646
|
+
*
|
|
647
|
+
* Security: the same guard as `/__sygnal/mcp` (./mcp.ts refusal(): a loopback peer, a local Host
|
|
648
|
+
* (or one in `server.allowedHosts`), a local Origin when there is one); GET and HEAD only; the page
|
|
649
|
+
* gets `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`,
|
|
650
|
+
* `X-Frame-Options: DENY` and a CSP (inline style and script, `connect-src 'self'`, no framing).
|
|
651
|
+
* Read only: it serves the page and the stream, nothing else.
|
|
652
|
+
*/
|
|
653
|
+
// @ts-ignore
|
|
654
|
+
const GRAPH_PATH = '/__sygnal/graph';
|
|
655
|
+
const GRAPH_EVENTS = GRAPH_PATH + '/events';
|
|
656
|
+
const GRAPH_CLIENT = 'virtual:sygnal/graph';
|
|
657
|
+
const GRAPH_CHANNEL = 'sygnal:graph';
|
|
658
|
+
/** The property the dev transform sets on component functions (read by inspect()) */
|
|
659
|
+
const STAMP_PROP = '__sygnalKey';
|
|
660
|
+
const SOURCE_RE$1 = /\.[cm]?[jt]sx?$/;
|
|
661
|
+
const CONFIG_FILES$1 = ['sygnal-check.config.json', 'package.json'];
|
|
662
|
+
const EMPTY = { version: 1, source: 'static', components: [], events: {}, diagnostics: [] };
|
|
663
|
+
const PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data: blob:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
664
|
+
const SECURITY = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'X-Frame-Options': 'DENY' };
|
|
665
|
+
const posix = (p) => p.split(path.sep).join('/');
|
|
666
|
+
/** Whether the project's sygnal-check has the live graph page with "Live data" (checked without loading it). */
|
|
667
|
+
function graphAvailable(root) {
|
|
668
|
+
try {
|
|
669
|
+
const main = node_module.createRequire(path.join(root, 'package.json')).resolve('sygnal-check');
|
|
670
|
+
return fs.existsSync(path.join(path.dirname(main), 'graphView', 'liveData.js'));
|
|
671
|
+
}
|
|
672
|
+
catch (_) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* The live project behind the page and the checker. `settings` are live.js's (paths = the check's
|
|
678
|
+
* include, strict / a11y / ignore / defaults as the checker resolves them).
|
|
679
|
+
*/
|
|
680
|
+
function createEngine(mod, root, settings, logger, stamps) {
|
|
681
|
+
const live = mod.createLiveProject({ root });
|
|
682
|
+
const listeners = [];
|
|
683
|
+
let last = null;
|
|
684
|
+
// the stamps by the live project's path of each file; `known` maps a module path as Vite gives it
|
|
685
|
+
// (posix, possibly through a symlink) to that path
|
|
686
|
+
let stampsByFile = new Map();
|
|
687
|
+
let known = new Map();
|
|
688
|
+
let failed = '';
|
|
689
|
+
const real = (f) => { try {
|
|
690
|
+
return posix(fs.realpathSync(f));
|
|
691
|
+
}
|
|
692
|
+
catch (_) {
|
|
693
|
+
return posix(f);
|
|
694
|
+
} };
|
|
695
|
+
const build = () => {
|
|
696
|
+
let b;
|
|
697
|
+
try {
|
|
698
|
+
b = live.build({ settings, stamps });
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
const msg = String(err?.message || err);
|
|
702
|
+
if (msg !== failed)
|
|
703
|
+
logger.warn(`[sygnal] sygnal-check failed: ${msg}`, { timestamp: true });
|
|
704
|
+
failed = msg;
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
failed = '';
|
|
708
|
+
if (!b.graph)
|
|
709
|
+
b = { ...b, json: JSON.stringify(EMPTY) };
|
|
710
|
+
last = b;
|
|
711
|
+
stampsByFile = b.stamps || new Map();
|
|
712
|
+
known = new Map();
|
|
713
|
+
for (const f of b.files || []) {
|
|
714
|
+
known.set(posix(f), f);
|
|
715
|
+
known.set(real(f), f);
|
|
716
|
+
}
|
|
717
|
+
for (const fn of listeners) {
|
|
718
|
+
try {
|
|
719
|
+
fn(b);
|
|
720
|
+
}
|
|
721
|
+
catch (_) { /* a listener never stops the others */ }
|
|
722
|
+
}
|
|
723
|
+
return b;
|
|
724
|
+
};
|
|
725
|
+
const stampsFor = (file) => {
|
|
726
|
+
if (!last)
|
|
727
|
+
return null;
|
|
728
|
+
const own = known.get(posix(file)) ?? known.get(real(file));
|
|
729
|
+
if (!own)
|
|
730
|
+
return null;
|
|
731
|
+
let text = null;
|
|
732
|
+
try {
|
|
733
|
+
text = fs.readFileSync(own, 'utf8');
|
|
734
|
+
}
|
|
735
|
+
catch (_) { }
|
|
736
|
+
const entry = live.cache?.get?.(own);
|
|
737
|
+
// changed since the last build (Vite transforms it before the watcher's debounced build)
|
|
738
|
+
if (text !== null && (!entry || entry.text !== text))
|
|
739
|
+
build();
|
|
740
|
+
return stampsByFile.get(own) || null;
|
|
741
|
+
};
|
|
742
|
+
let timer;
|
|
743
|
+
const watch = (server) => {
|
|
744
|
+
const onChange = (file) => {
|
|
745
|
+
if (String(file).split(/[\\/]/).includes('node_modules'))
|
|
746
|
+
return;
|
|
747
|
+
if (!SOURCE_RE$1.test(file) && !CONFIG_FILES$1.includes(path.basename(file)))
|
|
748
|
+
return;
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
timer = setTimeout(build, 100);
|
|
751
|
+
};
|
|
752
|
+
const watcher = server?.watcher;
|
|
753
|
+
if (watcher && typeof watcher.on === 'function') {
|
|
754
|
+
watcher.on('change', onChange);
|
|
755
|
+
watcher.on('add', onChange);
|
|
756
|
+
watcher.on('unlink', onChange);
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
server.httpServer?.once?.('close', () => clearTimeout(timer));
|
|
760
|
+
}
|
|
761
|
+
catch (_) { }
|
|
762
|
+
};
|
|
763
|
+
return {
|
|
764
|
+
build, stampsFor, watch, mod,
|
|
765
|
+
get last() { return last; },
|
|
766
|
+
onBuild(fn) { listeners.push(fn); },
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/** The tail the dev transform appends to a module: a non-enumerable key on each component function. */
|
|
770
|
+
function stampTail(list) {
|
|
771
|
+
const ok = list.filter(s => /^[A-Za-z_$][\w$]*$/.test(s.binding));
|
|
772
|
+
if (!ok.length)
|
|
773
|
+
return '';
|
|
774
|
+
const pairs = ok.map(s => `[typeof ${s.binding} == 'function' ? ${s.binding} : null, ${JSON.stringify(s.key)}]`).join(', ');
|
|
775
|
+
return `\n;(() => { try { for (const [f, k] of [${pairs}]) if (f) Object.defineProperty(f, '${STAMP_PROP}', { value: k, configurable: true }) } catch (_) {} })();\n`;
|
|
776
|
+
}
|
|
777
|
+
/** 'virtual:sygnal/graph': the app side of the page's "Live data" (dev only) */
|
|
778
|
+
function graphClientModule() {
|
|
779
|
+
return `import { installGraphBridge } from 'sygnal/devtools';\ninstallGraphBridge({ channel: ${JSON.stringify(GRAPH_CHANNEL)} });\n`;
|
|
780
|
+
}
|
|
781
|
+
function deny(res, status, text, headers = {}) {
|
|
782
|
+
res.statusCode = status;
|
|
783
|
+
for (const k in { ...SECURITY, ...headers })
|
|
784
|
+
res.setHeader(k, { ...SECURITY, ...headers }[k]);
|
|
785
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
786
|
+
res.end(text + '\n');
|
|
787
|
+
}
|
|
788
|
+
function projectTitle(root) {
|
|
789
|
+
try {
|
|
790
|
+
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
791
|
+
if (typeof name === 'string' && name)
|
|
792
|
+
return name;
|
|
793
|
+
}
|
|
794
|
+
catch (_) { }
|
|
795
|
+
return path.basename(root);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* The middleware for `/__sygnal/graph` and its stream. `engine` resolves to null when sygnal-check
|
|
799
|
+
* isn't installed (or too old): the route then answers 404 with the install hint.
|
|
800
|
+
*/
|
|
801
|
+
function graphMiddleware(server, root, engine, ping = 25000) {
|
|
802
|
+
const streams = new Set();
|
|
803
|
+
let subscribed = false;
|
|
804
|
+
let pinger = null;
|
|
805
|
+
const event = (b) => `event: graph\nid: ${b.version}\ndata: ${b.json}\n\n`;
|
|
806
|
+
const subscribe = (e) => {
|
|
807
|
+
if (subscribed)
|
|
808
|
+
return;
|
|
809
|
+
subscribed = true;
|
|
810
|
+
e.onBuild((b) => { if (b.changed)
|
|
811
|
+
for (const res of streams)
|
|
812
|
+
res.write(event(b)); });
|
|
813
|
+
pinger = setInterval(() => { for (const res of streams)
|
|
814
|
+
res.write(': ping\n\n'); }, ping);
|
|
815
|
+
pinger.unref?.();
|
|
816
|
+
try {
|
|
817
|
+
server.httpServer?.once?.('close', () => {
|
|
818
|
+
clearInterval(pinger);
|
|
819
|
+
for (const res of streams)
|
|
820
|
+
res.end();
|
|
821
|
+
streams.clear();
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (_) { }
|
|
825
|
+
};
|
|
826
|
+
return async (req, res, next) => {
|
|
827
|
+
const url = String(req.url || '').replace(/[?#].*$/, '');
|
|
828
|
+
if (url !== GRAPH_PATH && url !== GRAPH_PATH + '/' && url !== GRAPH_EVENTS)
|
|
829
|
+
return next();
|
|
830
|
+
const why = refusal(req, server?.config?.server?.allowedHosts, 'the Sygnal graph page');
|
|
831
|
+
if (why)
|
|
832
|
+
return deny(res, 403, `Forbidden: ${why}`);
|
|
833
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
834
|
+
return deny(res, 405, 'Method not allowed', { Allow: 'GET, HEAD' });
|
|
835
|
+
const e = await engine;
|
|
836
|
+
if (!e)
|
|
837
|
+
return deny(res, 404, 'The Sygnal graph page needs a sygnal-check with the live graph page in the project: npm i -D sygnal-check@latest');
|
|
838
|
+
subscribe(e);
|
|
839
|
+
const b = e.last || e.build();
|
|
840
|
+
if (!b)
|
|
841
|
+
return deny(res, 500, 'sygnal-check failed: see the dev server output');
|
|
842
|
+
if (url === GRAPH_EVENTS) {
|
|
843
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/event-stream; charset=utf-8', Connection: 'keep-alive', 'Content-Security-Policy': "default-src 'none'" });
|
|
844
|
+
if (req.method === 'HEAD')
|
|
845
|
+
return res.end();
|
|
846
|
+
res.write('retry: 1000\n\n');
|
|
847
|
+
if (req.headers['last-event-id'] !== String(b.version))
|
|
848
|
+
res.write(event(b));
|
|
849
|
+
streams.add(res);
|
|
850
|
+
req.on('close', () => streams.delete(res));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
let body;
|
|
854
|
+
try {
|
|
855
|
+
body = e.mod.graphHtml(b.graph || EMPTY, {
|
|
856
|
+
title: projectTitle(root),
|
|
857
|
+
subtitle: 'dev server',
|
|
858
|
+
root,
|
|
859
|
+
live: { events: GRAPH_EVENTS, version: b.version },
|
|
860
|
+
appData: { channel: GRAPH_CHANNEL },
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
return deny(res, 500, `The graph page failed: ${err?.message || err}`);
|
|
865
|
+
}
|
|
866
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/html; charset=utf-8', 'Content-Security-Policy': PAGE_CSP, 'Content-Length': globalThis.Buffer.byteLength(body) });
|
|
867
|
+
res.end(req.method === 'HEAD' ? undefined : body);
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
/** Adds the page's URL under Vite's own when the dev server prints them. */
|
|
871
|
+
function printGraphUrl(server) {
|
|
872
|
+
const print = server?.printUrls;
|
|
873
|
+
if (typeof print !== 'function')
|
|
874
|
+
return;
|
|
875
|
+
server.printUrls = function (...args) {
|
|
876
|
+
const out = print.apply(this, args);
|
|
877
|
+
try {
|
|
878
|
+
const local = server.resolvedUrls?.local?.[0];
|
|
879
|
+
if (local)
|
|
880
|
+
(server.config?.logger || console).info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`);
|
|
881
|
+
}
|
|
882
|
+
catch (_) { }
|
|
883
|
+
return out;
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
626
887
|
/**
|
|
627
888
|
* Sygnal Vite Plugin
|
|
628
889
|
*
|
|
@@ -748,6 +1009,21 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
|
|
|
748
1009
|
* `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
|
|
749
1010
|
* by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
|
|
750
1011
|
* markup in the view function itself (see viewTokensTail).
|
|
1012
|
+
* 12. `graph` option (PLAN-10 W-2 / P-2, D543 / D544, default true; dev server only, never
|
|
1013
|
+
* `vite build`, `vite preview` or Vitest): the live app graph page at /__sygnal/graph, with
|
|
1014
|
+
* its SSE stream at /__sygnal/graph/events (./graph.ts; the /__sygnal/mcp guard: loopback
|
|
1015
|
+
* peer, local Host or server.allowedHosts, local Origin). sygnal-check (from the project,
|
|
1016
|
+
* skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
|
|
1017
|
+
* builds it with createLiveProject on start and after each change: one build gives the page,
|
|
1018
|
+
* 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"). The modules of the
|
|
1020
|
+
* scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
|
|
1021
|
+
* key, D521) on each top-level component function, which the runtime inspect() reports as
|
|
1022
|
+
* `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
|
|
1023
|
+
* (installGraphBridge from 'sygnal/devtools'), which answers the page's "Live data" over a
|
|
1024
|
+
* same-origin BroadcastChannel: the page fades components with no instance, shows instance
|
|
1025
|
+
* counts, flashes a card when its action runs and shows an instance's state. `graph: false`:
|
|
1026
|
+
* no route, stamps or bridge (the checker of 5 runs check() as before).
|
|
751
1027
|
*
|
|
752
1028
|
* Why not Vite's `define`? Vite's dependency optimizer does not apply user
|
|
753
1029
|
* `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
|
|
@@ -791,6 +1067,8 @@ const OVERLAY_PLUGIN = 'sygnal-check';
|
|
|
791
1067
|
// PLAN-6 E-1: the page side of the dev MCP endpoint
|
|
792
1068
|
const MCP_CLIENT = 'virtual:sygnal/mcp';
|
|
793
1069
|
const MCP_CLIENT_ID = '\0' + MCP_CLIENT;
|
|
1070
|
+
// PLAN-10 P-2: the page side of the graph page's "Live data"
|
|
1071
|
+
const GRAPH_CLIENT_ID = '\0' + GRAPH_CLIENT;
|
|
794
1072
|
// Register the runtime checks with the core that loaded last (see the Vike wrapper)
|
|
795
1073
|
const REINSTALL_IMPORT = `import { installChecks as __sygnalInstallChecks } from 'sygnal/diagnostics';`;
|
|
796
1074
|
const REINSTALL_CALL = 'try { __sygnalInstallChecks() } catch (e) { console.warn(e) }\n';
|
|
@@ -814,11 +1092,17 @@ function sygnal(options = {}) {
|
|
|
814
1092
|
const mcpOptions = typeof options.mcp === 'object' && options.mcp ? options.mcp : {};
|
|
815
1093
|
const devtoolsOn = options.devtools !== false || mcpOn;
|
|
816
1094
|
const redux = typeof options.devtools == 'object' && !!options.devtools?.redux;
|
|
817
|
-
|
|
1095
|
+
// PLAN-10 W-2 / P-2 (D543, D544): the graph page, the component stamps and (with DevTools and a
|
|
1096
|
+
// sygnal-check that has the page, decided in config()) the app side of its "Live data"
|
|
1097
|
+
const graphOn = options.graph !== false;
|
|
1098
|
+
let graphClient = false;
|
|
1099
|
+
let engine = null;
|
|
1100
|
+
const devtoolsBase = (!devtoolsOn ? ''
|
|
818
1101
|
: redux ? `import { connectReduxDevtools as __sygnalReduxDevtools } from 'sygnal/devtools';__sygnalReduxDevtools();`
|
|
819
1102
|
: `import 'sygnal/devtools';`) + (mcpOn ? `import '${MCP_CLIENT}';` : '');
|
|
1103
|
+
let devtoolsImport = devtoolsBase;
|
|
820
1104
|
// What a dev entry gets: DevTools, then (unless diagnostics are 'off') flags + checks
|
|
821
|
-
|
|
1105
|
+
let devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
|
|
822
1106
|
const devInject = devOn || devtoolsOn;
|
|
823
1107
|
let astroClient;
|
|
824
1108
|
return {
|
|
@@ -827,6 +1111,10 @@ function sygnal(options = {}) {
|
|
|
827
1111
|
isServe = env.command === 'serve';
|
|
828
1112
|
isVitest = !!nodeProcess?.env?.VITEST;
|
|
829
1113
|
root = config?.root ? path.resolve(config.root) : nodeProcess.cwd();
|
|
1114
|
+
// P-2: the graph page's app side, in the dev server only, when the page can exist
|
|
1115
|
+
graphClient = isServe && !isVitest && graphOn && devtoolsOn && graphAvailable(root);
|
|
1116
|
+
devtoolsImport = devtoolsBase + (graphClient ? `import '${GRAPH_CLIENT}';` : '');
|
|
1117
|
+
devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
|
|
830
1118
|
const result = {
|
|
831
1119
|
// Ensure sygnal is bundled for SSR rather than externalized.
|
|
832
1120
|
// Without this, Node fails to resolve sygnal's exports from
|
|
@@ -931,6 +1219,8 @@ function sygnal(options = {}) {
|
|
|
931
1219
|
return DEV_CLIENT_ID;
|
|
932
1220
|
if (source === MCP_CLIENT && mcpOn)
|
|
933
1221
|
return MCP_CLIENT_ID;
|
|
1222
|
+
if (source === GRAPH_CLIENT && graphClient)
|
|
1223
|
+
return GRAPH_CLIENT_ID;
|
|
934
1224
|
if (source === 'sygnal' && importer && wrapRun && runtimeImporters.has(cleanId(importer))) {
|
|
935
1225
|
return RUNTIME_ID;
|
|
936
1226
|
}
|
|
@@ -953,6 +1243,8 @@ function sygnal(options = {}) {
|
|
|
953
1243
|
return devClientModule();
|
|
954
1244
|
if (id === MCP_CLIENT_ID && mcpOn)
|
|
955
1245
|
return mcpClientModule(mcpOptions);
|
|
1246
|
+
if (id === GRAPH_CLIENT_ID && graphClient)
|
|
1247
|
+
return graphClientModule();
|
|
956
1248
|
if (id === RUNTIME_ID)
|
|
957
1249
|
return runtimeModule(diagnostics);
|
|
958
1250
|
// The checks register again once the real entry has loaded: a
|
|
@@ -972,15 +1264,27 @@ function sygnal(options = {}) {
|
|
|
972
1264
|
if (mcpOn && server?.middlewares) {
|
|
973
1265
|
attachMcp(server, root, mcpOptions, sygnalVersion(root));
|
|
974
1266
|
}
|
|
975
|
-
|
|
1267
|
+
const checkOn = options.check !== false;
|
|
1268
|
+
if (!checkOn && !graphOn)
|
|
976
1269
|
return;
|
|
977
1270
|
const checkOptions = typeof options.check === 'object' && options.check ? options.check : {};
|
|
978
|
-
// Deferred: the dev server doesn't wait for the first check
|
|
979
1271
|
const checkDefaults = { strict: diagnostics.strict, ignore: diagnostics.ignore };
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1272
|
+
// Deferred: the dev server doesn't wait for the first check. `engine`: the live graph
|
|
1273
|
+
// project (W-2), null without a sygnal-check that has it
|
|
1274
|
+
engine = new Promise((resolve) => {
|
|
1275
|
+
setTimeout(() => {
|
|
1276
|
+
startDev(server, root, checkOptions, checkDefaults, options.check !== undefined, checkOn, graphOn).then(resolve, (err) => {
|
|
1277
|
+
server?.config?.logger?.warn?.(`[sygnal] sygnal-check could not start: ${err?.message || err}`);
|
|
1278
|
+
resolve(null);
|
|
1279
|
+
});
|
|
1280
|
+
}, 0);
|
|
1281
|
+
});
|
|
1282
|
+
// W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
|
|
1283
|
+
if (graphOn && server?.middlewares) {
|
|
1284
|
+
server.middlewares.use(graphMiddleware(server, root, engine));
|
|
1285
|
+
if (graphAvailable(root))
|
|
1286
|
+
printGraphUrl(server);
|
|
1287
|
+
}
|
|
984
1288
|
},
|
|
985
1289
|
transform(code, id, opts) {
|
|
986
1290
|
// SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
|
|
@@ -989,7 +1293,10 @@ function sygnal(options = {}) {
|
|
|
989
1293
|
? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
|
|
990
1294
|
: devTransform.call(this, code, id, opts);
|
|
991
1295
|
// 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
|
|
992
|
-
const
|
|
1296
|
+
const words = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null;
|
|
1297
|
+
// 12. P-2 (D544): the static keys of the module's components (dev server, graph on)
|
|
1298
|
+
const stamps = engine && isServe && !isVitest && !opts?.ssr ? componentStampTail(engine, id) : null;
|
|
1299
|
+
const tokens = words && stamps ? Promise.all([words, stamps]).then(([a, b]) => a + b) : words || stamps;
|
|
993
1300
|
if (!tokens)
|
|
994
1301
|
return result;
|
|
995
1302
|
return Promise.all([result, tokens]).then(([r, tail]) => {
|
|
@@ -1081,6 +1388,25 @@ function sygnal(options = {}) {
|
|
|
1081
1388
|
return done();
|
|
1082
1389
|
}
|
|
1083
1390
|
}
|
|
1391
|
+
// 12. PLAN-10 P-2 (D544): a module of the scanned files gets, in dev, a tail that sets each of its
|
|
1392
|
+
// components' static key (sygnal-check's D521 'file#name', from the live graph engine's last build,
|
|
1393
|
+
// built again first when the module's text changed) as a non-enumerable `__sygnalKey` on the
|
|
1394
|
+
// function; the runtime inspect() reports it as `key`, and the graph page joins instances to cards.
|
|
1395
|
+
function componentStampTail(engine, id) {
|
|
1396
|
+
const file = cleanId(id);
|
|
1397
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0'))
|
|
1398
|
+
return null;
|
|
1399
|
+
return engine.then((e) => {
|
|
1400
|
+
if (!e)
|
|
1401
|
+
return '';
|
|
1402
|
+
try {
|
|
1403
|
+
return stampTail(e.stampsFor(file) || []);
|
|
1404
|
+
}
|
|
1405
|
+
catch (_) {
|
|
1406
|
+
return '';
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1084
1410
|
// 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
|
|
1085
1411
|
// through as conditionally rendered when its words appear in the component's view; the runtime
|
|
1086
1412
|
// can read only the view function's own source. A component module (one that sets `X.intent` or
|
|
@@ -1415,16 +1741,39 @@ function checkInclude(root, include, notice, label = 'check.include') {
|
|
|
1415
1741
|
notice(`[sygnal] sygnal-check: no ${DEFAULT_INCLUDE.join('/, ')}/ directory under ${root}; checking the project root (set check.include to choose)`);
|
|
1416
1742
|
return ['.'];
|
|
1417
1743
|
}
|
|
1418
|
-
/**
|
|
1419
|
-
|
|
1744
|
+
/**
|
|
1745
|
+
* Load sygnal-check and start what dev uses it for: the checker (`check`, reported to the terminal
|
|
1746
|
+
* and the browser) and the live graph engine (`graph`, PLAN-10 W-2: the page, its stream and the
|
|
1747
|
+
* component stamps). With an engine, both come from one build. Resolves to the engine, or null.
|
|
1748
|
+
*/
|
|
1749
|
+
async function startDev(server, root, opts, defaults, explicit, checkOn, graphOn) {
|
|
1420
1750
|
const mod = await loadSygnalCheck(root);
|
|
1421
1751
|
const logger = server?.config?.logger || console;
|
|
1422
1752
|
if (!mod || typeof mod.check !== 'function') {
|
|
1423
|
-
if (explicit)
|
|
1753
|
+
if (explicit && checkOn)
|
|
1424
1754
|
logger.info('[sygnal] sygnal-check is not installed, so static checks are off (npm i -D sygnal-check)');
|
|
1425
|
-
return;
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
const setup = checkSetup(mod, root, opts, defaults, (m) => logger.info(m));
|
|
1758
|
+
// PLAN-10 W-2: a sygnal-check with the live project and the page (feature-detected, so an older
|
|
1759
|
+
// one keeps the checker as it was)
|
|
1760
|
+
const engine = graphOn && setup.configAware && typeof mod.createLiveProject === 'function' && typeof mod.graphHtml === 'function'
|
|
1761
|
+
? createEngine(mod, root, {
|
|
1762
|
+
paths: setup.include, strict: setup.checkOptions.strict, a11y: setup.checkOptions.a11y,
|
|
1763
|
+
ignore: setup.checkOptions.ignore, defaults: setup.checkOptions.defaults,
|
|
1764
|
+
}, logger, true)
|
|
1765
|
+
: null;
|
|
1766
|
+
if (checkOn)
|
|
1767
|
+
startChecker(server, root, mod, setup, opts, engine);
|
|
1768
|
+
if (engine) {
|
|
1769
|
+
if (!engine.last)
|
|
1770
|
+
engine.build();
|
|
1771
|
+
engine.watch(server);
|
|
1426
1772
|
}
|
|
1427
|
-
|
|
1773
|
+
return engine;
|
|
1774
|
+
}
|
|
1775
|
+
/** What sygnal-check checks, with which options (see the `check` option). */
|
|
1776
|
+
function checkSetup(mod, root, opts, defaults, notice) {
|
|
1428
1777
|
// PLAN-7 C-5 (D299): a sygnal-check with config support resolves the settings itself:
|
|
1429
1778
|
// explicit check.* options > its config file > the `diagnostics` defaults passed as `defaults`.
|
|
1430
1779
|
// An older one gets exactly what it got before.
|
|
@@ -1460,6 +1809,16 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1460
1809
|
a11y: opts.a11y === 'error' ? 'error' : 'warn',
|
|
1461
1810
|
};
|
|
1462
1811
|
}
|
|
1812
|
+
return { include, checkOptions, configAware };
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Run sygnal-check now and after source changes; report to the terminal and the browser. With the
|
|
1816
|
+
* live graph engine (W-2) the findings come from its builds (one build for the page and the
|
|
1817
|
+
* checker); else from check() on each change.
|
|
1818
|
+
*/
|
|
1819
|
+
function startChecker(server, root, mod, setup, opts, engine) {
|
|
1820
|
+
const logger = server?.config?.logger || console;
|
|
1821
|
+
const { include, checkOptions, configAware } = setup;
|
|
1463
1822
|
const a11y = checkOptions.a11y;
|
|
1464
1823
|
// Only error-severity findings open Vite's overlay: while an overlay is open
|
|
1465
1824
|
// Vite's client reloads the page on the first HMR update, so warnings stay
|
|
@@ -1488,14 +1847,18 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1488
1847
|
}
|
|
1489
1848
|
catch (_) { }
|
|
1490
1849
|
};
|
|
1491
|
-
const run = (initial = false) => {
|
|
1850
|
+
const run = (initial = false, built) => {
|
|
1492
1851
|
let diags;
|
|
1493
|
-
|
|
1494
|
-
diags =
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1852
|
+
if (built)
|
|
1853
|
+
diags = built;
|
|
1854
|
+
else {
|
|
1855
|
+
try {
|
|
1856
|
+
diags = mod.check(include, checkOptions);
|
|
1857
|
+
}
|
|
1858
|
+
catch (err) {
|
|
1859
|
+
logger.warn(`[sygnal] sygnal-check failed: ${err?.message || err}`, { timestamp: true });
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1499
1862
|
}
|
|
1500
1863
|
// A sygnal-check from before D144 reports SYG7xx as errors under strict: keep them warnings
|
|
1501
1864
|
// (one with config support postdates D144 and resolves a11y itself)
|
|
@@ -1536,7 +1899,15 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1536
1899
|
if (!initial)
|
|
1537
1900
|
broadcast(changed);
|
|
1538
1901
|
};
|
|
1539
|
-
|
|
1902
|
+
if (engine) {
|
|
1903
|
+
// every build of the engine: the first on start, then after changes (its own watcher)
|
|
1904
|
+
let first = !engine.last;
|
|
1905
|
+
engine.onBuild((b) => { const initial = first; first = false; run(initial, b.diagnostics || []); });
|
|
1906
|
+
if (engine.last)
|
|
1907
|
+
run(true, engine.last.diagnostics || []);
|
|
1908
|
+
}
|
|
1909
|
+
else
|
|
1910
|
+
run(true);
|
|
1540
1911
|
// A page that loads (or reloads) asks for the current findings: answer
|
|
1541
1912
|
// that client only, never every client.
|
|
1542
1913
|
try {
|
|
@@ -1551,6 +1922,8 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1551
1922
|
});
|
|
1552
1923
|
}
|
|
1553
1924
|
catch (_) { }
|
|
1925
|
+
if (engine)
|
|
1926
|
+
return;
|
|
1554
1927
|
let timer;
|
|
1555
1928
|
const onChange = (file) => {
|
|
1556
1929
|
if (file.split(/[\\/]/).includes('node_modules'))
|