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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-10 W-2 / P-2: the live app graph page of sygnal/vite's dev server (D543, D544).
|
|
3
|
+
*
|
|
4
|
+
* `/__sygnal/graph` serves sygnal-check's graph page (`graphHtml`, the same file `sygnal-check
|
|
5
|
+
* --graph=html --watch` serves) in live mode, and `/__sygnal/graph/events` its SSE stream: the
|
|
6
|
+
* whole graph (`event: graph`, `id: <version>`) on connect (unless `Last-Event-ID` already has it)
|
|
7
|
+
* and after each build whose graph changed, a `: ping` comment every 25 s. Dev server only.
|
|
8
|
+
*
|
|
9
|
+
* One live project (sygnal-check's createLiveProject, W-1) feeds the page, the check overlay and
|
|
10
|
+
* the console (the plugin's `check`): each build gives the graph and the diagnostics, so they agree.
|
|
11
|
+
* It builds on start and 100 ms after the last source or config file change Vite's watcher reports.
|
|
12
|
+
*
|
|
13
|
+
* P-2: each build also gives the top-level binding and static key (D521: 'file#name', '#2' for a
|
|
14
|
+
* second of the same name in a file) of every component in a scanned file (sygnal-check's
|
|
15
|
+
* componentStamps); the dev transform appends to those modules a tail that sets a non-enumerable
|
|
16
|
+
* `__sygnalKey` on each component function, and the runtime inspect() reports it as `key`. A
|
|
17
|
+
* module whose text changed since the last build is built again first, so a stamp is never stale.
|
|
18
|
+
*
|
|
19
|
+
* The page is served on the app's origin: its "Live data" (graphView/liveData.js) joins the open
|
|
20
|
+
* app tabs over a BroadcastChannel (src/extra/devGraph.ts), with no server in between.
|
|
21
|
+
*
|
|
22
|
+
* Security: the same guard as `/__sygnal/mcp` (./mcp.ts refusal(): a loopback peer, a local Host
|
|
23
|
+
* (or one in `server.allowedHosts`), a local Origin when there is one); GET and HEAD only; the page
|
|
24
|
+
* gets `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`,
|
|
25
|
+
* `X-Frame-Options: DENY` and a CSP (inline style and script, `connect-src 'self'`, no framing).
|
|
26
|
+
* Read only: it serves the page and the stream, nothing else.
|
|
27
|
+
*/
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
import fs from 'node:fs'
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
import path from 'node:path'
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
import { createRequire } from 'node:module'
|
|
34
|
+
import { refusal } from './mcp'
|
|
35
|
+
|
|
36
|
+
export const GRAPH_PATH = '/__sygnal/graph'
|
|
37
|
+
export const GRAPH_EVENTS = GRAPH_PATH + '/events'
|
|
38
|
+
export const GRAPH_CLIENT = 'virtual:sygnal/graph'
|
|
39
|
+
export const GRAPH_CHANNEL = 'sygnal:graph'
|
|
40
|
+
/** The property the dev transform sets on component functions (read by inspect()) */
|
|
41
|
+
export const STAMP_PROP = '__sygnalKey'
|
|
42
|
+
|
|
43
|
+
const SOURCE_RE = /\.[cm]?[jt]sx?$/
|
|
44
|
+
const CONFIG_FILES = ['sygnal-check.config.json', 'package.json']
|
|
45
|
+
const EMPTY = { version: 1, source: 'static', components: [], events: {}, diagnostics: [] }
|
|
46
|
+
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'"
|
|
47
|
+
const SECURITY: Record<string, string> = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'X-Frame-Options': 'DENY' }
|
|
48
|
+
|
|
49
|
+
const posix = (p: string) => p.split(path.sep).join('/')
|
|
50
|
+
|
|
51
|
+
/** Whether the project's sygnal-check has the live graph page with "Live data" (checked without loading it). */
|
|
52
|
+
export function graphAvailable(root: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
const main = createRequire(path.join(root, 'package.json')).resolve('sygnal-check')
|
|
55
|
+
return fs.existsSync(path.join(path.dirname(main), 'graphView', 'liveData.js'))
|
|
56
|
+
} catch (_) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LiveBuild { graph: any; json: string; version: number; changed: boolean; diagnostics: any[]; stamps: Map<string, Array<{ binding: string; key: string }>> | null; files: string[] }
|
|
62
|
+
|
|
63
|
+
export interface LiveEngine {
|
|
64
|
+
/** build now; listeners hear about it */
|
|
65
|
+
build(): LiveBuild | null
|
|
66
|
+
readonly last: LiveBuild | null
|
|
67
|
+
onBuild(fn: (b: LiveBuild) => void): void
|
|
68
|
+
/** the stamps of a module (absolute path), after building again when its text changed */
|
|
69
|
+
stampsFor(file: string): Array<{ binding: string; key: string }> | null
|
|
70
|
+
/** follow Vite's watcher (100 ms debounce) */
|
|
71
|
+
watch(server: any): void
|
|
72
|
+
readonly mod: any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The live project behind the page and the checker. `settings` are live.js's (paths = the check's
|
|
77
|
+
* include, strict / a11y / ignore / defaults as the checker resolves them).
|
|
78
|
+
*/
|
|
79
|
+
export function createEngine(mod: any, root: string, settings: any, logger: any, stamps: boolean): LiveEngine {
|
|
80
|
+
const live = mod.createLiveProject({ root })
|
|
81
|
+
const listeners: Array<(b: LiveBuild) => void> = []
|
|
82
|
+
let last: LiveBuild | null = null
|
|
83
|
+
// the stamps by the live project's path of each file; `known` maps a module path as Vite gives it
|
|
84
|
+
// (posix, possibly through a symlink) to that path
|
|
85
|
+
let stampsByFile = new Map<string, Array<{ binding: string; key: string }>>()
|
|
86
|
+
let known = new Map<string, string>()
|
|
87
|
+
let failed = ''
|
|
88
|
+
const real = (f: string) => { try { return posix(fs.realpathSync(f)) } catch (_) { return posix(f) } }
|
|
89
|
+
|
|
90
|
+
const build = (): LiveBuild | null => {
|
|
91
|
+
let b: any
|
|
92
|
+
try {
|
|
93
|
+
b = live.build({ settings, stamps })
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
const msg = String(err?.message || err)
|
|
96
|
+
if (msg !== failed) logger.warn(`[sygnal] sygnal-check failed: ${msg}`, { timestamp: true })
|
|
97
|
+
failed = msg
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
failed = ''
|
|
101
|
+
if (!b.graph) b = { ...b, json: JSON.stringify(EMPTY) }
|
|
102
|
+
last = b
|
|
103
|
+
stampsByFile = b.stamps || new Map()
|
|
104
|
+
known = new Map()
|
|
105
|
+
for (const f of b.files || []) { known.set(posix(f), f); known.set(real(f), f) }
|
|
106
|
+
for (const fn of listeners) { try { fn(b) } catch (_) { /* a listener never stops the others */ } }
|
|
107
|
+
return b
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stampsFor = (file: string) => {
|
|
111
|
+
if (!last) return null
|
|
112
|
+
const own = known.get(posix(file)) ?? known.get(real(file))
|
|
113
|
+
if (!own) return null
|
|
114
|
+
let text: string | null = null
|
|
115
|
+
try { text = fs.readFileSync(own, 'utf8') } catch (_) {}
|
|
116
|
+
const entry = live.cache?.get?.(own)
|
|
117
|
+
// changed since the last build (Vite transforms it before the watcher's debounced build)
|
|
118
|
+
if (text !== null && (!entry || entry.text !== text)) build()
|
|
119
|
+
return stampsByFile.get(own) || null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let timer: any
|
|
123
|
+
const watch = (server: any) => {
|
|
124
|
+
const onChange = (file: string) => {
|
|
125
|
+
if (String(file).split(/[\\/]/).includes('node_modules')) return
|
|
126
|
+
if (!SOURCE_RE.test(file) && !CONFIG_FILES.includes(path.basename(file))) return
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
timer = setTimeout(build, 100)
|
|
129
|
+
}
|
|
130
|
+
const watcher = server?.watcher
|
|
131
|
+
if (watcher && typeof watcher.on === 'function') {
|
|
132
|
+
watcher.on('change', onChange)
|
|
133
|
+
watcher.on('add', onChange)
|
|
134
|
+
watcher.on('unlink', onChange)
|
|
135
|
+
}
|
|
136
|
+
try { server.httpServer?.once?.('close', () => clearTimeout(timer)) } catch (_) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
build, stampsFor, watch, mod,
|
|
141
|
+
get last() { return last },
|
|
142
|
+
onBuild(fn) { listeners.push(fn) },
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The tail the dev transform appends to a module: a non-enumerable key on each component function. */
|
|
147
|
+
export function stampTail(list: Array<{ binding: string; key: string }>): string {
|
|
148
|
+
const ok = list.filter(s => /^[A-Za-z_$][\w$]*$/.test(s.binding))
|
|
149
|
+
if (!ok.length) return ''
|
|
150
|
+
const pairs = ok.map(s => `[typeof ${s.binding} == 'function' ? ${s.binding} : null, ${JSON.stringify(s.key)}]`).join(', ')
|
|
151
|
+
return `\n;(() => { try { for (const [f, k] of [${pairs}]) if (f) Object.defineProperty(f, '${STAMP_PROP}', { value: k, configurable: true }) } catch (_) {} })();\n`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 'virtual:sygnal/graph': the app side of the page's "Live data" (dev only) */
|
|
155
|
+
export function graphClientModule(): string {
|
|
156
|
+
return `import { installGraphBridge } from 'sygnal/devtools';\ninstallGraphBridge({ channel: ${JSON.stringify(GRAPH_CHANNEL)} });\n`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function deny(res: any, status: number, text: string, headers: Record<string, string> = {}) {
|
|
160
|
+
res.statusCode = status
|
|
161
|
+
for (const k in { ...SECURITY, ...headers }) res.setHeader(k, ({ ...SECURITY, ...headers } as any)[k])
|
|
162
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
163
|
+
res.end(text + '\n')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function projectTitle(root: string): string {
|
|
167
|
+
try {
|
|
168
|
+
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name
|
|
169
|
+
if (typeof name === 'string' && name) return name
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
return path.basename(root)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The middleware for `/__sygnal/graph` and its stream. `engine` resolves to null when sygnal-check
|
|
176
|
+
* isn't installed (or too old): the route then answers 404 with the install hint.
|
|
177
|
+
*/
|
|
178
|
+
export function graphMiddleware(server: any, root: string, engine: Promise<LiveEngine | null>, ping = 25000) {
|
|
179
|
+
const streams = new Set<any>()
|
|
180
|
+
let subscribed = false
|
|
181
|
+
let pinger: any = null
|
|
182
|
+
const event = (b: LiveBuild) => `event: graph\nid: ${b.version}\ndata: ${b.json}\n\n`
|
|
183
|
+
const subscribe = (e: LiveEngine) => {
|
|
184
|
+
if (subscribed) return
|
|
185
|
+
subscribed = true
|
|
186
|
+
e.onBuild((b) => { if (b.changed) for (const res of streams) res.write(event(b)) })
|
|
187
|
+
pinger = setInterval(() => { for (const res of streams) res.write(': ping\n\n') }, ping)
|
|
188
|
+
pinger.unref?.()
|
|
189
|
+
try {
|
|
190
|
+
server.httpServer?.once?.('close', () => {
|
|
191
|
+
clearInterval(pinger)
|
|
192
|
+
for (const res of streams) res.end()
|
|
193
|
+
streams.clear()
|
|
194
|
+
})
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return async (req: any, res: any, next: any) => {
|
|
199
|
+
const url = String(req.url || '').replace(/[?#].*$/, '')
|
|
200
|
+
if (url !== GRAPH_PATH && url !== GRAPH_PATH + '/' && url !== GRAPH_EVENTS) return next()
|
|
201
|
+
const why = refusal(req, server?.config?.server?.allowedHosts, 'the Sygnal graph page')
|
|
202
|
+
if (why) return deny(res, 403, `Forbidden: ${why}`)
|
|
203
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return deny(res, 405, 'Method not allowed', { Allow: 'GET, HEAD' })
|
|
204
|
+
const e = await engine
|
|
205
|
+
if (!e) 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')
|
|
206
|
+
subscribe(e)
|
|
207
|
+
const b = e.last || e.build()
|
|
208
|
+
if (!b) return deny(res, 500, 'sygnal-check failed: see the dev server output')
|
|
209
|
+
if (url === GRAPH_EVENTS) {
|
|
210
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/event-stream; charset=utf-8', Connection: 'keep-alive', 'Content-Security-Policy': "default-src 'none'" })
|
|
211
|
+
if (req.method === 'HEAD') return res.end()
|
|
212
|
+
res.write('retry: 1000\n\n')
|
|
213
|
+
if (req.headers['last-event-id'] !== String(b.version)) res.write(event(b))
|
|
214
|
+
streams.add(res)
|
|
215
|
+
req.on('close', () => streams.delete(res))
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
let body: string
|
|
219
|
+
try {
|
|
220
|
+
body = e.mod.graphHtml(b.graph || EMPTY, {
|
|
221
|
+
title: projectTitle(root),
|
|
222
|
+
subtitle: 'dev server',
|
|
223
|
+
root,
|
|
224
|
+
live: { events: GRAPH_EVENTS, version: b.version },
|
|
225
|
+
appData: { channel: GRAPH_CHANNEL },
|
|
226
|
+
})
|
|
227
|
+
} catch (err: any) {
|
|
228
|
+
return deny(res, 500, `The graph page failed: ${err?.message || err}`)
|
|
229
|
+
}
|
|
230
|
+
res.writeHead(200, { ...SECURITY, 'Content-Type': 'text/html; charset=utf-8', 'Content-Security-Policy': PAGE_CSP, 'Content-Length': (globalThis as any).Buffer.byteLength(body) })
|
|
231
|
+
res.end(req.method === 'HEAD' ? undefined : body)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Adds the page's URL under Vite's own when the dev server prints them. */
|
|
236
|
+
export function printGraphUrl(server: any) {
|
|
237
|
+
const print = server?.printUrls
|
|
238
|
+
if (typeof print !== 'function') return
|
|
239
|
+
server.printUrls = function (this: any, ...args: any[]) {
|
|
240
|
+
const out = print.apply(this, args)
|
|
241
|
+
try {
|
|
242
|
+
const local = server.resolvedUrls?.local?.[0]
|
|
243
|
+
if (local) (server.config?.logger || console).info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`)
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
return out
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/vite/mcp.ts
CHANGED
|
@@ -337,10 +337,10 @@ function inAllowed(h: string, allowed: any): boolean {
|
|
|
337
337
|
})
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
/** Why the request is refused (403), or null */
|
|
341
|
-
export function refusal(req: any, allowedHosts?: any): string | null {
|
|
340
|
+
/** Why the request is refused (403), or null. PLAN-10 W-2: the graph route (./graph.ts) uses the same guard */
|
|
341
|
+
export function refusal(req: any, allowedHosts?: any, what = 'the Sygnal MCP endpoint'): string | null {
|
|
342
342
|
const remote = String(req?.socket?.remoteAddress || '')
|
|
343
|
-
if (!LOOPBACK_IP.test(remote)) return
|
|
343
|
+
if (!LOOPBACK_IP.test(remote)) return `${what} only answers requests from this machine (not ${remote || 'an unknown address'})`
|
|
344
344
|
const host = req?.headers?.host
|
|
345
345
|
if (typeof host !== 'string' || !host) return 'missing Host header'
|
|
346
346
|
const h = hostname(host)
|
package/src/vite/plugin.d.ts
CHANGED
|
@@ -136,6 +136,18 @@ export interface SygnalPluginOptions {
|
|
|
136
136
|
* @default false
|
|
137
137
|
*/
|
|
138
138
|
mcp?: boolean | McpPluginOptions
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Serve the live app graph page at `/__sygnal/graph` on the dev server (requests from this
|
|
142
|
+
* machine with a local Host / Origin only, as `/__sygnal/mcp`), built by sygnal-check and updated
|
|
143
|
+
* on every save; its URL is printed under Vite's. Skipped silently when sygnal-check isn't
|
|
144
|
+
* installed. In dev the component functions also get their static key (non-enumerable, read by
|
|
145
|
+
* inspect() as `key`), and with DevTools on the open app tabs answer the page's "Live data"
|
|
146
|
+
* (mounted components, instance counts, action flashes, state). Dev server only: production
|
|
147
|
+
* builds contain none of it.
|
|
148
|
+
* @default true
|
|
149
|
+
*/
|
|
150
|
+
graph?: boolean
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
export interface McpPluginOptions {
|
package/src/vite/plugin.ts
CHANGED
|
@@ -123,6 +123,21 @@
|
|
|
123
123
|
* `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
|
|
124
124
|
* by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
|
|
125
125
|
* markup in the view function itself (see viewTokensTail).
|
|
126
|
+
* 12. `graph` option (PLAN-10 W-2 / P-2, D543 / D544, default true; dev server only, never
|
|
127
|
+
* `vite build`, `vite preview` or Vitest): the live app graph page at /__sygnal/graph, with
|
|
128
|
+
* its SSE stream at /__sygnal/graph/events (./graph.ts; the /__sygnal/mcp guard: loopback
|
|
129
|
+
* peer, local Host or server.allowedHosts, local Origin). sygnal-check (from the project,
|
|
130
|
+
* skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
|
|
131
|
+
* builds it with createLiveProject on start and after each change: one build gives the page,
|
|
132
|
+
* the check overlay / console findings of 5, and the component stamps. Its URL is printed
|
|
133
|
+
* under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"). The modules of the
|
|
134
|
+
* scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
|
|
135
|
+
* key, D521) on each top-level component function, which the runtime inspect() reports as
|
|
136
|
+
* `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
|
|
137
|
+
* (installGraphBridge from 'sygnal/devtools'), which answers the page's "Live data" over a
|
|
138
|
+
* same-origin BroadcastChannel: the page fades components with no instance, shows instance
|
|
139
|
+
* counts, flashes a card when its action runs and shows an instance's state. `graph: false`:
|
|
140
|
+
* no route, stamps or bridge (the checker of 5 runs check() as before).
|
|
126
141
|
*
|
|
127
142
|
* Why not Vite's `define`? Vite's dependency optimizer does not apply user
|
|
128
143
|
* `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
|
|
@@ -162,6 +177,8 @@ import { pathToFileURL } from 'node:url'
|
|
|
162
177
|
import { globalThisAlias } from './globalthis'
|
|
163
178
|
import { attachMcp, mcpClientModule } from './mcp'
|
|
164
179
|
import type { McpPluginOptions } from './mcp'
|
|
180
|
+
import { GRAPH_CLIENT, createEngine, graphAvailable, graphClientModule, graphMiddleware, printGraphUrl, stampTail } from './graph'
|
|
181
|
+
import type { LiveEngine } from './graph'
|
|
165
182
|
|
|
166
183
|
export type { McpPluginOptions } from './mcp'
|
|
167
184
|
|
|
@@ -282,6 +299,17 @@ export interface SygnalPluginOptions {
|
|
|
282
299
|
* @default false
|
|
283
300
|
*/
|
|
284
301
|
mcp?: boolean | McpPluginOptions
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* PLAN-10 W-2 (D543): serve the live app graph page at `/__sygnal/graph` on the dev server (local
|
|
305
|
+
* requests only, as `/__sygnal/mcp`), built by sygnal-check (skipped silently when it isn't
|
|
306
|
+
* installed) and updated on every save; its URL is printed under Vite's. P-2 (D544): in dev the
|
|
307
|
+
* component functions get their static key (a non-enumerable `__sygnalKey`), and the app tabs
|
|
308
|
+
* answer the page's "Live data" (mounted components, instance counts, action flashes, state).
|
|
309
|
+
* Dev server only: never `vite build`, `vite preview` or Vitest. false: no route, no stamps.
|
|
310
|
+
* @default true
|
|
311
|
+
*/
|
|
312
|
+
graph?: boolean
|
|
285
313
|
}
|
|
286
314
|
|
|
287
315
|
// Virtual modules (dev server only)
|
|
@@ -297,6 +325,8 @@ const OVERLAY_PLUGIN = 'sygnal-check'
|
|
|
297
325
|
// PLAN-6 E-1: the page side of the dev MCP endpoint
|
|
298
326
|
const MCP_CLIENT = 'virtual:sygnal/mcp'
|
|
299
327
|
const MCP_CLIENT_ID = '\0' + MCP_CLIENT
|
|
328
|
+
// PLAN-10 P-2: the page side of the graph page's "Live data"
|
|
329
|
+
const GRAPH_CLIENT_ID = '\0' + GRAPH_CLIENT
|
|
300
330
|
// Register the runtime checks with the core that loaded last (see the Vike wrapper)
|
|
301
331
|
const REINSTALL_IMPORT = `import { installChecks as __sygnalInstallChecks } from 'sygnal/diagnostics';`
|
|
302
332
|
const REINSTALL_CALL = 'try { __sygnalInstallChecks() } catch (e) { console.warn(e) }\n'
|
|
@@ -321,11 +351,17 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
321
351
|
const mcpOptions: McpPluginOptions = typeof options.mcp === 'object' && options.mcp ? options.mcp : {}
|
|
322
352
|
const devtoolsOn = options.devtools !== false || mcpOn
|
|
323
353
|
const redux = typeof options.devtools == 'object' && !!options.devtools?.redux
|
|
324
|
-
|
|
354
|
+
// PLAN-10 W-2 / P-2 (D543, D544): the graph page, the component stamps and (with DevTools and a
|
|
355
|
+
// sygnal-check that has the page, decided in config()) the app side of its "Live data"
|
|
356
|
+
const graphOn = options.graph !== false
|
|
357
|
+
let graphClient = false
|
|
358
|
+
let engine: Promise<LiveEngine | null> | null = null
|
|
359
|
+
const devtoolsBase = (!devtoolsOn ? ''
|
|
325
360
|
: redux ? `import { connectReduxDevtools as __sygnalReduxDevtools } from 'sygnal/devtools';__sygnalReduxDevtools();`
|
|
326
361
|
: `import 'sygnal/devtools';`) + (mcpOn ? `import '${MCP_CLIENT}';` : '')
|
|
362
|
+
let devtoolsImport = devtoolsBase
|
|
327
363
|
// What a dev entry gets: DevTools, then (unless diagnostics are 'off') flags + checks
|
|
328
|
-
|
|
364
|
+
let devSnippet = devtoolsImport + (devOn ? flags + devImports : '')
|
|
329
365
|
const devInject = devOn || devtoolsOn
|
|
330
366
|
let astroClient: Set<string> | undefined
|
|
331
367
|
|
|
@@ -336,6 +372,10 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
336
372
|
isServe = env.command === 'serve'
|
|
337
373
|
isVitest = !!nodeProcess?.env?.VITEST
|
|
338
374
|
root = config?.root ? path.resolve(config.root) : nodeProcess.cwd()
|
|
375
|
+
// P-2: the graph page's app side, in the dev server only, when the page can exist
|
|
376
|
+
graphClient = isServe && !isVitest && graphOn && devtoolsOn && graphAvailable(root)
|
|
377
|
+
devtoolsImport = devtoolsBase + (graphClient ? `import '${GRAPH_CLIENT}';` : '')
|
|
378
|
+
devSnippet = devtoolsImport + (devOn ? flags + devImports : '')
|
|
339
379
|
|
|
340
380
|
const result: any = {
|
|
341
381
|
// Ensure sygnal is bundled for SSR rather than externalized.
|
|
@@ -440,6 +480,7 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
440
480
|
if (!isServe || isVitest) return null
|
|
441
481
|
if (source === DEV_CLIENT) return DEV_CLIENT_ID
|
|
442
482
|
if (source === MCP_CLIENT && mcpOn) return MCP_CLIENT_ID
|
|
483
|
+
if (source === GRAPH_CLIENT && graphClient) return GRAPH_CLIENT_ID
|
|
443
484
|
if (source === 'sygnal' && importer && wrapRun && runtimeImporters.has(cleanId(importer))) {
|
|
444
485
|
return RUNTIME_ID
|
|
445
486
|
}
|
|
@@ -459,6 +500,7 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
459
500
|
load(id: string) {
|
|
460
501
|
if (id === DEV_CLIENT_ID) return devClientModule()
|
|
461
502
|
if (id === MCP_CLIENT_ID && mcpOn) return mcpClientModule(mcpOptions)
|
|
503
|
+
if (id === GRAPH_CLIENT_ID && graphClient) return graphClientModule()
|
|
462
504
|
if (id === RUNTIME_ID) return runtimeModule(diagnostics)
|
|
463
505
|
// The checks register again once the real entry has loaded: a
|
|
464
506
|
// pre-bundled copy of it can carry its own copy of the Sygnal core
|
|
@@ -476,14 +518,25 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
476
518
|
if (mcpOn && server?.middlewares) {
|
|
477
519
|
attachMcp(server, root, mcpOptions, sygnalVersion(root))
|
|
478
520
|
}
|
|
479
|
-
|
|
521
|
+
const checkOn = options.check !== false
|
|
522
|
+
if (!checkOn && !graphOn) return
|
|
480
523
|
const checkOptions = typeof options.check === 'object' && options.check ? options.check : {}
|
|
481
|
-
// Deferred: the dev server doesn't wait for the first check
|
|
482
524
|
const checkDefaults = { strict: diagnostics.strict, ignore: diagnostics.ignore }
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
525
|
+
// Deferred: the dev server doesn't wait for the first check. `engine`: the live graph
|
|
526
|
+
// project (W-2), null without a sygnal-check that has it
|
|
527
|
+
engine = new Promise<LiveEngine | null>((resolve) => {
|
|
528
|
+
setTimeout(() => {
|
|
529
|
+
startDev(server, root, checkOptions, checkDefaults, options.check !== undefined, checkOn, graphOn).then(resolve, (err) => {
|
|
530
|
+
server?.config?.logger?.warn?.(`[sygnal] sygnal-check could not start: ${err?.message || err}`)
|
|
531
|
+
resolve(null)
|
|
532
|
+
})
|
|
533
|
+
}, 0)
|
|
534
|
+
})
|
|
535
|
+
// W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
|
|
536
|
+
if (graphOn && server?.middlewares) {
|
|
537
|
+
server.middlewares.use(graphMiddleware(server, root, engine))
|
|
538
|
+
if (graphAvailable(root)) printGraphUrl(server)
|
|
539
|
+
}
|
|
487
540
|
},
|
|
488
541
|
|
|
489
542
|
transform(this: any, code: string, id: string, opts?: { ssr?: boolean }) {
|
|
@@ -493,7 +546,10 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
493
546
|
? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
|
|
494
547
|
: devTransform.call(this, code, id, opts)
|
|
495
548
|
// 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
|
|
496
|
-
const
|
|
549
|
+
const words = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null
|
|
550
|
+
// 12. P-2 (D544): the static keys of the module's components (dev server, graph on)
|
|
551
|
+
const stamps = engine && isServe && !isVitest && !opts?.ssr ? componentStampTail(engine, id) : null
|
|
552
|
+
const tokens = words && stamps ? Promise.all([words, stamps]).then(([a, b]) => a + b) : words || stamps
|
|
497
553
|
if (!tokens) return result
|
|
498
554
|
return Promise.all([result, tokens]).then(([r, tail]: [any, string]) => {
|
|
499
555
|
if (!tail) return r
|
|
@@ -587,6 +643,19 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
587
643
|
}
|
|
588
644
|
}
|
|
589
645
|
|
|
646
|
+
// 12. PLAN-10 P-2 (D544): a module of the scanned files gets, in dev, a tail that sets each of its
|
|
647
|
+
// components' static key (sygnal-check's D521 'file#name', from the live graph engine's last build,
|
|
648
|
+
// built again first when the module's text changed) as a non-enumerable `__sygnalKey` on the
|
|
649
|
+
// function; the runtime inspect() reports it as `key`, and the graph page joins instances to cards.
|
|
650
|
+
function componentStampTail(engine: Promise<LiveEngine | null>, id: string): Promise<string> | null {
|
|
651
|
+
const file = cleanId(id)
|
|
652
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0')) return null
|
|
653
|
+
return engine.then((e) => {
|
|
654
|
+
if (!e) return ''
|
|
655
|
+
try { return stampTail(e.stampsFor(file) || []) } catch (_) { return '' }
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
|
|
590
659
|
// 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
|
|
591
660
|
// through as conditionally rendered when its words appear in the component's view; the runtime
|
|
592
661
|
// can read only the view function's own source. A component module (one that sets `X.intent` or
|
|
@@ -899,16 +968,41 @@ function checkInclude(root: string, include: string[] | undefined, notice: (m: s
|
|
|
899
968
|
return ['.']
|
|
900
969
|
}
|
|
901
970
|
|
|
902
|
-
/**
|
|
903
|
-
|
|
904
|
-
|
|
971
|
+
/**
|
|
972
|
+
* Load sygnal-check and start what dev uses it for: the checker (`check`, reported to the terminal
|
|
973
|
+
* and the browser) and the live graph engine (`graph`, PLAN-10 W-2: the page, its stream and the
|
|
974
|
+
* component stamps). With an engine, both come from one build. Resolves to the engine, or null.
|
|
975
|
+
*/
|
|
976
|
+
async function startDev(server: any, root: string, opts: CheckPluginOptions,
|
|
977
|
+
defaults: { strict: boolean, ignore: string[] }, explicit: boolean, checkOn: boolean, graphOn: boolean): Promise<LiveEngine | null> {
|
|
905
978
|
const mod = await loadSygnalCheck(root)
|
|
906
979
|
const logger = server?.config?.logger || console
|
|
907
980
|
if (!mod || typeof mod.check !== 'function') {
|
|
908
|
-
if (explicit) logger.info('[sygnal] sygnal-check is not installed, so static checks are off (npm i -D sygnal-check)')
|
|
909
|
-
return
|
|
981
|
+
if (explicit && checkOn) logger.info('[sygnal] sygnal-check is not installed, so static checks are off (npm i -D sygnal-check)')
|
|
982
|
+
return null
|
|
910
983
|
}
|
|
911
|
-
const
|
|
984
|
+
const setup = checkSetup(mod, root, opts, defaults, (m: string) => logger.info(m))
|
|
985
|
+
// PLAN-10 W-2: a sygnal-check with the live project and the page (feature-detected, so an older
|
|
986
|
+
// one keeps the checker as it was)
|
|
987
|
+
const engine = graphOn && setup.configAware && typeof mod.createLiveProject === 'function' && typeof mod.graphHtml === 'function'
|
|
988
|
+
? createEngine(mod, root, {
|
|
989
|
+
paths: setup.include, strict: setup.checkOptions.strict, a11y: setup.checkOptions.a11y,
|
|
990
|
+
ignore: setup.checkOptions.ignore, defaults: setup.checkOptions.defaults,
|
|
991
|
+
}, logger, true)
|
|
992
|
+
: null
|
|
993
|
+
if (checkOn) startChecker(server, root, mod, setup, opts, engine)
|
|
994
|
+
if (engine) {
|
|
995
|
+
if (!engine.last) engine.build()
|
|
996
|
+
engine.watch(server)
|
|
997
|
+
}
|
|
998
|
+
return engine
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
interface CheckSetup { include: string[]; checkOptions: any; configAware: boolean }
|
|
1002
|
+
|
|
1003
|
+
/** What sygnal-check checks, with which options (see the `check` option). */
|
|
1004
|
+
function checkSetup(mod: any, root: string, opts: CheckPluginOptions,
|
|
1005
|
+
defaults: { strict: boolean, ignore: string[] }, notice: (m: string) => void): CheckSetup {
|
|
912
1006
|
// PLAN-7 C-5 (D299): a sygnal-check with config support resolves the settings itself:
|
|
913
1007
|
// explicit check.* options > its config file > the `diagnostics` defaults passed as `defaults`.
|
|
914
1008
|
// An older one gets exactly what it got before.
|
|
@@ -940,6 +1034,17 @@ async function startChecker(server: any, root: string, opts: CheckPluginOptions,
|
|
|
940
1034
|
a11y: opts.a11y === 'error' ? 'error' : 'warn',
|
|
941
1035
|
}
|
|
942
1036
|
}
|
|
1037
|
+
return { include, checkOptions, configAware }
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Run sygnal-check now and after source changes; report to the terminal and the browser. With the
|
|
1042
|
+
* live graph engine (W-2) the findings come from its builds (one build for the page and the
|
|
1043
|
+
* checker); else from check() on each change.
|
|
1044
|
+
*/
|
|
1045
|
+
function startChecker(server: any, root: string, mod: any, setup: CheckSetup, opts: CheckPluginOptions, engine: LiveEngine | null) {
|
|
1046
|
+
const logger = server?.config?.logger || console
|
|
1047
|
+
const { include, checkOptions, configAware } = setup
|
|
943
1048
|
const a11y = checkOptions.a11y
|
|
944
1049
|
// Only error-severity findings open Vite's overlay: while an overlay is open
|
|
945
1050
|
// Vite's client reloads the page on the first HMR update, so warnings stay
|
|
@@ -966,13 +1071,16 @@ async function startChecker(server: any, root: string, opts: CheckPluginOptions,
|
|
|
966
1071
|
} catch (_) {}
|
|
967
1072
|
}
|
|
968
1073
|
|
|
969
|
-
const run = (initial = false) => {
|
|
1074
|
+
const run = (initial = false, built?: any[]) => {
|
|
970
1075
|
let diags: any[]
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1076
|
+
if (built) diags = built
|
|
1077
|
+
else {
|
|
1078
|
+
try {
|
|
1079
|
+
diags = mod.check(include, checkOptions)
|
|
1080
|
+
} catch (err: any) {
|
|
1081
|
+
logger.warn(`[sygnal] sygnal-check failed: ${err?.message || err}`, { timestamp: true })
|
|
1082
|
+
return
|
|
1083
|
+
}
|
|
976
1084
|
}
|
|
977
1085
|
// A sygnal-check from before D144 reports SYG7xx as errors under strict: keep them warnings
|
|
978
1086
|
// (one with config support postdates D144 and resolves a11y itself)
|
|
@@ -1011,7 +1119,12 @@ async function startChecker(server: any, root: string, opts: CheckPluginOptions,
|
|
|
1011
1119
|
if (!initial) broadcast(changed)
|
|
1012
1120
|
}
|
|
1013
1121
|
|
|
1014
|
-
|
|
1122
|
+
if (engine) {
|
|
1123
|
+
// every build of the engine: the first on start, then after changes (its own watcher)
|
|
1124
|
+
let first = !engine.last
|
|
1125
|
+
engine.onBuild((b) => { const initial = first; first = false; run(initial, b.diagnostics || []) })
|
|
1126
|
+
if (engine.last) run(true, engine.last.diagnostics || [])
|
|
1127
|
+
} else run(true)
|
|
1015
1128
|
// A page that loads (or reloads) asks for the current findings: answer
|
|
1016
1129
|
// that client only, never every client.
|
|
1017
1130
|
try {
|
|
@@ -1023,6 +1136,7 @@ async function startChecker(server: any, root: string, opts: CheckPluginOptions,
|
|
|
1023
1136
|
})
|
|
1024
1137
|
} catch (_) {}
|
|
1025
1138
|
|
|
1139
|
+
if (engine) return
|
|
1026
1140
|
let timer: any
|
|
1027
1141
|
const onChange = (file: string) => {
|
|
1028
1142
|
if (file.split(/[\\/]/).includes('node_modules')) return
|