sygnal 6.5.0 → 7.0.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 +156 -0
- package/README.md +2 -0
- package/dist/astro/index.cjs.js +423 -27
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +423 -27
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js.map +1 -1
- 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 +169 -14
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +168 -15
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +2 -0
- 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/ui/tabs.md +5 -4
- package/dist/guide/ui/tooltip.md +24 -0
- package/dist/index.cjs.js +781 -320
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +319 -43
- package/dist/index.esm.js +781 -320
- 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/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 +410 -27
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +410 -27
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +17 -16
- 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 +8 -2
- package/src/core/hosts/collection.ts +4 -3
- package/src/core/instance.ts +60 -16
- package/src/core/runtime.ts +5 -1
- 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/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 +8 -2
- package/src/extra/behaviors.ts +21 -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/fetch.ts +21 -1
- package/src/extra/diagnostics/checks/forms.ts +4 -3
- package/src/extra/diagnostics/checks/index.ts +2 -1
- package/src/extra/diagnostics/checks/inspect.ts +68 -6
- package/src/extra/diagnostics/checks/next.ts +3 -0
- package/src/extra/diagnostics/checks/public.d.ts +133 -12
- package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
- package/src/extra/diagnostics/codes.ts +19 -0
- package/src/extra/fetchDriver.ts +112 -7
- package/src/extra/fetchEventSource.ts +113 -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 +10 -4
- package/src/extra/testing.ts +68 -11
- package/src/index.d.ts +199 -30
- package/src/index.ts +1 -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/vike/onRenderClient.ts +44 -46
- package/src/vike/onRenderHtml.ts +5 -26
- package/src/vike/shell.ts +33 -0
- package/src/vite/graph.ts +255 -0
- package/src/vite/mcp.ts +3 -3
- package/src/vite/plugin.d.ts +19 -0
- package/src/vite/plugin.ts +144 -25
package/dist/vite/plugin.mjs
CHANGED
|
@@ -354,11 +354,11 @@ function inAllowed(h, allowed) {
|
|
|
354
354
|
return e.startsWith('.') ? h === e.slice(1) || h.endsWith(e) : h === e;
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
|
-
/** Why the request is refused (403), or null */
|
|
358
|
-
function refusal(req, allowedHosts) {
|
|
357
|
+
/** Why the request is refused (403), or null. PLAN-10 W-2: the graph route (./graph.ts) uses the same guard */
|
|
358
|
+
function refusal(req, allowedHosts, what = 'the Sygnal MCP endpoint') {
|
|
359
359
|
const remote = String(req?.socket?.remoteAddress || '');
|
|
360
360
|
if (!LOOPBACK_IP.test(remote))
|
|
361
|
-
return
|
|
361
|
+
return `${what} only answers requests from this machine (not ${remote || 'an unknown address'})`;
|
|
362
362
|
const host = req?.headers?.host;
|
|
363
363
|
if (typeof host !== 'string' || !host)
|
|
364
364
|
return 'missing Host header';
|
|
@@ -620,6 +620,275 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
|
|
|
620
620
|
`;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* PLAN-10 W-2 / P-2: the live app graph page of sygnal/vite's dev server (D543, D544).
|
|
625
|
+
*
|
|
626
|
+
* `/__sygnal/graph` serves sygnal-check's graph page (`graphHtml`, the same file `sygnal-check
|
|
627
|
+
* --graph=html --watch` serves) in live mode, and `/__sygnal/graph/events` its SSE stream: the
|
|
628
|
+
* whole graph (`event: graph`, `id: <version>`) on connect (unless `Last-Event-ID` already has it)
|
|
629
|
+
* and after each build whose graph changed, a `: ping` comment every 25 s. Dev server only.
|
|
630
|
+
*
|
|
631
|
+
* One live project (sygnal-check's createLiveProject, W-1) feeds the page, the check overlay and
|
|
632
|
+
* the console (the plugin's `check`): each build gives the graph and the diagnostics, so they agree.
|
|
633
|
+
* It builds on start and 100 ms after the last source or config file change Vite's watcher reports.
|
|
634
|
+
*
|
|
635
|
+
* P-2: each build also gives the top-level binding and static key (D521: 'file#name', '#2' for a
|
|
636
|
+
* second of the same name in a file) of every component in a scanned file (sygnal-check's
|
|
637
|
+
* componentStamps); the dev transform appends to those modules a tail that sets a non-enumerable
|
|
638
|
+
* `__sygnalKey` on each component function, and the runtime inspect() reports it as `key`. A
|
|
639
|
+
* module whose text changed since the last build is built again first, so a stamp is never stale.
|
|
640
|
+
*
|
|
641
|
+
* The page is served on the app's origin: its "Live data" (graphView/liveData.js) joins the open
|
|
642
|
+
* app tabs over a BroadcastChannel (src/extra/devGraph.ts), with no server in between.
|
|
643
|
+
*
|
|
644
|
+
* Security: the same guard as `/__sygnal/mcp` (./mcp.ts refusal(): a loopback peer, a local Host
|
|
645
|
+
* (or one in `server.allowedHosts`), a local Origin when there is one); GET and HEAD only; the page
|
|
646
|
+
* gets `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`,
|
|
647
|
+
* `X-Frame-Options: DENY` and a CSP (inline style and script, `connect-src 'self'`, no framing).
|
|
648
|
+
* Read only: it serves the page and the stream, nothing else.
|
|
649
|
+
*/
|
|
650
|
+
// @ts-ignore
|
|
651
|
+
const GRAPH_PATH = '/__sygnal/graph';
|
|
652
|
+
const GRAPH_EVENTS = GRAPH_PATH + '/events';
|
|
653
|
+
const GRAPH_CLIENT = 'virtual:sygnal/graph';
|
|
654
|
+
const GRAPH_CHANNEL = 'sygnal:graph';
|
|
655
|
+
/** The property the dev transform sets on component functions (read by inspect()) */
|
|
656
|
+
const STAMP_PROP = '__sygnalKey';
|
|
657
|
+
const SOURCE_RE$1 = /\.[cm]?[jt]sx?$/;
|
|
658
|
+
const CONFIG_FILES$1 = ['sygnal-check.config.json', 'package.json'];
|
|
659
|
+
const EMPTY = { version: 1, source: 'static', components: [], events: {}, diagnostics: [] };
|
|
660
|
+
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'";
|
|
661
|
+
const SECURITY = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'X-Frame-Options': 'DENY' };
|
|
662
|
+
const posix = (p) => p.split(path.sep).join('/');
|
|
663
|
+
/** Whether the project's sygnal-check has the live graph page with "Live data" (checked without loading it). */
|
|
664
|
+
function graphAvailable(root) {
|
|
665
|
+
try {
|
|
666
|
+
const main = createRequire(path.join(root, 'package.json')).resolve('sygnal-check');
|
|
667
|
+
return fs.existsSync(path.join(path.dirname(main), 'graphView', 'liveData.js'));
|
|
668
|
+
}
|
|
669
|
+
catch (_) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* The live project behind the page and the checker. `settings` are live.js's (paths = the check's
|
|
675
|
+
* include, strict / a11y / ignore / defaults as the checker resolves them).
|
|
676
|
+
*/
|
|
677
|
+
function createEngine(mod, root, settings, logger, stamps) {
|
|
678
|
+
const live = mod.createLiveProject({ root });
|
|
679
|
+
const listeners = [];
|
|
680
|
+
let last = null;
|
|
681
|
+
// the stamps by the live project's path of each file; `known` maps a module path as Vite gives it
|
|
682
|
+
// (posix, possibly through a symlink) to that path
|
|
683
|
+
let stampsByFile = new Map();
|
|
684
|
+
let known = new Map();
|
|
685
|
+
let failed = '';
|
|
686
|
+
const real = (f) => { try {
|
|
687
|
+
return posix(fs.realpathSync(f));
|
|
688
|
+
}
|
|
689
|
+
catch (_) {
|
|
690
|
+
return posix(f);
|
|
691
|
+
} };
|
|
692
|
+
const build = () => {
|
|
693
|
+
let b;
|
|
694
|
+
try {
|
|
695
|
+
b = live.build({ settings, stamps });
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
const msg = String(err?.message || err);
|
|
699
|
+
if (msg !== failed)
|
|
700
|
+
logger.warn(`[sygnal] sygnal-check failed: ${msg}`, { timestamp: true });
|
|
701
|
+
failed = msg;
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
failed = '';
|
|
705
|
+
if (!b.graph)
|
|
706
|
+
b = { ...b, json: JSON.stringify(EMPTY) };
|
|
707
|
+
last = b;
|
|
708
|
+
stampsByFile = b.stamps || new Map();
|
|
709
|
+
known = new Map();
|
|
710
|
+
for (const f of b.files || []) {
|
|
711
|
+
known.set(posix(f), f);
|
|
712
|
+
known.set(real(f), f);
|
|
713
|
+
}
|
|
714
|
+
for (const fn of listeners) {
|
|
715
|
+
try {
|
|
716
|
+
fn(b);
|
|
717
|
+
}
|
|
718
|
+
catch (_) { /* a listener never stops the others */ }
|
|
719
|
+
}
|
|
720
|
+
return b;
|
|
721
|
+
};
|
|
722
|
+
const stampsFor = (file) => {
|
|
723
|
+
if (!last)
|
|
724
|
+
return null;
|
|
725
|
+
const own = known.get(posix(file)) ?? known.get(real(file));
|
|
726
|
+
if (!own)
|
|
727
|
+
return null;
|
|
728
|
+
let text = null;
|
|
729
|
+
try {
|
|
730
|
+
text = fs.readFileSync(own, 'utf8');
|
|
731
|
+
}
|
|
732
|
+
catch (_) { }
|
|
733
|
+
const entry = live.cache?.get?.(own);
|
|
734
|
+
// changed since the last build (Vite transforms it before the watcher's debounced build)
|
|
735
|
+
if (text !== null && (!entry || entry.text !== text))
|
|
736
|
+
build();
|
|
737
|
+
return stampsByFile.get(own) || null;
|
|
738
|
+
};
|
|
739
|
+
let timer;
|
|
740
|
+
const watch = (server) => {
|
|
741
|
+
const onChange = (file) => {
|
|
742
|
+
if (String(file).split(/[\\/]/).includes('node_modules'))
|
|
743
|
+
return;
|
|
744
|
+
if (!SOURCE_RE$1.test(file) && !CONFIG_FILES$1.includes(path.basename(file)))
|
|
745
|
+
return;
|
|
746
|
+
clearTimeout(timer);
|
|
747
|
+
timer = setTimeout(build, 100);
|
|
748
|
+
};
|
|
749
|
+
const watcher = server?.watcher;
|
|
750
|
+
if (watcher && typeof watcher.on === 'function') {
|
|
751
|
+
watcher.on('change', onChange);
|
|
752
|
+
watcher.on('add', onChange);
|
|
753
|
+
watcher.on('unlink', onChange);
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
server.httpServer?.once?.('close', () => clearTimeout(timer));
|
|
757
|
+
}
|
|
758
|
+
catch (_) { }
|
|
759
|
+
};
|
|
760
|
+
return {
|
|
761
|
+
build, stampsFor, watch, mod,
|
|
762
|
+
get last() { return last; },
|
|
763
|
+
onBuild(fn) { listeners.push(fn); },
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/** The tail the dev transform appends to a module: a non-enumerable key on each component function. */
|
|
767
|
+
function stampTail(list) {
|
|
768
|
+
const ok = list.filter(s => /^[A-Za-z_$][\w$]*$/.test(s.binding));
|
|
769
|
+
if (!ok.length)
|
|
770
|
+
return '';
|
|
771
|
+
const pairs = ok.map(s => `[typeof ${s.binding} == 'function' ? ${s.binding} : null, ${JSON.stringify(s.key)}]`).join(', ');
|
|
772
|
+
return `\n;(() => { try { for (const [f, k] of [${pairs}]) if (f) Object.defineProperty(f, '${STAMP_PROP}', { value: k, configurable: true }) } catch (_) {} })();\n`;
|
|
773
|
+
}
|
|
774
|
+
/** 'virtual:sygnal/graph': the app side of the page's "Live data" (dev only) */
|
|
775
|
+
function graphClientModule() {
|
|
776
|
+
return `import { installGraphBridge } from 'sygnal/devtools';\ninstallGraphBridge({ channel: ${JSON.stringify(GRAPH_CHANNEL)} });\n`;
|
|
777
|
+
}
|
|
778
|
+
function deny(res, status, text, headers = {}) {
|
|
779
|
+
res.statusCode = status;
|
|
780
|
+
for (const k in { ...SECURITY, ...headers })
|
|
781
|
+
res.setHeader(k, { ...SECURITY, ...headers }[k]);
|
|
782
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
783
|
+
res.end(text + '\n');
|
|
784
|
+
}
|
|
785
|
+
function projectTitle(root) {
|
|
786
|
+
try {
|
|
787
|
+
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
788
|
+
if (typeof name === 'string' && name)
|
|
789
|
+
return name;
|
|
790
|
+
}
|
|
791
|
+
catch (_) { }
|
|
792
|
+
return path.basename(root);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The middleware for `/__sygnal/graph` and its stream. `engine` resolves to null when sygnal-check
|
|
796
|
+
* isn't installed (or too old): the route then answers 404 with the install hint.
|
|
797
|
+
*/
|
|
798
|
+
function graphMiddleware(server, root, engine, ping = 25000) {
|
|
799
|
+
const streams = new Set();
|
|
800
|
+
let subscribed = false;
|
|
801
|
+
let pinger = null;
|
|
802
|
+
const event = (b) => `event: graph\nid: ${b.version}\ndata: ${b.json}\n\n`;
|
|
803
|
+
const subscribe = (e) => {
|
|
804
|
+
if (subscribed)
|
|
805
|
+
return;
|
|
806
|
+
subscribed = true;
|
|
807
|
+
e.onBuild((b) => { if (b.changed)
|
|
808
|
+
for (const res of streams)
|
|
809
|
+
res.write(event(b)); });
|
|
810
|
+
pinger = setInterval(() => { for (const res of streams)
|
|
811
|
+
res.write(': ping\n\n'); }, ping);
|
|
812
|
+
pinger.unref?.();
|
|
813
|
+
try {
|
|
814
|
+
server.httpServer?.once?.('close', () => {
|
|
815
|
+
clearInterval(pinger);
|
|
816
|
+
for (const res of streams)
|
|
817
|
+
res.end();
|
|
818
|
+
streams.clear();
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
catch (_) { }
|
|
822
|
+
};
|
|
823
|
+
return async (req, res, next) => {
|
|
824
|
+
const url = String(req.url || '').replace(/[?#].*$/, '');
|
|
825
|
+
if (url !== GRAPH_PATH && url !== GRAPH_PATH + '/' && url !== GRAPH_EVENTS)
|
|
826
|
+
return next();
|
|
827
|
+
const why = refusal(req, server?.config?.server?.allowedHosts, 'the Sygnal graph page');
|
|
828
|
+
if (why)
|
|
829
|
+
return deny(res, 403, `Forbidden: ${why}`);
|
|
830
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
831
|
+
return deny(res, 405, 'Method not allowed', { Allow: 'GET, HEAD' });
|
|
832
|
+
const e = await engine;
|
|
833
|
+
if (!e)
|
|
834
|
+
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');
|
|
835
|
+
subscribe(e);
|
|
836
|
+
const b = e.last || e.build();
|
|
837
|
+
if (!b)
|
|
838
|
+
return deny(res, 500, 'sygnal-check failed: see the dev server output');
|
|
839
|
+
if (url === GRAPH_EVENTS) {
|
|
840
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/event-stream; charset=utf-8', Connection: 'keep-alive', 'Content-Security-Policy': "default-src 'none'" });
|
|
841
|
+
if (req.method === 'HEAD')
|
|
842
|
+
return res.end();
|
|
843
|
+
res.write('retry: 1000\n\n');
|
|
844
|
+
if (req.headers['last-event-id'] !== String(b.version))
|
|
845
|
+
res.write(event(b));
|
|
846
|
+
streams.add(res);
|
|
847
|
+
req.on('close', () => streams.delete(res));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
let body;
|
|
851
|
+
try {
|
|
852
|
+
body = e.mod.graphHtml(b.graph || EMPTY, {
|
|
853
|
+
title: projectTitle(root),
|
|
854
|
+
subtitle: 'dev server',
|
|
855
|
+
root,
|
|
856
|
+
live: { events: GRAPH_EVENTS, version: b.version },
|
|
857
|
+
appData: { channel: GRAPH_CHANNEL },
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
catch (err) {
|
|
861
|
+
return deny(res, 500, `The graph page failed: ${err?.message || err}`);
|
|
862
|
+
}
|
|
863
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/html; charset=utf-8', 'Content-Security-Policy': PAGE_CSP, 'Content-Length': globalThis.Buffer.byteLength(body) });
|
|
864
|
+
res.end(req.method === 'HEAD' ? undefined : body);
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/** The banner line when the project has no sygnal-check with the graph page (it used to print nothing). */
|
|
868
|
+
const GRAPH_MISSING_LINE = ' ➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)';
|
|
869
|
+
/**
|
|
870
|
+
* Adds the page's URL under Vite's own when the dev server prints them, or, when the project's
|
|
871
|
+
* sygnal-check is missing or too old for the page (`available` false), a line saying what to install.
|
|
872
|
+
*/
|
|
873
|
+
function printGraphUrl(server, available = true) {
|
|
874
|
+
const print = server?.printUrls;
|
|
875
|
+
if (typeof print !== 'function')
|
|
876
|
+
return;
|
|
877
|
+
server.printUrls = function (...args) {
|
|
878
|
+
const out = print.apply(this, args);
|
|
879
|
+
try {
|
|
880
|
+
const logger = server.config?.logger || console;
|
|
881
|
+
const local = server.resolvedUrls?.local?.[0];
|
|
882
|
+
if (!available)
|
|
883
|
+
logger.info(GRAPH_MISSING_LINE);
|
|
884
|
+
else if (local)
|
|
885
|
+
logger.info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`);
|
|
886
|
+
}
|
|
887
|
+
catch (_) { }
|
|
888
|
+
return out;
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
623
892
|
/**
|
|
624
893
|
* Sygnal Vite Plugin
|
|
625
894
|
*
|
|
@@ -745,6 +1014,22 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
|
|
|
745
1014
|
* `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
|
|
746
1015
|
* by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
|
|
747
1016
|
* markup in the view function itself (see viewTokensTail).
|
|
1017
|
+
* 12. `graph` option (PLAN-10 W-2 / P-2, D543 / D544, default true; dev server only, never
|
|
1018
|
+
* `vite build`, `vite preview` or Vitest): the live app graph page at /__sygnal/graph, with
|
|
1019
|
+
* its SSE stream at /__sygnal/graph/events (./graph.ts; the /__sygnal/mcp guard: loopback
|
|
1020
|
+
* peer, local Host or server.allowedHosts, local Origin). sygnal-check (from the project,
|
|
1021
|
+
* skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
|
|
1022
|
+
* builds it with createLiveProject on start and after each change: one build gives the page,
|
|
1023
|
+
* the check overlay / console findings of 5, and the component stamps. Its URL is printed
|
|
1024
|
+
* under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"; without a sygnal-check
|
|
1025
|
+
* 0.10.0+ in the project, a line saying so instead). The modules of the
|
|
1026
|
+
* scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
|
|
1027
|
+
* key, D521) on each top-level component function, which the runtime inspect() reports as
|
|
1028
|
+
* `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
|
|
1029
|
+
* (installGraphBridge from 'sygnal/devtools'), which answers the page's "Live data" over a
|
|
1030
|
+
* same-origin BroadcastChannel: the page fades components with no instance, shows instance
|
|
1031
|
+
* counts, flashes a card when its action runs and shows an instance's state. `graph: false`:
|
|
1032
|
+
* no route, stamps or bridge (the checker of 5 runs check() as before).
|
|
748
1033
|
*
|
|
749
1034
|
* Why not Vite's `define`? Vite's dependency optimizer does not apply user
|
|
750
1035
|
* `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
|
|
@@ -788,6 +1073,8 @@ const OVERLAY_PLUGIN = 'sygnal-check';
|
|
|
788
1073
|
// PLAN-6 E-1: the page side of the dev MCP endpoint
|
|
789
1074
|
const MCP_CLIENT = 'virtual:sygnal/mcp';
|
|
790
1075
|
const MCP_CLIENT_ID = '\0' + MCP_CLIENT;
|
|
1076
|
+
// PLAN-10 P-2: the page side of the graph page's "Live data"
|
|
1077
|
+
const GRAPH_CLIENT_ID = '\0' + GRAPH_CLIENT;
|
|
791
1078
|
// Register the runtime checks with the core that loaded last (see the Vike wrapper)
|
|
792
1079
|
const REINSTALL_IMPORT = `import { installChecks as __sygnalInstallChecks } from 'sygnal/diagnostics';`;
|
|
793
1080
|
const REINSTALL_CALL = 'try { __sygnalInstallChecks() } catch (e) { console.warn(e) }\n';
|
|
@@ -803,7 +1090,7 @@ function sygnal(options = {}) {
|
|
|
803
1090
|
// Files whose `import ... from 'sygnal'` resolves to the run() wrapper
|
|
804
1091
|
const runtimeImporters = new Set();
|
|
805
1092
|
// Statements run before the app starts: dev flag (+ strict flag)
|
|
806
|
-
const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '');
|
|
1093
|
+
const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '') + (diagnostics.timingAudit ? AUDIT_FLAG : '');
|
|
807
1094
|
const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`;
|
|
808
1095
|
// D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
|
|
809
1096
|
// E-1: the MCP endpoint needs the bridge, so `mcp` turns it on
|
|
@@ -811,11 +1098,17 @@ function sygnal(options = {}) {
|
|
|
811
1098
|
const mcpOptions = typeof options.mcp === 'object' && options.mcp ? options.mcp : {};
|
|
812
1099
|
const devtoolsOn = options.devtools !== false || mcpOn;
|
|
813
1100
|
const redux = typeof options.devtools == 'object' && !!options.devtools?.redux;
|
|
814
|
-
|
|
1101
|
+
// PLAN-10 W-2 / P-2 (D543, D544): the graph page, the component stamps and (with DevTools and a
|
|
1102
|
+
// sygnal-check that has the page, decided in config()) the app side of its "Live data"
|
|
1103
|
+
const graphOn = options.graph !== false;
|
|
1104
|
+
let graphClient = false;
|
|
1105
|
+
let engine = null;
|
|
1106
|
+
const devtoolsBase = (!devtoolsOn ? ''
|
|
815
1107
|
: redux ? `import { connectReduxDevtools as __sygnalReduxDevtools } from 'sygnal/devtools';__sygnalReduxDevtools();`
|
|
816
1108
|
: `import 'sygnal/devtools';`) + (mcpOn ? `import '${MCP_CLIENT}';` : '');
|
|
1109
|
+
let devtoolsImport = devtoolsBase;
|
|
817
1110
|
// What a dev entry gets: DevTools, then (unless diagnostics are 'off') flags + checks
|
|
818
|
-
|
|
1111
|
+
let devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
|
|
819
1112
|
const devInject = devOn || devtoolsOn;
|
|
820
1113
|
let astroClient;
|
|
821
1114
|
return {
|
|
@@ -824,6 +1117,10 @@ function sygnal(options = {}) {
|
|
|
824
1117
|
isServe = env.command === 'serve';
|
|
825
1118
|
isVitest = !!nodeProcess?.env?.VITEST;
|
|
826
1119
|
root = config?.root ? path.resolve(config.root) : nodeProcess.cwd();
|
|
1120
|
+
// P-2: the graph page's app side, in the dev server only, when the page can exist
|
|
1121
|
+
graphClient = isServe && !isVitest && graphOn && devtoolsOn && graphAvailable(root);
|
|
1122
|
+
devtoolsImport = devtoolsBase + (graphClient ? `import '${GRAPH_CLIENT}';` : '');
|
|
1123
|
+
devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
|
|
827
1124
|
const result = {
|
|
828
1125
|
// Ensure sygnal is bundled for SSR rather than externalized.
|
|
829
1126
|
// Without this, Node fails to resolve sygnal's exports from
|
|
@@ -928,6 +1225,8 @@ function sygnal(options = {}) {
|
|
|
928
1225
|
return DEV_CLIENT_ID;
|
|
929
1226
|
if (source === MCP_CLIENT && mcpOn)
|
|
930
1227
|
return MCP_CLIENT_ID;
|
|
1228
|
+
if (source === GRAPH_CLIENT && graphClient)
|
|
1229
|
+
return GRAPH_CLIENT_ID;
|
|
931
1230
|
if (source === 'sygnal' && importer && wrapRun && runtimeImporters.has(cleanId(importer))) {
|
|
932
1231
|
return RUNTIME_ID;
|
|
933
1232
|
}
|
|
@@ -950,6 +1249,8 @@ function sygnal(options = {}) {
|
|
|
950
1249
|
return devClientModule();
|
|
951
1250
|
if (id === MCP_CLIENT_ID && mcpOn)
|
|
952
1251
|
return mcpClientModule(mcpOptions);
|
|
1252
|
+
if (id === GRAPH_CLIENT_ID && graphClient)
|
|
1253
|
+
return graphClientModule();
|
|
953
1254
|
if (id === RUNTIME_ID)
|
|
954
1255
|
return runtimeModule(diagnostics);
|
|
955
1256
|
// The checks register again once the real entry has loaded: a
|
|
@@ -969,15 +1270,26 @@ function sygnal(options = {}) {
|
|
|
969
1270
|
if (mcpOn && server?.middlewares) {
|
|
970
1271
|
attachMcp(server, root, mcpOptions, sygnalVersion(root));
|
|
971
1272
|
}
|
|
972
|
-
|
|
1273
|
+
const checkOn = options.check !== false;
|
|
1274
|
+
if (!checkOn && !graphOn)
|
|
973
1275
|
return;
|
|
974
1276
|
const checkOptions = typeof options.check === 'object' && options.check ? options.check : {};
|
|
975
|
-
// Deferred: the dev server doesn't wait for the first check
|
|
976
1277
|
const checkDefaults = { strict: diagnostics.strict, ignore: diagnostics.ignore };
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1278
|
+
// Deferred: the dev server doesn't wait for the first check. `engine`: the live graph
|
|
1279
|
+
// project (W-2), null without a sygnal-check that has it
|
|
1280
|
+
engine = new Promise((resolve) => {
|
|
1281
|
+
setTimeout(() => {
|
|
1282
|
+
startDev(server, root, checkOptions, checkDefaults, options.check !== undefined, checkOn, graphOn).then(resolve, (err) => {
|
|
1283
|
+
server?.config?.logger?.warn?.(`[sygnal] sygnal-check could not start: ${err?.message || err}`);
|
|
1284
|
+
resolve(null);
|
|
1285
|
+
});
|
|
1286
|
+
}, 0);
|
|
1287
|
+
});
|
|
1288
|
+
// W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
|
|
1289
|
+
if (graphOn && server?.middlewares) {
|
|
1290
|
+
server.middlewares.use(graphMiddleware(server, root, engine));
|
|
1291
|
+
printGraphUrl(server, graphAvailable(root));
|
|
1292
|
+
}
|
|
981
1293
|
},
|
|
982
1294
|
transform(code, id, opts) {
|
|
983
1295
|
// SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
|
|
@@ -986,7 +1298,10 @@ function sygnal(options = {}) {
|
|
|
986
1298
|
? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
|
|
987
1299
|
: devTransform.call(this, code, id, opts);
|
|
988
1300
|
// 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
|
|
989
|
-
const
|
|
1301
|
+
const words = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null;
|
|
1302
|
+
// 12. P-2 (D544): the static keys of the module's components (dev server, graph on)
|
|
1303
|
+
const stamps = engine && isServe && !isVitest && !opts?.ssr ? componentStampTail(engine, id) : null;
|
|
1304
|
+
const tokens = words && stamps ? Promise.all([words, stamps]).then(([a, b]) => a + b) : words || stamps;
|
|
990
1305
|
if (!tokens)
|
|
991
1306
|
return result;
|
|
992
1307
|
return Promise.all([result, tokens]).then(([r, tail]) => {
|
|
@@ -1078,6 +1393,25 @@ function sygnal(options = {}) {
|
|
|
1078
1393
|
return done();
|
|
1079
1394
|
}
|
|
1080
1395
|
}
|
|
1396
|
+
// 12. PLAN-10 P-2 (D544): a module of the scanned files gets, in dev, a tail that sets each of its
|
|
1397
|
+
// components' static key (sygnal-check's D521 'file#name', from the live graph engine's last build,
|
|
1398
|
+
// built again first when the module's text changed) as a non-enumerable `__sygnalKey` on the
|
|
1399
|
+
// function; the runtime inspect() reports it as `key`, and the graph page joins instances to cards.
|
|
1400
|
+
function componentStampTail(engine, id) {
|
|
1401
|
+
const file = cleanId(id);
|
|
1402
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0'))
|
|
1403
|
+
return null;
|
|
1404
|
+
return engine.then((e) => {
|
|
1405
|
+
if (!e)
|
|
1406
|
+
return '';
|
|
1407
|
+
try {
|
|
1408
|
+
return stampTail(e.stampsFor(file) || []);
|
|
1409
|
+
}
|
|
1410
|
+
catch (_) {
|
|
1411
|
+
return '';
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1081
1415
|
// 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
|
|
1082
1416
|
// through as conditionally rendered when its words appear in the component's view; the runtime
|
|
1083
1417
|
// can read only the view function's own source. A component module (one that sets `X.intent` or
|
|
@@ -1239,7 +1573,7 @@ function normalizeDiagnostics(value) {
|
|
|
1239
1573
|
if (!MODES.includes(mode)) {
|
|
1240
1574
|
throw new Error(`[vite-plugin-sygnal] diagnostics mode must be one of ${MODES.map(m => `'${m}'`).join(', ')}; got ${JSON.stringify(mode)}`);
|
|
1241
1575
|
}
|
|
1242
|
-
return { mode, strict: !!o.strict, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
|
|
1576
|
+
return { mode, strict: !!o.strict, timingAudit: !!o.timingAudit, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
|
|
1243
1577
|
}
|
|
1244
1578
|
/** Module id without a query or hash. */
|
|
1245
1579
|
function cleanId(id) {
|
|
@@ -1412,16 +1746,39 @@ function checkInclude(root, include, notice, label = 'check.include') {
|
|
|
1412
1746
|
notice(`[sygnal] sygnal-check: no ${DEFAULT_INCLUDE.join('/, ')}/ directory under ${root}; checking the project root (set check.include to choose)`);
|
|
1413
1747
|
return ['.'];
|
|
1414
1748
|
}
|
|
1415
|
-
/**
|
|
1416
|
-
|
|
1749
|
+
/**
|
|
1750
|
+
* Load sygnal-check and start what dev uses it for: the checker (`check`, reported to the terminal
|
|
1751
|
+
* and the browser) and the live graph engine (`graph`, PLAN-10 W-2: the page, its stream and the
|
|
1752
|
+
* component stamps). With an engine, both come from one build. Resolves to the engine, or null.
|
|
1753
|
+
*/
|
|
1754
|
+
async function startDev(server, root, opts, defaults, explicit, checkOn, graphOn) {
|
|
1417
1755
|
const mod = await loadSygnalCheck(root);
|
|
1418
1756
|
const logger = server?.config?.logger || console;
|
|
1419
1757
|
if (!mod || typeof mod.check !== 'function') {
|
|
1420
|
-
if (explicit)
|
|
1758
|
+
if (explicit && checkOn)
|
|
1421
1759
|
logger.info('[sygnal] sygnal-check is not installed, so static checks are off (npm i -D sygnal-check)');
|
|
1422
|
-
return;
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
const setup = checkSetup(mod, root, opts, defaults, (m) => logger.info(m));
|
|
1763
|
+
// PLAN-10 W-2: a sygnal-check with the live project and the page (feature-detected, so an older
|
|
1764
|
+
// one keeps the checker as it was)
|
|
1765
|
+
const engine = graphOn && setup.configAware && typeof mod.createLiveProject === 'function' && typeof mod.graphHtml === 'function'
|
|
1766
|
+
? createEngine(mod, root, {
|
|
1767
|
+
paths: setup.include, strict: setup.checkOptions.strict, a11y: setup.checkOptions.a11y,
|
|
1768
|
+
ignore: setup.checkOptions.ignore, defaults: setup.checkOptions.defaults,
|
|
1769
|
+
}, logger, true)
|
|
1770
|
+
: null;
|
|
1771
|
+
if (checkOn)
|
|
1772
|
+
startChecker(server, root, mod, setup, opts, engine);
|
|
1773
|
+
if (engine) {
|
|
1774
|
+
if (!engine.last)
|
|
1775
|
+
engine.build();
|
|
1776
|
+
engine.watch(server);
|
|
1423
1777
|
}
|
|
1424
|
-
|
|
1778
|
+
return engine;
|
|
1779
|
+
}
|
|
1780
|
+
/** What sygnal-check checks, with which options (see the `check` option). */
|
|
1781
|
+
function checkSetup(mod, root, opts, defaults, notice) {
|
|
1425
1782
|
// PLAN-7 C-5 (D299): a sygnal-check with config support resolves the settings itself:
|
|
1426
1783
|
// explicit check.* options > its config file > the `diagnostics` defaults passed as `defaults`.
|
|
1427
1784
|
// An older one gets exactly what it got before.
|
|
@@ -1457,6 +1814,16 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1457
1814
|
a11y: opts.a11y === 'error' ? 'error' : 'warn',
|
|
1458
1815
|
};
|
|
1459
1816
|
}
|
|
1817
|
+
return { include, checkOptions, configAware };
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Run sygnal-check now and after source changes; report to the terminal and the browser. With the
|
|
1821
|
+
* live graph engine (W-2) the findings come from its builds (one build for the page and the
|
|
1822
|
+
* checker); else from check() on each change.
|
|
1823
|
+
*/
|
|
1824
|
+
function startChecker(server, root, mod, setup, opts, engine) {
|
|
1825
|
+
const logger = server?.config?.logger || console;
|
|
1826
|
+
const { include, checkOptions, configAware } = setup;
|
|
1460
1827
|
const a11y = checkOptions.a11y;
|
|
1461
1828
|
// Only error-severity findings open Vite's overlay: while an overlay is open
|
|
1462
1829
|
// Vite's client reloads the page on the first HMR update, so warnings stay
|
|
@@ -1485,14 +1852,18 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1485
1852
|
}
|
|
1486
1853
|
catch (_) { }
|
|
1487
1854
|
};
|
|
1488
|
-
const run = (initial = false) => {
|
|
1855
|
+
const run = (initial = false, built) => {
|
|
1489
1856
|
let diags;
|
|
1490
|
-
|
|
1491
|
-
diags =
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1857
|
+
if (built)
|
|
1858
|
+
diags = built;
|
|
1859
|
+
else {
|
|
1860
|
+
try {
|
|
1861
|
+
diags = mod.check(include, checkOptions);
|
|
1862
|
+
}
|
|
1863
|
+
catch (err) {
|
|
1864
|
+
logger.warn(`[sygnal] sygnal-check failed: ${err?.message || err}`, { timestamp: true });
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1496
1867
|
}
|
|
1497
1868
|
// A sygnal-check from before D144 reports SYG7xx as errors under strict: keep them warnings
|
|
1498
1869
|
// (one with config support postdates D144 and resolves a11y itself)
|
|
@@ -1533,7 +1904,15 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1533
1904
|
if (!initial)
|
|
1534
1905
|
broadcast(changed);
|
|
1535
1906
|
};
|
|
1536
|
-
|
|
1907
|
+
if (engine) {
|
|
1908
|
+
// every build of the engine: the first on start, then after changes (its own watcher)
|
|
1909
|
+
let first = !engine.last;
|
|
1910
|
+
engine.onBuild((b) => { const initial = first; first = false; run(initial, b.diagnostics || []); });
|
|
1911
|
+
if (engine.last)
|
|
1912
|
+
run(true, engine.last.diagnostics || []);
|
|
1913
|
+
}
|
|
1914
|
+
else
|
|
1915
|
+
run(true);
|
|
1537
1916
|
// A page that loads (or reloads) asks for the current findings: answer
|
|
1538
1917
|
// that client only, never every client.
|
|
1539
1918
|
try {
|
|
@@ -1548,6 +1927,8 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1548
1927
|
});
|
|
1549
1928
|
}
|
|
1550
1929
|
catch (_) { }
|
|
1930
|
+
if (engine)
|
|
1931
|
+
return;
|
|
1551
1932
|
let timer;
|
|
1552
1933
|
const onChange = (file) => {
|
|
1553
1934
|
if (file.split(/[\\/]/).includes('node_modules'))
|
|
@@ -1570,6 +1951,8 @@ async function startChecker(server, root, opts, defaults, explicit) {
|
|
|
1570
1951
|
}
|
|
1571
1952
|
const DEV_FLAG = 'if (globalThis.__SYGNAL_DEV__ === undefined) globalThis.__SYGNAL_DEV__ = true;';
|
|
1572
1953
|
const STRICT_FLAG = 'if (globalThis.__SYGNAL_STRICT__ === undefined) globalThis.__SYGNAL_STRICT__ = true;';
|
|
1954
|
+
// 7.0: the opt-in timing audit (SYG257, 'sygnal/diagnostics' checks/timingAudit.ts)
|
|
1955
|
+
const AUDIT_FLAG = 'if (globalThis.__SYGNAL_TIMING_AUDIT__ === undefined) globalThis.__SYGNAL_TIMING_AUDIT__ = true;';
|
|
1573
1956
|
// A top-level (column 0) run(Component, ...) statement: bare, or declaring a
|
|
1574
1957
|
// new binding. Assignments to existing variables, nested calls and indented
|
|
1575
1958
|
// calls (e.g. inside a function or test callback) do not match (B-007). It is
|